diff --git a/irc/accounts.go b/irc/accounts.go index b663d525..9b4905ea 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -189,15 +189,11 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames certFPKey := fmt.Sprintf(keyCertToAccount, certfp) var creds AccountCredentials - // always set passphrase salt - creds.PassphraseSalt, err = passwd.NewSalt() - if err != nil { - return errAccountCreation - } // it's fine if this is empty, that just means no certificate is authorized creds.Certificate = certfp if passphrase != "" { - creds.PassphraseHash, err = am.server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase) + creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase) + creds.PassphraseIsV2 = true if err != nil { am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err)) return errAccountCreation @@ -522,8 +518,50 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s return errAccountUnverified } - err = am.server.passwords.CompareHashAndPassword( - account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase) + if account.Credentials.PassphraseIsV2 { + err = passwd.ComparePassword(account.Credentials.PassphraseHash, []byte(passphrase)) + } else { + // compare using legacy method + err = am.server.passwords.CompareHashAndPassword(account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase) + if err == nil { + // passphrase worked! silently upgrade them to use v2 hashing going forward. + //TODO(dan): in future, replace this with an am.updatePassphrase(blah) function, which we can reuse in /ns update pass? + err = am.server.store.Update(func(tx *buntdb.Tx) error { + var creds AccountCredentials + creds.Certificate = account.Credentials.Certificate + creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase) + creds.PassphraseIsV2 = true + if err != nil { + am.server.logger.Error("internal", fmt.Sprintf("could not hash password (updating existing hash version): %v", err)) + return errAccountCredUpdate + } + + credText, err := json.Marshal(creds) + if err != nil { + am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials (updating existing hash version): %v", err)) + return errAccountCredUpdate + } + credStr := string(credText) + + // we know the account name is valid if this line is reached, otherwise the + // above would have failed. as such, chuck out and ignore err on casefolding + casefoldedAccountName, _ := CasefoldName(accountName) + credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccountName) + + //TODO(dan): sling, can you please checkout this mutex usage, see if it + // makes sense or not? bleh + am.serialCacheUpdateMutex.Lock() + defer am.serialCacheUpdateMutex.Unlock() + + tx.Set(credentialsKey, credStr, nil) + + return nil + }) + } + if err != nil { + return err + } + } if err != nil { return errAccountInvalidCredentials } @@ -984,6 +1022,7 @@ var ( type AccountCredentials struct { PassphraseSalt []byte PassphraseHash []byte + PassphraseIsV2 bool `json:"passphrase-is-v2"` Certificate string // fingerprint } diff --git a/irc/errors.go b/irc/errors.go index 761f6a00..94a9ad69 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -10,17 +10,18 @@ import "errors" // Runtime Errors var ( errAccountAlreadyRegistered = errors.New("Account already exists") + errAccountAlreadyVerified = errors.New("Account is already verified") + errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname") errAccountCreation = errors.New("Account could not be created") + errAccountCredUpdate = errors.New("Could not update password hash to new method") errAccountDoesNotExist = errors.New("Account does not exist") + errAccountInvalidCredentials = errors.New("Invalid account credentials") + errAccountNickReservationFailed = errors.New("Could not (un)reserve nick") errAccountNotLoggedIn = errors.New("You're not logged into an account") + errAccountTooManyNicks = errors.New("Account has too many reserved nicks") + errAccountUnverified = errors.New("Account is not yet verified") errAccountVerificationFailed = errors.New("Account verification failed") errAccountVerificationInvalidCode = errors.New("Invalid account verification code") - errAccountUnverified = errors.New("Account is not yet verified") - errAccountAlreadyVerified = errors.New("Account is already verified") - errAccountInvalidCredentials = errors.New("Invalid account credentials") - errAccountTooManyNicks = errors.New("Account has too many reserved nicks") - errAccountNickReservationFailed = errors.New("Could not (un)reserve nick") - errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname") errAccountUpdateFailed = errors.New("Error while updating your account information") errCallbackFailed = errors.New("Account verification could not be sent") errCertfpAlreadyExists = errors.New("An account already exists with your certificate") diff --git a/irc/passwd/unsalted.go b/irc/passwd/unsalted.go index 0c8478e1..2c23fd26 100644 --- a/irc/passwd/unsalted.go +++ b/irc/passwd/unsalted.go @@ -15,16 +15,19 @@ var ( ErrEmptyPassword = errors.New("empty password") ) -// GenerateEncodedPassword returns an encrypted password, encoded into a string with base64. -func GenerateEncodedPassword(passwd string) (encoded string, err error) { +// GenerateEncodedPasswordBytes returns an encrypted password, returning the bytes directly. +func GenerateEncodedPasswordBytes(passwd string) (encoded []byte, err error) { if passwd == "" { err = ErrEmptyPassword return } - bcrypted, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost) - if err != nil { - return - } + encoded, err = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost) + return +} + +// GenerateEncodedPassword returns an encrypted password, encoded into a string with base64. +func GenerateEncodedPassword(passwd string) (encoded string, err error) { + bcrypted, err := GenerateEncodedPasswordBytes(passwd) encoded = base64.StdEncoding.EncodeToString(bcrypted) return }