mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-21 19:39:43 +01:00
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/
This commit is contained in:
parent
dcb15d619d
commit
bf04dc24f9
@ -130,15 +130,11 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
|||||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||||
|
|
||||||
var creds AccountCredentials
|
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
|
// it's fine if this is empty, that just means no certificate is authorized
|
||||||
creds.Certificate = certfp
|
creds.Certificate = certfp
|
||||||
if passphrase != "" {
|
if passphrase != "" {
|
||||||
creds.PassphraseHash, err = am.server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase)
|
creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase)
|
||||||
|
creds.PassphraseIsV2 = true
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
|
am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
|
||||||
return errAccountCreation
|
return errAccountCreation
|
||||||
@ -459,8 +455,50 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
|||||||
return errAccountUnverified
|
return errAccountUnverified
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.server.passwords.CompareHashAndPassword(
|
if account.Credentials.PassphraseIsV2 {
|
||||||
account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase)
|
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 {
|
if err != nil {
|
||||||
return errAccountInvalidCredentials
|
return errAccountInvalidCredentials
|
||||||
}
|
}
|
||||||
@ -680,6 +718,7 @@ var (
|
|||||||
type AccountCredentials struct {
|
type AccountCredentials struct {
|
||||||
PassphraseSalt []byte
|
PassphraseSalt []byte
|
||||||
PassphraseHash []byte
|
PassphraseHash []byte
|
||||||
|
PassphraseIsV2 bool `json:"passphrase-is-v2"`
|
||||||
Certificate string // fingerprint
|
Certificate string // fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,17 +10,18 @@ import "errors"
|
|||||||
// Runtime Errors
|
// Runtime Errors
|
||||||
var (
|
var (
|
||||||
errAccountAlreadyRegistered = errors.New("Account already exists")
|
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")
|
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")
|
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")
|
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")
|
errAccountVerificationFailed = errors.New("Account verification failed")
|
||||||
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
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")
|
|
||||||
errCallbackFailed = errors.New("Account verification could not be sent")
|
errCallbackFailed = errors.New("Account verification could not be sent")
|
||||||
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
|
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
|
||||||
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
||||||
|
@ -15,16 +15,19 @@ var (
|
|||||||
ErrEmptyPassword = errors.New("empty password")
|
ErrEmptyPassword = errors.New("empty password")
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateEncodedPassword returns an encrypted password, encoded into a string with base64.
|
// GenerateEncodedPasswordBytes returns an encrypted password, returning the bytes directly.
|
||||||
func GenerateEncodedPassword(passwd string) (encoded string, err error) {
|
func GenerateEncodedPasswordBytes(passwd string) (encoded []byte, err error) {
|
||||||
if passwd == "" {
|
if passwd == "" {
|
||||||
err = ErrEmptyPassword
|
err = ErrEmptyPassword
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bcrypted, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost)
|
encoded, err = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost)
|
||||||
if err != nil {
|
return
|
||||||
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)
|
encoded = base64.StdEncoding.EncodeToString(bcrypted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user