mirror of
https://github.com/jeremyd/ergo.git
synced 2026-06-19 10:59:44 -07:00
refactor the password hashing / password autoupgrade system
This commit is contained in:
@@ -22,5 +22,6 @@ test:
|
||||
cd irc/caps && go test . && go vet .
|
||||
cd irc/isupport && go test . && go vet .
|
||||
cd irc/modes && go test . && go vet .
|
||||
cd irc/passwd && go test . && go vet .
|
||||
cd irc/utils && go test . && go vet .
|
||||
./.check-gofmt.sh
|
||||
|
||||
+85
-63
@@ -16,6 +16,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
@@ -175,7 +176,8 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
}
|
||||
|
||||
// can't register a guest nickname
|
||||
renamePrefix := strings.ToLower(am.server.AccountConfig().NickReservation.RenamePrefix)
|
||||
config := am.server.AccountConfig()
|
||||
renamePrefix := strings.ToLower(config.NickReservation.RenamePrefix)
|
||||
if renamePrefix != "" && strings.HasPrefix(casefoldedAccount, renamePrefix) {
|
||||
return errAccountAlreadyRegistered
|
||||
}
|
||||
@@ -188,30 +190,16 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
var creds AccountCredentials
|
||||
// it's fine if this is empty, that just means no certificate is authorized
|
||||
creds.Certificate = certfp
|
||||
if 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
|
||||
}
|
||||
}
|
||||
|
||||
credText, err := json.Marshal(creds)
|
||||
credStr, err := am.serializeCredentials(passphrase, certfp)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
|
||||
return errAccountCreation
|
||||
return err
|
||||
}
|
||||
credStr := string(credText)
|
||||
|
||||
registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
|
||||
|
||||
var setOptions *buntdb.SetOptions
|
||||
ttl := am.server.AccountConfig().Registration.VerifyTimeout
|
||||
ttl := config.Registration.VerifyTimeout
|
||||
if ttl != 0 {
|
||||
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
|
||||
}
|
||||
@@ -267,6 +255,75 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
}
|
||||
}
|
||||
|
||||
// validatePassphrase checks whether a passphrase is allowed by our rules
|
||||
func validatePassphrase(passphrase string) error {
|
||||
// sanity check the length
|
||||
if len(passphrase) == 0 || len(passphrase) > 600 {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
// for now, just enforce that spaces are not allowed
|
||||
for _, r := range passphrase {
|
||||
if unicode.IsSpace(r) {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
}
|
||||
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", fmt.Sprintf("could not hash password: %v", err))
|
||||
return "", errAccountCreation
|
||||
}
|
||||
}
|
||||
|
||||
credText, err := json.Marshal(creds)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
|
||||
return "", errAccountCreation
|
||||
}
|
||||
return string(credText), nil
|
||||
}
|
||||
|
||||
// changes the password for an account
|
||||
func (am *AccountManager) setPassword(account string, password string) (err error) {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
act, err := am.LoadAccount(casefoldedAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credStr, err := am.serializeCredentials(password, act.Credentials.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, _, err := tx.Set(credentialsKey, credStr, nil)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
|
||||
if callbackNamespace == "*" || callbackNamespace == "none" {
|
||||
return "", nil
|
||||
@@ -518,50 +575,15 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
||||
return errAccountUnverified
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
switch account.Credentials.Version {
|
||||
case 0:
|
||||
err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
|
||||
case 1:
|
||||
err = passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase))
|
||||
default:
|
||||
err = errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
@@ -1020,9 +1042,9 @@ var (
|
||||
|
||||
// AccountCredentials stores the various methods for verifying accounts.
|
||||
type AccountCredentials struct {
|
||||
PassphraseSalt []byte
|
||||
Version uint
|
||||
PassphraseSalt []byte // legacy field, not used by v1 and later
|
||||
PassphraseHash []byte
|
||||
PassphraseIsV2 bool `json:"passphrase-is-v2"`
|
||||
Certificate string // fingerprint
|
||||
}
|
||||
|
||||
|
||||
+11
-12
@@ -82,6 +82,7 @@ type AccountRegistrationConfig struct {
|
||||
}
|
||||
}
|
||||
AllowMultiplePerConnection bool `yaml:"allow-multiple-per-connection"`
|
||||
BcryptCost uint `yaml:"bcrypt-cost"`
|
||||
}
|
||||
|
||||
type VHostConfig struct {
|
||||
@@ -152,15 +153,6 @@ type OperConfig struct {
|
||||
Modes string
|
||||
}
|
||||
|
||||
// PasswordBytes returns the bytes represented by the password hash.
|
||||
func (conf *OperConfig) PasswordBytes() []byte {
|
||||
bytes, err := passwd.DecodePasswordHash(conf.Password)
|
||||
if err != nil {
|
||||
log.Fatal("decode password error: ", err)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
// LineLenConfig controls line lengths.
|
||||
type LineLenLimits struct {
|
||||
Tags int
|
||||
@@ -384,7 +376,11 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
|
||||
}
|
||||
oper.Name = name
|
||||
|
||||
oper.Pass = opConf.PasswordBytes()
|
||||
oper.Pass, err = decodeLegacyPasswordHash(opConf.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oper.Vhost = opConf.Vhost
|
||||
class, exists := oc[opConf.Class]
|
||||
if !exists {
|
||||
@@ -713,11 +709,14 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
config.Channels.defaultModes = ParseDefaultChannelModes(config.Channels.RawDefaultModes)
|
||||
|
||||
if config.Server.Password != "" {
|
||||
bytes, err := passwd.DecodePasswordHash(config.Server.Password)
|
||||
config.Server.passwordBytes, err = decodeLegacyPasswordHash(config.Server.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Server.passwordBytes = bytes
|
||||
}
|
||||
|
||||
if config.Accounts.Registration.BcryptCost == 0 {
|
||||
config.Accounts.Registration.BcryptCost = passwd.DefaultCost
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
@@ -25,8 +23,6 @@ const (
|
||||
keySchemaVersion = "db.version"
|
||||
// latest schema of the db
|
||||
latestDbSchema = "3"
|
||||
// key for the primary salt used by the ircd
|
||||
keySalt = "crypto.salt"
|
||||
)
|
||||
|
||||
type SchemaChanger func(*Config, *buntdb.Tx) error
|
||||
@@ -68,14 +64,6 @@ func InitDB(path string) {
|
||||
defer store.Close()
|
||||
|
||||
err = store.Update(func(tx *buntdb.Tx) error {
|
||||
// set base db salt
|
||||
salt, err := passwd.NewSalt()
|
||||
encodedSalt := base64.StdEncoding.EncodeToString(salt)
|
||||
if err != nil {
|
||||
log.Fatal("Could not generate cryptographically-secure salt for the user:", err.Error())
|
||||
}
|
||||
tx.Set(keySalt, encodedSalt, nil)
|
||||
|
||||
// set schema version
|
||||
tx.Set(keySchemaVersion, latestDbSchema, nil)
|
||||
return nil
|
||||
|
||||
@@ -16,6 +16,7 @@ var (
|
||||
errAccountCredUpdate = errors.New("Could not update password hash to new method")
|
||||
errAccountDoesNotExist = errors.New("Account does not exist")
|
||||
errAccountInvalidCredentials = errors.New("Invalid account credentials")
|
||||
errAccountBadPassphrase = errors.New("Passphrase contains forbidden characters or is otherwise invalid")
|
||||
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")
|
||||
|
||||
+1
-2
@@ -10,7 +10,6 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
@@ -29,7 +28,7 @@ func (wc *webircConfig) Populate() (err error) {
|
||||
|
||||
if wc.PasswordString != "" {
|
||||
var password []byte
|
||||
password, err = passwd.DecodePasswordHash(wc.PasswordString)
|
||||
wc.Password, err = decodeLegacyPasswordHash(wc.PasswordString)
|
||||
wc.Password = password
|
||||
}
|
||||
return err
|
||||
|
||||
+4
-3
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/tidwall/buntdb"
|
||||
@@ -159,6 +158,8 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
||||
} else if err == errAccountAlreadyRegistered {
|
||||
msg = "Account already exists"
|
||||
code = ERR_ACCOUNT_ALREADY_EXISTS
|
||||
} else if err == errAccountBadPassphrase {
|
||||
msg = "Passphrase contains forbidden characters or is otherwise invalid"
|
||||
}
|
||||
if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
|
||||
msg = err.Error()
|
||||
@@ -1822,7 +1823,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
|
||||
// check the provided password
|
||||
password := []byte(msg.Params[0])
|
||||
if passwd.ComparePassword(serverPassword, password) != nil {
|
||||
if bcrypt.CompareHashAndPassword(serverPassword, password) != nil {
|
||||
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
|
||||
rb.Add(nil, server.name, "ERROR", client.t("Password incorrect"))
|
||||
return true
|
||||
@@ -2406,7 +2407,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
| ||||