Browse Source

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/
devel+passhashingv2
Daniel Oaks 6 years ago
parent
commit
bf04dc24f9
3 changed files with 63 additions and 20 deletions
  1. 47
    8
      irc/accounts.go
  2. 7
    6
      irc/errors.go
  3. 9
    6
      irc/passwd/unsalted.go

+ 47
- 8
irc/accounts.go View File

@@ -130,15 +130,11 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
130 130
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
131 131
 
132 132
 	var creds AccountCredentials
133
-	// always set passphrase salt
134
-	creds.PassphraseSalt, err = passwd.NewSalt()
135
-	if err != nil {
136
-		return errAccountCreation
137
-	}
138 133
 	// it's fine if this is empty, that just means no certificate is authorized
139 134
 	creds.Certificate = certfp
140 135
 	if passphrase != "" {
141
-		creds.PassphraseHash, err = am.server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase)
136
+		creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase)
137
+		creds.PassphraseIsV2 = true
142 138
 		if err != nil {
143 139
 			am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
144 140
 			return errAccountCreation
@@ -459,8 +455,50 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
459 455
 		return errAccountUnverified
460 456
 	}
461 457
 
462
-	err = am.server.passwords.CompareHashAndPassword(
463
-		account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase)
458
+	if account.Credentials.PassphraseIsV2 {
459
+		err = passwd.ComparePassword(account.Credentials.PassphraseHash, []byte(passphrase))
460
+	} else {
461
+		// compare using legacy method
462
+		err = am.server.passwords.CompareHashAndPassword(account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase)
463
+		if err == nil {
464
+			// passphrase worked! silently upgrade them to use v2 hashing going forward.
465
+			//TODO(dan): in future, replace this with an am.updatePassphrase(blah) function, which we can reuse in /ns update pass?
466
+			err = am.server.store.Update(func(tx *buntdb.Tx) error {
467
+				var creds AccountCredentials
468
+				creds.Certificate = account.Credentials.Certificate
469
+				creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase)
470
+				creds.PassphraseIsV2 = true
471
+				if err != nil {
472
+					am.server.logger.Error("internal", fmt.Sprintf("could not hash password (updating existing hash version): %v", err))
473
+					return errAccountCredUpdate
474
+				}
475
+
476
+				credText, err := json.Marshal(creds)
477
+				if err != nil {
478
+					am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials (updating existing hash version): %v", err))
479
+					return errAccountCredUpdate
480
+				}
481
+				credStr := string(credText)
482
+
483
+				// we know the account name is valid if this line is reached, otherwise the
484
+				// above would have failed. as such, chuck out and ignore err on casefolding
485
+				casefoldedAccountName, _ := CasefoldName(accountName)
486
+				credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccountName)
487
+
488
+				//TODO(dan): sling, can you please checkout this mutex usage, see if it
489
+				// makes sense or not? bleh
490
+				am.serialCacheUpdateMutex.Lock()
491
+				defer am.serialCacheUpdateMutex.Unlock()
492
+
493
+				tx.Set(credentialsKey, credStr, nil)
494
+
495
+				return nil
496
+			})
497
+		}
498
+		if err != nil {
499
+			return err
500
+		}
501
+	}
464 502
 	if err != nil {
465 503
 		return errAccountInvalidCredentials
466 504
 	}
@@ -680,6 +718,7 @@ var (
680 718
 type AccountCredentials struct {
681 719
 	PassphraseSalt []byte
682 720
 	PassphraseHash []byte
721
+	PassphraseIsV2 bool   `json:"passphrase-is-v2"`
683 722
 	Certificate    string // fingerprint
684 723
 }
685 724
 

+ 7
- 6
irc/errors.go View File

@@ -10,17 +10,18 @@ import "errors"
10 10
 // Runtime Errors
11 11
 var (
12 12
 	errAccountAlreadyRegistered       = errors.New("Account already exists")
13
+	errAccountAlreadyVerified         = errors.New("Account is already verified")
14
+	errAccountCantDropPrimaryNick     = errors.New("Can't unreserve primary nickname")
13 15
 	errAccountCreation                = errors.New("Account could not be created")
16
+	errAccountCredUpdate              = errors.New("Could not update password hash to new method")
14 17
 	errAccountDoesNotExist            = errors.New("Account does not exist")
18
+	errAccountInvalidCredentials      = errors.New("Invalid account credentials")
19
+	errAccountNickReservationFailed   = errors.New("Could not (un)reserve nick")
15 20
 	errAccountNotLoggedIn             = errors.New("You're not logged into an account")
21
+	errAccountTooManyNicks            = errors.New("Account has too many reserved nicks")
22
+	errAccountUnverified              = errors.New("Account is not yet verified")
16 23
 	errAccountVerificationFailed      = errors.New("Account verification failed")
17 24
 	errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
18
-	errAccountUnverified              = errors.New("Account is not yet verified")
19
-	errAccountAlreadyVerified         = errors.New("Account is already verified")
20
-	errAccountInvalidCredentials      = errors.New("Invalid account credentials")
21
-	errAccountTooManyNicks            = errors.New("Account has too many reserved nicks")
22
-	errAccountNickReservationFailed   = errors.New("Could not (un)reserve nick")
23
-	errAccountCantDropPrimaryNick     = errors.New("Can't unreserve primary nickname")
24 25
 	errCallbackFailed                 = errors.New("Account verification could not be sent")
25 26
 	errCertfpAlreadyExists            = errors.New("An account already exists with your certificate")
26 27
 	errChannelAlreadyRegistered       = errors.New("Channel is already registered")

+ 9
- 6
irc/passwd/unsalted.go View File

@@ -15,16 +15,19 @@ var (
15 15
 	ErrEmptyPassword = errors.New("empty password")
16 16
 )
17 17
 
18
-// GenerateEncodedPassword returns an encrypted password, encoded into a string with base64.
19
-func GenerateEncodedPassword(passwd string) (encoded string, err error) {
18
+// GenerateEncodedPasswordBytes returns an encrypted password, returning the bytes directly.
19
+func GenerateEncodedPasswordBytes(passwd string) (encoded []byte, err error) {
20 20
 	if passwd == "" {
21 21
 		err = ErrEmptyPassword
22 22
 		return
23 23
 	}
24
-	bcrypted, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost)
25
-	if err != nil {
26
-		return
27
-	}
24
+	encoded, err = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost)
25
+	return
26
+}
27
+
28
+// GenerateEncodedPassword returns an encrypted password, encoded into a string with base64.
29
+func GenerateEncodedPassword(passwd string) (encoded string, err error) {
30
+	bcrypted, err := GenerateEncodedPasswordBytes(passwd)
28 31
 	encoded = base64.StdEncoding.EncodeToString(bcrypted)
29 32
 	return
30 33
 }

Loading…
Cancel
Save