From 626086906879c51d473b5ed9ab4e8b5c007c5dcb Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Sun, 1 Apr 2018 17:12:41 +1000 Subject: [PATCH] Upgrade password hashing. Previously, we generated and prepended a long salt before generating password hashes. This resulted in the hash verification cutting off long before it should do. This form of salting is also not necessary with bcrypt as it's provided by the password hashing and verification functions themselves, so totally rip it out. This commit also adds the functionality for the server to automagically upgrade users to use the new hashing system, which means better security and more assurance that people can't bruteforce passwords. No need to apply a database upgrade to do this, whoo! \o/ --- irc/accounts.go | 55 ++++++++++++++++++++++++++++++++++++------ irc/errors.go | 13 +++++----- irc/passwd/unsalted.go | 15 +++++++----- 3 files changed, 63 insertions(+), 20 deletions(-) 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 }