mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-26 05:49:25 +01:00
parent
9de9fcf069
commit
f920d3b79f
@ -15,12 +15,6 @@ from collections import namedtuple
|
|||||||
CapDef = namedtuple("CapDef", ['identifier', 'name', 'url', 'standard'])
|
CapDef = namedtuple("CapDef", ['identifier', 'name', 'url', 'standard'])
|
||||||
|
|
||||||
CAPDEFS = [
|
CAPDEFS = [
|
||||||
CapDef(
|
|
||||||
identifier="Acc",
|
|
||||||
name="draft/acc",
|
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/276",
|
|
||||||
standard="proposed IRCv3",
|
|
||||||
),
|
|
||||||
CapDef(
|
CapDef(
|
||||||
identifier="AccountNotify",
|
identifier="AccountNotify",
|
||||||
name="account-notify",
|
name="account-notify",
|
||||||
@ -163,7 +157,7 @@ CAPDEFS = [
|
|||||||
identifier="EventPlayback",
|
identifier="EventPlayback",
|
||||||
name="draft/event-playback",
|
name="draft/event-playback",
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/362",
|
url="https://github.com/ircv3/ircv3-specifications/pull/362",
|
||||||
standard="Proposed IRCv3",
|
standard="proposed IRCv3",
|
||||||
),
|
),
|
||||||
CapDef(
|
CapDef(
|
||||||
identifier="ZNCPlayback",
|
identifier="ZNCPlayback",
|
||||||
@ -181,7 +175,7 @@ CAPDEFS = [
|
|||||||
identifier="Multiline",
|
identifier="Multiline",
|
||||||
name="draft/multiline",
|
name="draft/multiline",
|
||||||
url="https://github.com/ircv3/ircv3-specifications/pull/398",
|
url="https://github.com/ircv3/ircv3-specifications/pull/398",
|
||||||
standard="Proposed IRCv3",
|
standard="proposed IRCv3",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
233
irc/accounts.go
233
irc/accounts.go
@ -36,6 +36,8 @@ const (
|
|||||||
|
|
||||||
keyVHostQueueAcctToId = "vhostQueue %s"
|
keyVHostQueueAcctToId = "vhostQueue %s"
|
||||||
vhostRequestIdx = "vhostQueue"
|
vhostRequestIdx = "vhostQueue"
|
||||||
|
|
||||||
|
maxCertfpsPerAccount = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// everything about accounts is persistent; therefore, the database is the authoritative
|
// everything about accounts is persistent; therefore, the database is the authoritative
|
||||||
@ -327,7 +329,14 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
|||||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||||
|
|
||||||
credStr, err := am.serializeCredentials(passphrase, certfp)
|
var creds AccountCredentials
|
||||||
|
creds.Version = 1
|
||||||
|
err = creds.SetPassphrase(passphrase, am.server.Config().Accounts.Registration.BcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
creds.AddCertfp(certfp)
|
||||||
|
credStr, err := creds.Serialize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -411,58 +420,124 @@ func validatePassphrase(passphrase string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper to assemble the serialized JSON for an account's credentials
|
|
||||||
func (am *AccountManager) serializeCredentials(passphrase string, certfp string) (result string, err error) {
|
|
||||||
var creds AccountCredentials
|
|
||||||
creds.Version = 1
|
|
||||||
// we need at least one of passphrase and certfp:
|
|
||||||
if passphrase == "" && certfp == "" {
|
|
||||||
return "", errAccountBadPassphrase
|
|
||||||
}
|
|
||||||
// but if we have one, it's fine if the other is missing, it just means no
|
|
||||||
// credential of that type will be accepted.
|
|
||||||
creds.Certificate = certfp
|
|
||||||
if passphrase != "" {
|
|
||||||
if validatePassphrase(passphrase) != nil {
|
|
||||||
return "", errAccountBadPassphrase
|
|
||||||
}
|
|
||||||
bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
|
|
||||||
creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
|
|
||||||
if err != nil {
|
|
||||||
am.server.logger.Error("internal", "could not hash password", err.Error())
|
|
||||||
return "", errAccountCreation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
credText, err := json.Marshal(creds)
|
|
||||||
if err != nil {
|
|
||||||
am.server.logger.Error("internal", "could not marshal credentials", err.Error())
|
|
||||||
return "", errAccountCreation
|
|
||||||
}
|
|
||||||
return string(credText), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// changes the password for an account
|
// changes the password for an account
|
||||||
func (am *AccountManager) setPassword(account string, password string) (err error) {
|
func (am *AccountManager) setPassword(account string, password string, hasPrivs bool) (err error) {
|
||||||
casefoldedAccount, err := CasefoldName(account)
|
cfAccount, err := CasefoldName(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errAccountDoesNotExist
|
||||||
}
|
}
|
||||||
act, err := am.LoadAccount(casefoldedAccount)
|
|
||||||
|
credKey := fmt.Sprintf(keyAccountCredentials, cfAccount)
|
||||||
|
var credStr string
|
||||||
|
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
// no need to check verification status here or below;
|
||||||
|
// you either need to be auth'ed to the account or be an oper to do this
|
||||||
|
credStr, err = tx.Get(credKey)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errAccountDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
var creds AccountCredentials
|
||||||
|
err = json.Unmarshal([]byte(credStr), &creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
credStr, err := am.serializeCredentials(password, act.Credentials.Certificate)
|
err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
if creds.Empty() && !hasPrivs {
|
||||||
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
return errEmptyCredentials
|
||||||
_, _, err := tx.Set(credentialsKey, credStr, nil)
|
}
|
||||||
|
|
||||||
|
newCredStr, err := creds.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
curCredStr, err := tx.Get(credKey)
|
||||||
|
if credStr != curCredStr {
|
||||||
|
return errCASFailed
|
||||||
|
}
|
||||||
|
_, _, err = tx.Set(credKey, newCredStr, nil)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
|
||||||
|
certfp, err = utils.NormalizeCertfp(certfp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfAccount, err := CasefoldName(account)
|
||||||
|
if err != nil {
|
||||||
|
return errAccountDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
credKey := fmt.Sprintf(keyAccountCredentials, cfAccount)
|
||||||
|
var credStr string
|
||||||
|
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
credStr, err = tx.Get(credKey)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errAccountDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
var creds AccountCredentials
|
||||||
|
err = json.Unmarshal([]byte(credStr), &creds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if add {
|
||||||
|
err = creds.AddCertfp(certfp)
|
||||||
|
} else {
|
||||||
|
err = creds.RemoveCertfp(certfp)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if creds.Empty() && !hasPrivs {
|
||||||
|
return errEmptyCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
newCredStr, err := creds.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
certfpKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||||
|
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
curCredStr, err := tx.Get(credKey)
|
||||||
|
if credStr != curCredStr {
|
||||||
|
return errCASFailed
|
||||||
|
}
|
||||||
|
if add {
|
||||||
|
_, err = tx.Get(certfpKey)
|
||||||
|
if err != buntdb.ErrNotFound {
|
||||||
|
return errCertfpAlreadyExists
|
||||||
|
}
|
||||||
|
tx.Set(certfpKey, cfAccount, nil)
|
||||||
|
} else {
|
||||||
|
tx.Delete(certfpKey)
|
||||||
|
}
|
||||||
|
_, _, err = tx.Set(credKey, newCredStr, nil)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
|
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
|
||||||
@ -574,8 +649,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
|||||||
// XXX we shouldn't do (de)serialization inside the txn,
|
// XXX we shouldn't do (de)serialization inside the txn,
|
||||||
// but this is like 2 usec on my system
|
// but this is like 2 usec on my system
|
||||||
json.Unmarshal([]byte(raw.Credentials), &creds)
|
json.Unmarshal([]byte(raw.Credentials), &creds)
|
||||||
if creds.Certificate != "" {
|
for _, cert := range creds.Certfps {
|
||||||
certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
|
certFPKey := fmt.Sprintf(keyCertToAccount, cert)
|
||||||
tx.Set(certFPKey, casefoldedAccount, nil)
|
tx.Set(certFPKey, casefoldedAccount, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -906,8 +981,9 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var creds AccountCredentials
|
var creds AccountCredentials
|
||||||
if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
|
if err = json.Unmarshal([]byte(credText), &creds); err == nil {
|
||||||
certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
|
for _, cert := range creds.Certfps {
|
||||||
|
certFPKey := fmt.Sprintf(keyCertToAccount, cert)
|
||||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
|
if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
|
||||||
tx.Delete(certFPKey)
|
tx.Delete(certFPKey)
|
||||||
@ -916,6 +992,7 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
skeleton, _ := Skeleton(accountName)
|
skeleton, _ := Skeleton(accountName)
|
||||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||||
@ -1326,7 +1403,73 @@ type AccountCredentials struct {
|
|||||||
Version uint
|
Version uint
|
||||||
PassphraseSalt []byte // legacy field, not used by v1 and later
|
PassphraseSalt []byte // legacy field, not used by v1 and later
|
||||||
PassphraseHash []byte
|
PassphraseHash []byte
|
||||||
Certificate string // fingerprint
|
Certfps []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AccountCredentials) Empty() bool {
|
||||||
|
return len(ac.PassphraseHash) == 0 && len(ac.Certfps) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to assemble the serialized JSON for an account's credentials
|
||||||
|
func (ac *AccountCredentials) Serialize() (result string, err error) {
|
||||||
|
ac.Version = 1
|
||||||
|
credText, err := json.Marshal(*ac)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(credText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
|
||||||
|
if passphrase == "" {
|
||||||
|
ac.PassphraseHash = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if validatePassphrase(passphrase) != nil {
|
||||||
|
return errAccountBadPassphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), int(bcryptCost))
|
||||||
|
if err != nil {
|
||||||
|
return errAccountBadPassphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AccountCredentials) AddCertfp(certfp string) (err error) {
|
||||||
|
for _, current := range ac.Certfps {
|
||||||
|
if certfp == current {
|
||||||
|
return errNoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxCertfpsPerAccount <= len(ac.Certfps) {
|
||||||
|
return errLimitExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.Certfps = append(ac.Certfps, certfp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AccountCredentials) RemoveCertfp(certfp string) (err error) {
|
||||||
|
found := false
|
||||||
|
newList := make([]string, 0, len(ac.Certfps))
|
||||||
|
for _, current := range ac.Certfps {
|
||||||
|
if current == certfp {
|
||||||
|
found = true
|
||||||
|
} else {
|
||||||
|
newList = append(newList, current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
// this is important because it prevents you from deleting someone else's
|
||||||
|
// fingerprint record
|
||||||
|
return errNoop
|
||||||
|
}
|
||||||
|
ac.Certfps = newList
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type BouncerAllowedSetting int
|
type BouncerAllowedSetting int
|
||||||
|
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 28
|
numCapabs = 27
|
||||||
// length of the uint64 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
@ -37,11 +37,7 @@ const (
|
|||||||
// https://ircv3.net/specs/extensions/chghost-3.2.html
|
// https://ircv3.net/specs/extensions/chghost-3.2.html
|
||||||
ChgHost Capability = iota
|
ChgHost Capability = iota
|
||||||
|
|
||||||
// Acc is the proposed IRCv3 capability named "draft/acc":
|
// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/276
|
|
||||||
Acc Capability = iota
|
|
||||||
|
|
||||||
// EventPlayback is the Proposed IRCv3 capability named "draft/event-playback":
|
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/362
|
// https://github.com/ircv3/ircv3-specifications/pull/362
|
||||||
EventPlayback Capability = iota
|
EventPlayback Capability = iota
|
||||||
|
|
||||||
@ -53,7 +49,7 @@ const (
|
|||||||
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
|
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
|
||||||
Languages Capability = iota
|
Languages Capability = iota
|
||||||
|
|
||||||
// Multiline is the Proposed IRCv3 capability named "draft/multiline":
|
// Multiline is the proposed IRCv3 capability named "draft/multiline":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/398
|
// https://github.com/ircv3/ircv3-specifications/pull/398
|
||||||
Multiline Capability = iota
|
Multiline Capability = iota
|
||||||
|
|
||||||
@ -135,7 +131,6 @@ var (
|
|||||||
"batch",
|
"batch",
|
||||||
"cap-notify",
|
"cap-notify",
|
||||||
"chghost",
|
"chghost",
|
||||||
"draft/acc",
|
|
||||||
"draft/event-playback",
|
"draft/event-playback",
|
||||||
"draft/labeled-response-0.2",
|
"draft/labeled-response-0.2",
|
||||||
"draft/languages",
|
"draft/languages",
|
||||||
|
@ -134,6 +134,7 @@ set using PURGE.`,
|
|||||||
|
|
||||||
INFO displays info about a registered channel.`,
|
INFO displays info about a registered channel.`,
|
||||||
helpShort: `$bINFO$b displays info about a registered channel.`,
|
helpShort: `$bINFO$b displays info about a registered channel.`,
|
||||||
|
enabled: chanregEnabled,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1419,10 +1419,11 @@ func (client *Client) attemptAutoOper(session *Session) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, oper := range client.server.Config().operators {
|
for _, oper := range client.server.Config().operators {
|
||||||
if oper.Auto && oper.Pass == nil && utils.CertfpsMatch(oper.Fingerprint, client.certfp) {
|
if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == client.certfp {
|
||||||
rb := NewResponseBuffer(session)
|
rb := NewResponseBuffer(session)
|
||||||
applyOper(client, oper, rb)
|
applyOper(client, oper, rb)
|
||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,11 +80,6 @@ var Commands map[string]Command
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Commands = map[string]Command{
|
Commands = map[string]Command{
|
||||||
"ACC": {
|
|
||||||
handler: accHandler,
|
|
||||||
usablePreReg: true,
|
|
||||||
minParams: 1,
|
|
||||||
},
|
|
||||||
"AMBIANCE": {
|
"AMBIANCE": {
|
||||||
handler: sceneHandler,
|
handler: sceneHandler,
|
||||||
minParams: 2,
|
minParams: 2,
|
||||||
|
@ -511,7 +511,12 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
|
|||||||
return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error())
|
return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
oper.Fingerprint = opConf.Fingerprint
|
if opConf.Fingerprint != "" {
|
||||||
|
oper.Fingerprint, err = utils.NormalizeCertfp(opConf.Fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Oper %s has an invalid fingerprint: %s", oper.Name, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
oper.Auto = opConf.Auto
|
oper.Auto = opConf.Auto
|
||||||
|
|
||||||
if oper.Pass == nil && oper.Fingerprint == "" {
|
if oper.Pass == nil && oper.Fingerprint == "" {
|
||||||
|
@ -22,7 +22,7 @@ const (
|
|||||||
// 'version' of the database schema
|
// 'version' of the database schema
|
||||||
keySchemaVersion = "db.version"
|
keySchemaVersion = "db.version"
|
||||||
// latest schema of the db
|
// latest schema of the db
|
||||||
latestDbSchema = "8"
|
latestDbSchema = "9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SchemaChanger func(*Config, *buntdb.Tx) error
|
type SchemaChanger func(*Config, *buntdb.Tx) error
|
||||||
@ -553,6 +553,57 @@ func schemaChangeV7ToV8(config *Config, tx *buntdb.Tx) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type accountCredsLegacyV8 struct {
|
||||||
|
Version uint
|
||||||
|
PassphraseSalt []byte // legacy field, not used by v1 and later
|
||||||
|
PassphraseHash []byte
|
||||||
|
Certificate string
|
||||||
|
}
|
||||||
|
|
||||||
|
type accountCredsLegacyV9 struct {
|
||||||
|
Version uint
|
||||||
|
PassphraseSalt []byte // legacy field, not used by v1 and later
|
||||||
|
PassphraseHash []byte
|
||||||
|
Certfps []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// #530: support multiple client certificate fingerprints
|
||||||
|
func schemaChangeV8ToV9(config *Config, tx *buntdb.Tx) error {
|
||||||
|
prefix := "account.credentials "
|
||||||
|
var accounts, blobs []string
|
||||||
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||||
|
var legacy accountCredsLegacyV8
|
||||||
|
var current accountCredsLegacyV9
|
||||||
|
if !strings.HasPrefix(key, prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
account := strings.TrimPrefix(key, prefix)
|
||||||
|
err := json.Unmarshal([]byte(value), &legacy)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("corrupt record for %s: %v\n", account, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
current.Version = legacy.Version
|
||||||
|
current.PassphraseSalt = legacy.PassphraseSalt // ugh can't get rid of this
|
||||||
|
current.PassphraseHash = legacy.PassphraseHash
|
||||||
|
if legacy.Certificate != "" {
|
||||||
|
current.Certfps = []string{legacy.Certificate}
|
||||||
|
}
|
||||||
|
blob, err := json.Marshal(current)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not marshal record for %s: %v\n", account, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
accounts = append(accounts, account)
|
||||||
|
blobs = append(blobs, string(blob))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
for i, account := range accounts {
|
||||||
|
tx.Set(prefix+account, blobs[i], nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
allChanges := []SchemaChange{
|
allChanges := []SchemaChange{
|
||||||
{
|
{
|
||||||
@ -590,6 +641,11 @@ func init() {
|
|||||||
TargetVersion: "8",
|
TargetVersion: "8",
|
||||||
Changer: schemaChangeV7ToV8,
|
Changer: schemaChangeV7ToV8,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
InitialVersion: "8",
|
||||||
|
TargetVersion: "9",
|
||||||
|
Changer: schemaChangeV8ToV9,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// build the index
|
// build the index
|
||||||
|
@ -50,6 +50,10 @@ var (
|
|||||||
errBanned = errors.New("IP or nickmask banned")
|
errBanned = errors.New("IP or nickmask banned")
|
||||||
errInvalidParams = utils.ErrInvalidParams
|
errInvalidParams = utils.ErrInvalidParams
|
||||||
errNoVhost = errors.New(`You do not have an approved vhost`)
|
errNoVhost = errors.New(`You do not have an approved vhost`)
|
||||||
|
errLimitExceeded = errors.New("Limit exceeded")
|
||||||
|
errNoop = errors.New("Action was a no-op")
|
||||||
|
errCASFailed = errors.New("Compare-and-swap update of database value failed")
|
||||||
|
errEmptyCredentials = errors.New("No more credentials are approved")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Socket Errors
|
// Socket Errors
|
||||||
|
@ -39,13 +39,17 @@ type webircConfig struct {
|
|||||||
// Populate fills out our password or fingerprint.
|
// Populate fills out our password or fingerprint.
|
||||||
func (wc *webircConfig) Populate() (err error) {
|
func (wc *webircConfig) Populate() (err error) {
|
||||||
if wc.Fingerprint == "" && wc.PasswordString == "" {
|
if wc.Fingerprint == "" && wc.PasswordString == "" {
|
||||||
return ErrNoFingerprintOrPassword
|
err = ErrNoFingerprintOrPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
if wc.PasswordString != "" {
|
if err == nil && wc.PasswordString != "" {
|
||||||
wc.Password, err = decodeLegacyPasswordHash(wc.PasswordString)
|
wc.Password, err = decodeLegacyPasswordHash(wc.PasswordString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil && wc.Fingerprint != "" {
|
||||||
|
wc.Fingerprint, err = utils.NormalizeCertfp(wc.Fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
wc.allowedNets, err = utils.ParseNetList(wc.Hosts)
|
wc.allowedNets, err = utils.ParseNetList(wc.Hosts)
|
||||||
}
|
}
|
||||||
|
192
irc/handlers.go
192
irc/handlers.go
@ -31,52 +31,6 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ACC [LS|REGISTER|VERIFY] ...
|
|
||||||
func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
|
||||||
subcommand := strings.ToLower(msg.Params[0])
|
|
||||||
|
|
||||||
if subcommand == "ls" {
|
|
||||||
config := server.Config().Accounts
|
|
||||||
|
|
||||||
rb.Add(nil, server.name, "ACC", "LS", "SUBCOMMANDS", "LS REGISTER VERIFY")
|
|
||||||
|
|
||||||
// this list is sorted by the config loader, yay
|
|
||||||
rb.Add(nil, server.name, "ACC", "LS", "CALLBACKS", strings.Join(config.Registration.EnabledCallbacks, " "))
|
|
||||||
|
|
||||||
rb.Add(nil, server.name, "ACC", "LS", "CREDTYPES", "passphrase certfp")
|
|
||||||
|
|
||||||
flags := []string{"nospaces"}
|
|
||||||
if config.NickReservation.Enabled {
|
|
||||||
flags = append(flags, "regnick")
|
|
||||||
}
|
|
||||||
sort.Strings(flags)
|
|
||||||
rb.Add(nil, server.name, "ACC", "LS", "FLAGS", strings.Join(flags, " "))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// disallow account stuff before connection registration has completed, for now
|
|
||||||
if !client.Registered() {
|
|
||||||
client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure reg is enabled
|
|
||||||
if !server.AccountConfig().Registration.Enabled {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNAVAILABLE", client.t("Account registration is disabled"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if subcommand == "register" {
|
|
||||||
return accRegisterHandler(server, client, msg, rb)
|
|
||||||
} else if subcommand == "verify" {
|
|
||||||
return accVerifyHandler(server, client, msg, rb)
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234
|
// helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234
|
||||||
func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
|
func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
|
||||||
callback := strings.ToLower(spec)
|
callback := strings.ToLower(spec)
|
||||||
@ -103,113 +57,6 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
|
|
||||||
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
|
||||||
nick := client.Nick()
|
|
||||||
|
|
||||||
if len(msg.Params) < 4 {
|
|
||||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
account := msg.Params[1]
|
|
||||||
|
|
||||||
// check for account name of *
|
|
||||||
if account == "*" {
|
|
||||||
account = nick
|
|
||||||
} else {
|
|
||||||
if server.Config().Accounts.NickReservation.Enabled {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_MUST_USE_REGNICK", account, client.t("Must register with current nickname instead of separate account name"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clients can't reg new accounts if they're already logged in
|
|
||||||
if client.LoggedIntoAccount() {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNSPECIFIED_ERROR", account, client.t("You're already logged into an account"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitise account name
|
|
||||||
casefoldedAccount, err := CasefoldName(account)
|
|
||||||
if err != nil {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_ACCOUNT_NAME", account, client.t("Account name is not valid"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackSpec := msg.Params[2]
|
|
||||||
callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
|
|
||||||
|
|
||||||
if callbackNamespace == "" {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CALLBACK", account, callbackSpec, client.t("Cannot send verification code there"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// get credential type/value
|
|
||||||
var credentialType, credentialValue string
|
|
||||||
|
|
||||||
if len(msg.Params) > 4 {
|
|
||||||
credentialType = strings.ToLower(msg.Params[3])
|
|
||||||
credentialValue = msg.Params[4]
|
|
||||||
} else {
|
|
||||||
// exactly 4 params
|
|
||||||
credentialType = "passphrase" // default from the spec
|
|
||||||
credentialValue = msg.Params[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the credential type is valid
|
|
||||||
var credentialValid bool
|
|
||||||
for _, name := range server.AccountConfig().Registration.EnabledCredentialTypes {
|
|
||||||
if credentialType == name {
|
|
||||||
credentialValid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if credentialType == "certfp" && client.certfp == "" {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CREDENTIAL", account, client.t("You must connect with a TLS client certificate to use certfp"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !credentialValid {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CRED_TYPE", account, credentialType, client.t("Credential type is not supported"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var passphrase, certfp string
|
|
||||||
if credentialType == "certfp" {
|
|
||||||
certfp = client.certfp
|
|
||||||
} else if credentialType == "passphrase" {
|
|
||||||
passphrase = credentialValue
|
|
||||||
}
|
|
||||||
|
|
||||||
throttled, remainingTime := client.loginThrottle.Touch()
|
|
||||||
if throttled {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNSPECIFIED_ERROR", account, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
|
|
||||||
if err != nil {
|
|
||||||
msg, code := registrationErrorToMessageAndCode(err)
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", code, account, client.t(msg))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// automatically complete registration
|
|
||||||
if callbackNamespace == "*" {
|
|
||||||
err := server.accounts.Verify(client, casefoldedAccount, "")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
sendSuccessfulRegResponse(client, rb, false)
|
|
||||||
} else {
|
|
||||||
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s")
|
|
||||||
message := fmt.Sprintf(messageTemplate, fmt.Sprintf("%s:%s", callbackNamespace, callbackValue))
|
|
||||||
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, nick, casefoldedAccount, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func registrationErrorToMessageAndCode(err error) (message, code string) {
|
func registrationErrorToMessageAndCode(err error) (message, code string) {
|
||||||
// default responses: let's be risk-averse about displaying internal errors
|
// default responses: let's be risk-averse about displaying internal errors
|
||||||
// to the clients, especially for something as sensitive as accounts
|
// to the clients, especially for something as sensitive as accounts
|
||||||
@ -263,41 +110,6 @@ func sendSuccessfulAccountAuth(client *Client, rb *ResponseBuffer, forNS, forSAS
|
|||||||
client.server.logger.Info("accounts", "client", details.nick, "logged into account", details.accountName)
|
client.server.logger.Info("accounts", "client", details.nick, "logged into account", details.accountName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACC VERIFY <accountname> <auth_code>
|
|
||||||
func accVerifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
|
||||||
account := strings.TrimSpace(msg.Params[1])
|
|
||||||
|
|
||||||
if len(msg.Params) < 3 {
|
|
||||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, client.t("Not enough parameters"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
err := server.accounts.Verify(client, account, msg.Params[2])
|
|
||||||
|
|
||||||
var code string
|
|
||||||
var message string
|
|
||||||
|
|
||||||
if err == errAccountVerificationInvalidCode {
|
|
||||||
code = "ACCOUNT_INVALID_VERIFY_CODE"
|
|
||||||
message = err.Error()
|
|
||||||
} else if err == errAccountAlreadyVerified {
|
|
||||||
code = "ACCOUNT_ALREADY_VERIFIED"
|
|
||||||
message = err.Error()
|
|
||||||
} else if err != nil {
|
|
||||||
code = "VERIFY_UNSPECIFIED_ERROR"
|
|
||||||
message = errAccountVerificationFailed.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
rb.Add(nil, server.name, RPL_VERIFY_SUCCESS, client.Nick(), account, client.t("Account verification successful"))
|
|
||||||
sendSuccessfulAccountAuth(client, rb, false, false)
|
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "ACC", code, account, client.t(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AUTHENTICATE [<mechanism>|<data>|*]
|
// AUTHENTICATE [<mechanism>|<data>|*]
|
||||||
func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
@ -2300,7 +2112,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
oper := server.GetOperator(msg.Params[0])
|
oper := server.GetOperator(msg.Params[0])
|
||||||
if oper != nil {
|
if oper != nil {
|
||||||
if oper.Fingerprint != "" {
|
if oper.Fingerprint != "" {
|
||||||
if utils.CertfpsMatch(oper.Fingerprint, client.certfp) {
|
if oper.Fingerprint == client.certfp {
|
||||||
checkPassed = true
|
checkPassed = true
|
||||||
} else {
|
} else {
|
||||||
checkFailed = true
|
checkFailed = true
|
||||||
@ -2772,7 +2584,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
|
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if 0 < len(info.Fingerprint) && !utils.CertfpsMatch(info.Fingerprint, client.certfp) {
|
if info.Fingerprint != "" && info.Fingerprint != client.certfp {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ func handleLegacyPasswordV0(server *Server, account string, credentials AccountC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// upgrade credentials
|
// upgrade credentials
|
||||||
err = server.accounts.setPassword(account, passphrase)
|
err = server.accounts.setPassword(account, passphrase, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err))
|
server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err))
|
||||||
}
|
}
|
||||||
|
157
irc/nickserv.go
157
irc/nickserv.go
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/goshuirc/irc-go/ircfmt"
|
"github.com/goshuirc/irc-go/ircfmt"
|
||||||
|
|
||||||
"github.com/oragono/oragono/irc/modes"
|
"github.com/oragono/oragono/irc/modes"
|
||||||
|
"github.com/oragono/oragono/irc/passwd"
|
||||||
"github.com/oragono/oragono/irc/sno"
|
"github.com/oragono/oragono/irc/sno"
|
||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
)
|
)
|
||||||
@ -72,6 +73,7 @@ entry for $bSET$b for more information.`,
|
|||||||
GHOST disconnects the given user from the network if they're logged in with the
|
GHOST disconnects the given user from the network if they're logged in with the
|
||||||
same user account, letting you reclaim your nickname.`,
|
same user account, letting you reclaim your nickname.`,
|
||||||
helpShort: `$bGHOST$b reclaims your nickname.`,
|
helpShort: `$bGHOST$b reclaims your nickname.`,
|
||||||
|
enabled: servCmdRequiresAuthEnabled,
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
@ -92,6 +94,7 @@ will not be able to use it.`,
|
|||||||
IDENTIFY lets you login to the given username using either password auth, or
|
IDENTIFY lets you login to the given username using either password auth, or
|
||||||
certfp (your client certificate) if a password is not given.`,
|
certfp (your client certificate) if a password is not given.`,
|
||||||
helpShort: `$bIDENTIFY$b lets you login to your account.`,
|
helpShort: `$bIDENTIFY$b lets you login to your account.`,
|
||||||
|
enabled: servCmdRequiresAuthEnabled,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
@ -179,7 +182,8 @@ Or: $bPASSWD <username> <new>$b
|
|||||||
PASSWD lets you change your account password. You must supply your current
|
PASSWD lets you change your account password. You must supply your current
|
||||||
password and confirm the new one by typing it twice. If you're an IRC operator
|
password and confirm the new one by typing it twice. If you're an IRC operator
|
||||||
with the correct permissions, you can use PASSWD to reset someone else's
|
with the correct permissions, you can use PASSWD to reset someone else's
|
||||||
password by supplying their username and then the desired password.`,
|
password by supplying their username and then the desired password. To
|
||||||
|
indicate an empty password, use * instead.`,
|
||||||
helpShort: `$bPASSWD$b lets you change your password.`,
|
helpShort: `$bPASSWD$b lets you change your password.`,
|
||||||
enabled: servCmdRequiresAuthEnabled,
|
enabled: servCmdRequiresAuthEnabled,
|
||||||
minParams: 2,
|
minParams: 2,
|
||||||
@ -259,6 +263,20 @@ information on the settings and their possible values, see HELP SET.`,
|
|||||||
minParams: 3,
|
minParams: 3,
|
||||||
capabs: []string{"accreg"},
|
capabs: []string{"accreg"},
|
||||||
},
|
},
|
||||||
|
"cert": {
|
||||||
|
handler: nsCertHandler,
|
||||||
|
help: `Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b
|
||||||
|
|
||||||
|
CERT examines or modifies the TLS certificate fingerprints that can be used to
|
||||||
|
log into an account. Specifically, $bCERT LIST$b lists the authorized
|
||||||
|
fingerprints, $bCERT ADD <fingerprint>$b adds a new fingerprint, and
|
||||||
|
$bCERT DEL <fingerprint>$b removes a fingerprint. If you're an IRC operator
|
||||||
|
with the correct permissions, you can act on another user's account, for
|
||||||
|
example with $bCERT ADD <account> <fingerprint>$b.`,
|
||||||
|
helpShort: `$bCERT$b controls a user account's certificate fingerprints`,
|
||||||
|
enabled: servCmdRequiresAuthEnabled,
|
||||||
|
minParams: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -548,6 +566,11 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
|
|||||||
}
|
}
|
||||||
|
|
||||||
func nsInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func nsInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
if !server.Config().Accounts.AuthenticationEnabled && !client.HasRoleCapabs("accreg") {
|
||||||
|
nsNotice(rb, client.t("This command has been disabled by the server administrators"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var accountName string
|
var accountName string
|
||||||
if len(params) > 0 {
|
if len(params) > 0 {
|
||||||
nick := params[0]
|
nick := params[0]
|
||||||
@ -659,6 +682,9 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
|
|||||||
|
|
||||||
func nsSaregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func nsSaregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
account, passphrase := params[0], params[1]
|
account, passphrase := params[0], params[1]
|
||||||
|
if passphrase == "*" {
|
||||||
|
passphrase = ""
|
||||||
|
}
|
||||||
err := server.accounts.Register(nil, account, "admin", "", passphrase, "")
|
err := server.accounts.Register(nil, account, "admin", "", passphrase, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = server.accounts.Verify(nil, account, "")
|
err = server.accounts.Verify(nil, account, "")
|
||||||
@ -753,30 +779,40 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
|
|||||||
var errorMessage string
|
var errorMessage string
|
||||||
|
|
||||||
hasPrivs := client.HasRoleCapabs("accreg")
|
hasPrivs := client.HasRoleCapabs("accreg")
|
||||||
if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(params) {
|
switch len(params) {
|
||||||
case 2:
|
case 2:
|
||||||
if !hasPrivs {
|
if !hasPrivs {
|
||||||
errorMessage = "Insufficient privileges"
|
errorMessage = `Insufficient privileges`
|
||||||
} else {
|
} else {
|
||||||
target, newPassword = params[0], params[1]
|
target, newPassword = params[0], params[1]
|
||||||
|
if newPassword == "*" {
|
||||||
|
newPassword = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
target = client.Account()
|
target = client.Account()
|
||||||
if target == "" {
|
if target == "" {
|
||||||
errorMessage = "You're not logged into an account"
|
errorMessage = `You're not logged into an account`
|
||||||
} else if params[1] != params[2] {
|
} else if params[1] != params[2] {
|
||||||
errorMessage = "Passwords do not match"
|
errorMessage = `Passwords do not match`
|
||||||
} else {
|
} else {
|
||||||
// check that they correctly supplied the preexisting password
|
if !nsLoginThrottleCheck(client, rb) {
|
||||||
_, err := server.accounts.checkPassphrase(target, params[0])
|
return
|
||||||
|
}
|
||||||
|
accountData, err := server.accounts.LoadAccount(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorMessage = "Password incorrect"
|
errorMessage = `You're not logged into an account`
|
||||||
|
} else {
|
||||||
|
hash := accountData.Credentials.PassphraseHash
|
||||||
|
if hash != nil && passwd.CompareHashAndPassword(hash, []byte(params[0])) != nil {
|
||||||
|
errorMessage = `Password incorrect`
|
||||||
} else {
|
} else {
|
||||||
newPassword = params[1]
|
newPassword = params[1]
|
||||||
|
if newPassword == "*" {
|
||||||
|
newPassword = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -788,10 +824,15 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := server.accounts.setPassword(target, newPassword)
|
err := server.accounts.setPassword(target, newPassword, hasPrivs)
|
||||||
if err == nil {
|
switch err {
|
||||||
|
case nil:
|
||||||
nsNotice(rb, client.t("Password changed"))
|
nsNotice(rb, client.t("Password changed"))
|
||||||
} else {
|
case errEmptyCredentials:
|
||||||
|
nsNotice(rb, client.t("You can't delete your password unless you add a certificate fingerprint"))
|
||||||
|
case errCASFailed:
|
||||||
|
nsNotice(rb, client.t("Try again later"))
|
||||||
|
default:
|
||||||
server.logger.Error("internal", "could not upgrade user password:", err.Error())
|
server.logger.Error("internal", "could not upgrade user password:", err.Error())
|
||||||
nsNotice(rb, client.t("Password could not be changed due to server error"))
|
nsNotice(rb, client.t("Password could not be changed due to server error"))
|
||||||
}
|
}
|
||||||
@ -837,3 +878,93 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
|
|||||||
nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123)))
|
nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nsCertHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
verb := strings.ToLower(params[0])
|
||||||
|
params = params[1:]
|
||||||
|
var target, certfp string
|
||||||
|
|
||||||
|
switch verb {
|
||||||
|
case "list":
|
||||||
|
if 1 <= len(params) {
|
||||||
|
target = params[0]
|
||||||
|
}
|
||||||
|
case "add", "del":
|
||||||
|
if 2 <= len(params) {
|
||||||
|
target, certfp = params[0], params[1]
|
||||||
|
} else if len(params) == 1 {
|
||||||
|
certfp = params[0]
|
||||||
|
} else {
|
||||||
|
nsNotice(rb, client.t("Invalid parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
nsNotice(rb, client.t("Invalid parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPrivs := client.HasRoleCapabs("accreg")
|
||||||
|
if target != "" && !hasPrivs {
|
||||||
|
nsNotice(rb, client.t("Insufficient privileges"))
|
||||||
|
return
|
||||||
|
} else if target == "" {
|
||||||
|
target = client.Account()
|
||||||
|
if target == "" {
|
||||||
|
nsNotice(rb, client.t("You're not logged into an account"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch verb {
|
||||||
|
case "list":
|
||||||
|
accountData, err := server.accounts.LoadAccount(target)
|
||||||
|
if err == errAccountDoesNotExist {
|
||||||
|
nsNotice(rb, client.t("Account does not exist"))
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
nsNotice(rb, client.t("An error occurred"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
certfps := accountData.Credentials.Certfps
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("There are %d certificate fingerprint(s) authorized for account %s."), len(certfps), accountData.Name))
|
||||||
|
for i, certfp := range certfps {
|
||||||
|
nsNotice(rb, fmt.Sprintf("%d: %s", i+1, certfp))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case "add":
|
||||||
|
err = server.accounts.addRemoveCertfp(target, certfp, true, hasPrivs)
|
||||||
|
case "del":
|
||||||
|
err = server.accounts.addRemoveCertfp(target, certfp, false, hasPrivs)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
if verb == "add" {
|
||||||
|
nsNotice(rb, client.t("Certificate fingerprint successfully added"))
|
||||||
|
} else {
|
||||||
|
nsNotice(rb, client.t("Certificate fingerprint successfully removed"))
|
||||||
|
}
|
||||||
|
case errNoop:
|
||||||
|
if verb == "add" {
|
||||||
|
nsNotice(rb, client.t("That certificate fingerprint was already authorized"))
|
||||||
|
} else {
|
||||||
|
nsNotice(rb, client.t("Certificate fingerprint not found"))
|
||||||
|
}
|
||||||
|
case errAccountDoesNotExist:
|
||||||
|
nsNotice(rb, client.t("Account does not exist"))
|
||||||
|
case errLimitExceeded:
|
||||||
|
nsNotice(rb, client.t("You already have too many certificate fingerprints"))
|
||||||
|
case utils.ErrInvalidCertfp:
|
||||||
|
nsNotice(rb, client.t("Invalid certificate fingerprint"))
|
||||||
|
case errCertfpAlreadyExists:
|
||||||
|
nsNotice(rb, client.t("That certificate fingerprint is already associated with another account"))
|
||||||
|
case errEmptyCredentials:
|
||||||
|
nsNotice(rb, client.t("You can't remove all your certificate fingerprints unless you add a password"))
|
||||||
|
case errCASFailed:
|
||||||
|
nsNotice(rb, client.t("Try again later"))
|
||||||
|
default:
|
||||||
|
server.logger.Error("internal", "could not modify certificates:", err.Error())
|
||||||
|
nsNotice(rb, client.t("An error occurred"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,12 +8,16 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// slingamn's own private b32 alphabet, removing 1, l, o, and 0
|
// slingamn's own private b32 alphabet, removing 1, l, o, and 0
|
||||||
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
||||||
|
|
||||||
|
ErrInvalidCertfp = errors.New("Invalid certfp")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -70,14 +74,12 @@ func GenerateSecretKey() string {
|
|||||||
return base64.RawURLEncoding.EncodeToString(buf[:])
|
return base64.RawURLEncoding.EncodeToString(buf[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeCertfp(certfp string) string {
|
// Normalize openssl-formatted certfp's to oragono's format
|
||||||
return strings.ToLower(strings.Replace(certfp, ":", "", -1))
|
func NormalizeCertfp(certfp string) (result string, err error) {
|
||||||
|
result = strings.ToLower(strings.Replace(certfp, ":", "", -1))
|
||||||
|
decoded, err := hex.DecodeString(result)
|
||||||
|
if err != nil || len(decoded) != 32 {
|
||||||
|
return "", ErrInvalidCertfp
|
||||||
}
|
}
|
||||||
|
return
|
||||||
// Convenience to compare certfps as returned by different tools, e.g., openssl vs. oragono
|
|
||||||
func CertfpsMatch(storedCertfp, suppliedCertfp string) bool {
|
|
||||||
if storedCertfp == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return normalizeCertfp(storedCertfp) == normalizeCertfp(suppliedCertfp)
|
|
||||||
}
|
}
|
||||||
|
@ -85,17 +85,23 @@ func BenchmarkMungeSecretToken(b *testing.B) {
|
|||||||
func TestCertfpComparisons(t *testing.T) {
|
func TestCertfpComparisons(t *testing.T) {
|
||||||
opensslFP := "3D:6B:11:BF:B4:05:C3:F8:4B:38:CD:30:38:FB:EC:01:71:D5:03:54:79:04:07:88:4C:A5:5D:23:41:85:66:C9"
|
opensslFP := "3D:6B:11:BF:B4:05:C3:F8:4B:38:CD:30:38:FB:EC:01:71:D5:03:54:79:04:07:88:4C:A5:5D:23:41:85:66:C9"
|
||||||
oragonoFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c9"
|
oragonoFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c9"
|
||||||
badFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c8"
|
badFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c"
|
||||||
if !CertfpsMatch(opensslFP, oragonoFP) {
|
badFP2 := "*"
|
||||||
t.Error("these certs should match")
|
|
||||||
|
normalizedOpenssl, err := NormalizeCertfp(opensslFP)
|
||||||
|
assertEqual(err, nil, t)
|
||||||
|
assertEqual(normalizedOpenssl, oragonoFP, t)
|
||||||
|
|
||||||
|
normalizedOragono, err := NormalizeCertfp(oragonoFP)
|
||||||
|
assertEqual(err, nil, t)
|
||||||
|
assertEqual(normalizedOragono, oragonoFP, t)
|
||||||
|
|
||||||
|
_, err = NormalizeCertfp(badFP)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("corrupt fp should fail normalization")
|
||||||
}
|
}
|
||||||
if !CertfpsMatch(oragonoFP, opensslFP) {
|
_, err = NormalizeCertfp(badFP2)
|
||||||
t.Error("these certs should match")
|
if err == nil {
|
||||||
}
|
t.Errorf("corrupt fp should fail normalization")
|
||||||
if CertfpsMatch("", "") {
|
|
||||||
t.Error("empty stored certfp should not match empty provided certfp")
|
|
||||||
}
|
|
||||||
if CertfpsMatch(opensslFP, badFP) {
|
|
||||||
t.Error("these certs should not match")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,6 +244,9 @@ server:
|
|||||||
|
|
||||||
# account options
|
# account options
|
||||||
accounts:
|
accounts:
|
||||||
|
# is account authentication enabled, i.e., can users log into existing accounts?
|
||||||
|
authentication-enabled: true
|
||||||
|
|
||||||
# account registration
|
# account registration
|
||||||
registration:
|
registration:
|
||||||
# can users register new accounts for themselves? if this is false, operators with
|
# can users register new accounts for themselves? if this is false, operators with
|
||||||
@ -271,9 +274,6 @@ accounts:
|
|||||||
# password: ""
|
# password: ""
|
||||||
# sender: "admin@my.network"
|
# sender: "admin@my.network"
|
||||||
|
|
||||||
# is account authentication enabled?
|
|
||||||
authentication-enabled: true
|
|
||||||
|
|
||||||
# throttle account login attempts (to prevent either password guessing, or DoS
|
# throttle account login attempts (to prevent either password guessing, or DoS
|
||||||
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
||||||
login-throttling:
|
login-throttling:
|
||||||
|
Loading…
Reference in New Issue
Block a user