Bläddra i källkod

refactor the password hashing / password autoupgrade system

tags/v0.12.0
Shivaram Lingamneni 5 år sedan
förälder
incheckning
dfb0a57040
18 ändrade filer med 277 tillägg och 380 borttagningar
  1. 1
    0
      Makefile
  2. 85
    63
      irc/accounts.go
  3. 11
    12
      irc/config.go
  4. 0
    12
      irc/database.go
  5. 1
    0
      irc/errors.go
  6. 1
    2
      irc/gateways.go
  7. 4
    3
      irc/handlers.go
  8. 72
    0
      irc/legacy.go
  9. 2
    0
      irc/nickserv.go
  10. 34
    0
      irc/passwd/bcrypt.go
  11. 58
    0
      irc/passwd/bcrypt_test.go
  12. 0
    66
      irc/passwd/salted.go
  13. 0
    86
      irc/passwd/salted_test.go
  14. 0
    53
      irc/passwd/unsalted.go
  15. 0
    54
      irc/passwd/unsalted_test.go
  16. 0
    24
      irc/server.go
  17. 3
    3
      oragono.go
  18. 5
    2
      oragono.yaml

+ 1
- 0
Makefile Visa fil

@@ -22,5 +22,6 @@ test:
22 22
 	cd irc/caps && go test . && go vet .
23 23
 	cd irc/isupport && go test . && go vet .
24 24
 	cd irc/modes && go test . && go vet .
25
+	cd irc/passwd && go test . && go vet .
25 26
 	cd irc/utils && go test . && go vet .
26 27
 	./.check-gofmt.sh

+ 85
- 63
irc/accounts.go Visa fil

@@ -16,6 +16,7 @@ import (
16 16
 	"sync"
17 17
 	"sync/atomic"
18 18
 	"time"
19
+	"unicode"
19 20
 
20 21
 	"github.com/oragono/oragono/irc/caps"
21 22
 	"github.com/oragono/oragono/irc/passwd"
@@ -175,7 +176,8 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
175 176
 	}
176 177
 
177 178
 	// can't register a guest nickname
178
-	renamePrefix := strings.ToLower(am.server.AccountConfig().NickReservation.RenamePrefix)
179
+	config := am.server.AccountConfig()
180
+	renamePrefix := strings.ToLower(config.NickReservation.RenamePrefix)
179 181
 	if renamePrefix != "" && strings.HasPrefix(casefoldedAccount, renamePrefix) {
180 182
 		return errAccountAlreadyRegistered
181 183
 	}
@@ -188,30 +190,16 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
188 190
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
189 191
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
190 192
 
191
-	var creds AccountCredentials
192
-	// it's fine if this is empty, that just means no certificate is authorized
193
-	creds.Certificate = certfp
194
-	if passphrase != "" {
195
-		creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase)
196
-		creds.PassphraseIsV2 = true
197
-		if err != nil {
198
-			am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
199
-			return errAccountCreation
200
-		}
201
-	}
202
-
203
-	credText, err := json.Marshal(creds)
193
+	credStr, err := am.serializeCredentials(passphrase, certfp)
204 194
 	if err != nil {
205
-		am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
206
-		return errAccountCreation
195
+		return err
207 196
 	}
208
-	credStr := string(credText)
209 197
 
210 198
 	registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
211 199
 	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
212 200
 
213 201
 	var setOptions *buntdb.SetOptions
214
-	ttl := am.server.AccountConfig().Registration.VerifyTimeout
202
+	ttl := config.Registration.VerifyTimeout
215 203
 	if ttl != 0 {
216 204
 		setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
217 205
 	}
@@ -267,6 +255,75 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
267 255
 	}
268 256
 }
269 257
 
258
+// validatePassphrase checks whether a passphrase is allowed by our rules
259
+func validatePassphrase(passphrase string) error {
260
+	// sanity check the length
261
+	if len(passphrase) == 0 || len(passphrase) > 600 {
262
+		return errAccountBadPassphrase
263
+	}
264
+	// for now, just enforce that spaces are not allowed
265
+	for _, r := range passphrase {
266
+		if unicode.IsSpace(r) {
267
+			return errAccountBadPassphrase
268
+		}
269
+	}
270
+	return nil
271
+}
272
+
273
+// helper to assemble the serialized JSON for an account's credentials
274
+func (am *AccountManager) serializeCredentials(passphrase string, certfp string) (result string, err error) {
275
+	var creds AccountCredentials
276
+	creds.Version = 1
277
+	// we need at least one of passphrase and certfp:
278
+	if passphrase == "" && certfp == "" {
279
+		return "", errAccountBadPassphrase
280
+	}
281
+	// but if we have one, it's fine if the other is missing, it just means no
282
+	// credential of that type will be accepted.
283
+	creds.Certificate = certfp
284
+	if passphrase != "" {
285
+		if validatePassphrase(passphrase) != nil {
286
+			return "", errAccountBadPassphrase
287
+		}
288
+		bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
289
+		creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
290
+		if err != nil {
291
+			am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
292
+			return "", errAccountCreation
293
+		}
294
+	}
295
+
296
+	credText, err := json.Marshal(creds)
297
+	if err != nil {
298
+		am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
299
+		return "", errAccountCreation
300
+	}
301
+	return string(credText), nil
302
+}
303
+
304
+// changes the password for an account
305
+func (am *AccountManager) setPassword(account string, password string) (err error) {
306
+	casefoldedAccount, err := CasefoldName(account)
307
+	if err != nil {
308
+		return err
309
+	}
310
+	act, err := am.LoadAccount(casefoldedAccount)
311
+	if err != nil {
312
+		return err
313
+	}
314
+
315
+	credStr, err := am.serializeCredentials(password, act.Credentials.Certificate)
316
+	if err != nil {
317
+		return err
318
+	}
319
+
320
+	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
321
+	return am.server.store.Update(func(tx *buntdb.Tx) error {
322
+		_, _, err := tx.Set(credentialsKey, credStr, nil)
323
+		return err
324
+	})
325
+}
326
+
270 327
 func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
271 328
 	if callbackNamespace == "*" || callbackNamespace == "none" {
272 329
 		return "", nil
@@ -518,50 +575,15 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
518 575
 		return errAccountUnverified
519 576
 	}
520 577
 
521
-	if account.Credentials.PassphraseIsV2 {
522
-		err = passwd.ComparePassword(account.Credentials.PassphraseHash, []byte(passphrase))
523
-	} else {
524
-		// compare using legacy method
525
-		err = am.server.passwords.CompareHashAndPassword(account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase)
526
-		if err == nil {
527
-			// passphrase worked! silently upgrade them to use v2 hashing going forward.
528
-			//TODO(dan): in future, replace this with an am.updatePassphrase(blah) function, which we can reuse in /ns update pass?
529
-			err = am.server.store.Update(func(tx *buntdb.Tx) error {
530
-				var creds AccountCredentials
531
-				creds.Certificate = account.Credentials.Certificate
532
-				creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase)
533
-				creds.PassphraseIsV2 = true
534
-				if err != nil {
535
-					am.server.logger.Error("internal", fmt.Sprintf("could not hash password (updating existing hash version): %v", err))
536
-					return errAccountCredUpdate
537
-				}
538
-
539
-				credText, err := json.Marshal(creds)
540
-				if err != nil {
541
-					am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials (updating existing hash version): %v", err))
542
-					return errAccountCredUpdate
543
-				}
544
-				credStr := string(credText)
545
-
546
-				// we know the account name is valid if this line is reached, otherwise the
547
-				// above would have failed. as such, chuck out and ignore err on casefolding
548
-				casefoldedAccountName, _ := CasefoldName(accountName)
549
-				credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccountName)
550
-
551
-				//TODO(dan): sling, can you please checkout this mutex usage, see if it
552
-				// makes sense or not? bleh
553
-				am.serialCacheUpdateMutex.Lock()
554
-				defer am.serialCacheUpdateMutex.Unlock()
555
-
556
-				tx.Set(credentialsKey, credStr, nil)
557
-
558
-				return nil
559
-			})
560
-		}
561
-		if err != nil {
562
-			return err
563
-		}
578
+	switch account.Credentials.Version {
579
+	case 0:
580
+		err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
581
+	case 1:
582
+		err = passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase))
583
+	default:
584
+		err = errAccountInvalidCredentials
564 585
 	}
586
+
565 587
 	if err != nil {
566 588
 		return errAccountInvalidCredentials
567 589
 	}
@@ -1020,9 +1042,9 @@ var (
1020 1042
 
1021 1043
 // AccountCredentials stores the various methods for verifying accounts.
1022 1044
 type AccountCredentials struct {
1023
-	PassphraseSalt []byte
1045
+	Version        uint
1046
+	PassphraseSalt []byte // legacy field, not used by v1 and later
1024 1047
 	PassphraseHash []byte
1025
-	PassphraseIsV2 bool   `json:"passphrase-is-v2"`
1026 1048
 	Certificate    string // fingerprint
1027 1049
 }
1028 1050
 

+ 11
- 12
irc/config.go Visa fil

@@ -82,6 +82,7 @@ type AccountRegistrationConfig struct {
82 82
 		}
83 83
 	}
84 84
 	AllowMultiplePerConnection bool `yaml:"allow-multiple-per-connection"`
85
+	BcryptCost                 uint `yaml:"bcrypt-cost"`
85 86
 }
86 87
 
87 88
 type VHostConfig struct {
@@ -152,15 +153,6 @@ type OperConfig struct {
152 153
 	Modes     string
153 154
 }
154 155
 
155
-// PasswordBytes returns the bytes represented by the password hash.
156
-func (conf *OperConfig) PasswordBytes() []byte {
157
-	bytes, err := passwd.DecodePasswordHash(conf.Password)
158
-	if err != nil {
159
-		log.Fatal("decode password error: ", err)
160
-	}
161
-	return bytes
162
-}
163
-
164 156
 // LineLenConfig controls line lengths.
165 157
 type LineLenLimits struct {
166 158
 	Tags int
@@ -384,7 +376,11 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
384 376
 		}
385 377
 		oper.Name = name
386 378
 
387
-		oper.Pass = opConf.PasswordBytes()
379
+		oper.Pass, err = decodeLegacyPasswordHash(opConf.Password)
380
+		if err != nil {
381
+			return nil, err
382
+		}
383
+
388 384
 		oper.Vhost = opConf.Vhost
389 385
 		class, exists := oc[opConf.Class]
390 386
 		if !exists {
@@ -713,11 +709,14 @@ func LoadConfig(filename string) (config *Config, err error) {
713 709
 	config.Channels.defaultModes = ParseDefaultChannelModes(config.Channels.RawDefaultModes)
714 710
 
715 711
 	if config.Server.Password != "" {
716
-		bytes, err := passwd.DecodePasswordHash(config.Server.Password)
712
+		config.Server.passwordBytes, err = decodeLegacyPasswordHash(config.Server.Password)
717 713
 		if err != nil {
718 714
 			return nil, err
719 715
 		}
720
-		config.Server.passwordBytes = bytes
716
+	}
717
+
718
+	if config.Accounts.Registration.BcryptCost == 0 {
719
+		config.Accounts.Registration.BcryptCost = passwd.DefaultCost
721 720
 	}
722 721
 
723 722
 	return config, nil

+ 0
- 12
irc/database.go Visa fil

@@ -5,7 +5,6 @@
5 5
 package irc
6 6
 
7 7
 import (
8
-	"encoding/base64"
9 8
 	"encoding/json"
10 9
 	"fmt"
11 10
 	"log"
@@ -14,7 +13,6 @@ import (
14 13
 	"time"
15 14
 
16 15
 	"github.com/oragono/oragono/irc/modes"
17
-	"github.com/oragono/oragono/irc/passwd"
18 16
 	"github.com/oragono/oragono/irc/utils"
19 17
 
20 18
 	"github.com/tidwall/buntdb"
@@ -25,8 +23,6 @@ const (
25 23
 	keySchemaVersion = "db.version"
26 24
 	// latest schema of the db
27 25
 	latestDbSchema = "3"
28
-	// key for the primary salt used by the ircd
29
-	keySalt = "crypto.salt"
30 26
 )
31 27
 
32 28
 type SchemaChanger func(*Config, *buntdb.Tx) error
@@ -68,14 +64,6 @@ func InitDB(path string) {
68 64
 	defer store.Close()
69 65
 
70 66
 	err = store.Update(func(tx *buntdb.Tx) error {
71
-		// set base db salt
72
-		salt, err := passwd.NewSalt()
73
-		encodedSalt := base64.StdEncoding.EncodeToString(salt)
74
-		if err != nil {
75
-			log.Fatal("Could not generate cryptographically-secure salt for the user:", err.Error())
76
-		}
77
-		tx.Set(keySalt, encodedSalt, nil)
78
-
79 67
 		// set schema version
80 68
 		tx.Set(keySchemaVersion, latestDbSchema, nil)
81 69
 		return nil

+ 1
- 0
irc/errors.go Visa fil

@@ -16,6 +16,7 @@ var (
16 16
 	errAccountCredUpdate              = errors.New("Could not update password hash to new method")
17 17
 	errAccountDoesNotExist            = errors.New("Account does not exist")
18 18
 	errAccountInvalidCredentials      = errors.New("Invalid account credentials")
19
+	errAccountBadPassphrase           = errors.New("Passphrase contains forbidden characters or is otherwise invalid")
19 20
 	errAccountNickReservationFailed   = errors.New("Could not (un)reserve nick")
20 21
 	errAccountNotLoggedIn             = errors.New("You're not logged into an account")
21 22
 	errAccountTooManyNicks            = errors.New("Account has too many reserved nicks")

+ 1
- 2
irc/gateways.go Visa fil

@@ -10,7 +10,6 @@ import (
10 10
 	"net"
11 11
 
12 12
 	"github.com/oragono/oragono/irc/modes"
13
-	"github.com/oragono/oragono/irc/passwd"
14 13
 	"github.com/oragono/oragono/irc/utils"
15 14
 )
16 15
 
@@ -29,7 +28,7 @@ func (wc *webircConfig) Populate() (err error) {
29 28
 
30 29
 	if wc.PasswordString != "" {
31 30
 		var password []byte
32
-		password, err = passwd.DecodePasswordHash(wc.PasswordString)
31
+		wc.Password, err = decodeLegacyPasswordHash(wc.PasswordString)
33 32
 		wc.Password = password
34 33
 	}
35 34
 	return err

+ 4
- 3
irc/handlers.go Visa fil

@@ -27,7 +27,6 @@ import (
27 27
 	"github.com/oragono/oragono/irc/caps"
28 28
 	"github.com/oragono/oragono/irc/custime"
29 29
 	"github.com/oragono/oragono/irc/modes"
30
-	"github.com/oragono/oragono/irc/passwd"
31 30
 	"github.com/oragono/oragono/irc/sno"
32 31
 	"github.com/oragono/oragono/irc/utils"
33 32
 	"github.com/tidwall/buntdb"
@@ -159,6 +158,8 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
159 158
 		} else if err == errAccountAlreadyRegistered {
160 159
 			msg = "Account already exists"
161 160
 			code = ERR_ACCOUNT_ALREADY_EXISTS
161
+		} else if err == errAccountBadPassphrase {
162
+			msg = "Passphrase contains forbidden characters or is otherwise invalid"
162 163
 		}
163 164
 		if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
164 165
 			msg = err.Error()
@@ -1822,7 +1823,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1822 1823
 
1823 1824
 	// check the provided password
1824 1825
 	password := []byte(msg.Params[0])
1825
-	if passwd.ComparePassword(serverPassword, password) != nil {
1826
+	if bcrypt.CompareHashAndPassword(serverPassword, password) != nil {
1826 1827
 		rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
1827 1828
 		rb.Add(nil, server.name, "ERROR", client.t("Password incorrect"))
1828 1829
 		return true
@@ -2406,7 +2407,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2406 2407
 			if isGatewayAllowed(client.socket.conn.RemoteAddr(), gateway) {
2407 2408
 				// confirm password and/or fingerprint
2408 2409
 				givenPassword := msg.Params[0]
2409
-				if 0 < len(info.Password) && passwd.ComparePasswordString(info.Password, givenPassword) != nil {
2410
+				if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, []byte(givenPassword)) != nil {
2410 2411
 					continue
2411 2412
 				}
2412 2413
 				if 0 < len(info.Fingerprint) && client.certfp != info.Fingerprint {

+ 72
- 0
irc/legacy.go Visa fil

@@ -0,0 +1,72 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni
2
+
3
+package irc
4
+
5
+import (
6
+	"encoding/base64"
7
+	"errors"
8
+	"fmt"
9
+
10
+	"github.com/tidwall/buntdb"
11
+	"golang.org/x/crypto/bcrypt"
12
+)
13
+
14
+var (
15
+	errInvalidPasswordHash = errors.New("invalid password hash")
16
+)
17
+
18
+// Decode a hashed passphrase as it would appear in a config file,
19
+// retaining compatibility with old versions of `oragono genpasswd`
20
+// that used to apply a redundant layer of base64
21
+func decodeLegacyPasswordHash(hash string) ([]byte, error) {
22
+	// a correctly formatted bcrypt hash is 60 bytes of printable ASCII
23
+	if len(hash) == 80 {
24
+		// double-base64, remove the outer layer:
25
+		return base64.StdEncoding.DecodeString(hash)
26
+	} else if len(hash) == 60 {
27
+		return []byte(hash), nil
28
+	} else {
29
+		return nil, errInvalidPasswordHash
30
+	}
31
+}
32
+
33
+// helper to check a version 0 password hash, with global and per-passphrase salts
34
+func checkLegacyPasswordV0(hashedPassword, globalSalt, passphraseSalt []byte, passphrase string) error {
35
+	var assembledPasswordBytes []byte
36
+	assembledPasswordBytes = append(assembledPasswordBytes, globalSalt...)
37
+	assembledPasswordBytes = append(assembledPasswordBytes, '-')
38
+	assembledPasswordBytes = append(assembledPasswordBytes, passphraseSalt...)
39
+	assembledPasswordBytes = append(assembledPasswordBytes, '-')
40
+	assembledPasswordBytes = append(assembledPasswordBytes, []byte(passphrase)...)
41
+	return bcrypt.CompareHashAndPassword(hashedPassword, assembledPasswordBytes)
42
+}
43
+
44
+// checks a version 0 password hash; if successful, upgrades the database entry to version 1
45
+func handleLegacyPasswordV0(server *Server, account string, credentials AccountCredentials, passphrase string) (err error) {
46
+	var globalSaltString string
47
+	err = server.store.View(func(tx *buntdb.Tx) (err error) {
48
+		globalSaltString, err = tx.Get("crypto.salt")
49
+		return err
50
+	})
51
+	if err != nil {
52
+		return err
53
+	}
54
+	globalSalt, err := base64.StdEncoding.DecodeString(globalSaltString)
55
+	if err != nil {
56
+		return err
57
+	}
58
+
59
+	err = checkLegacyPasswordV0(credentials.PassphraseHash, globalSalt, credentials.PassphraseSalt, passphrase)
60
+	if err != nil {
61
+		// invalid password
62
+		return err
63
+	}
64
+
65
+	// upgrade credentials
66
+	err = server.accounts.setPassword(account, passphrase)
67
+	if err != nil {
68
+		server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err))
69
+	}
70
+
71
+	return nil
72
+}

+ 2
- 0
irc/nickserv.go Visa fil

@@ -312,6 +312,8 @@ func nsRegisterHandler(server *Server, client *Client, command, params string, r
312 312
 			errMsg = client.t("An account already exists for your certificate fingerprint")
313 313
 		} else if err == errAccountAlreadyRegistered {
314 314
 			errMsg = client.t("Account already exists")
315
+		} else if err == errAccountBadPassphrase {
316
+			errMsg = client.t("Passphrase contains forbidden characters or is otherwise invalid")
315 317
 		}
316 318
 		nsNotice(rb, errMsg)
317 319
 		return

+ 34
- 0
irc/passwd/bcrypt.go Visa fil

@@ -0,0 +1,34 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package passwd
5
+
6
+import "golang.org/x/crypto/bcrypt"
7
+import "golang.org/x/crypto/sha3"
8
+
9
+const (
10
+	MinCost     = bcrypt.MinCost
11
+	DefaultCost = 12 // ballpark: 250 msec on a modern Intel CPU
12
+)
13
+
14
+// implements Dropbox's strategy of applying an initial pass of a "normal"
15
+// (i.e., fast) cryptographically secure hash with 512 bits of output before
16
+// applying bcrypt. This allows the use of, e.g., Diceware/XKCD-style passphrases
17
+// that may be longer than the 80-character bcrypt limit.
18
+// https://blogs.dropbox.com/tech/2016/09/how-dropbox-securely-stores-your-passwords/
19
+
20
+// we are only using this for user-generated passwords, as opposed to the server
21
+// and operator passwords that are hashed by `oragono genpasswd` and then
22
+// hard-coded by the server admins into the config file, to avoid breaking
23
+// backwards compatibility (since we can't upgrade the config file on the fly
24
+// the way we can with the database).
25
+
26
+func GenerateFromPassword(password []byte, cost int) (result []byte, err error) {
27
+	sum := sha3.Sum512(password)
28
+	return bcrypt.GenerateFromPassword(sum[:], cost)
29
+}
30
+
31
+func CompareHashAndPassword(hashedPassword, password []byte) error {
32
+	sum := sha3.Sum512(password)
33
+	return bcrypt.CompareHashAndPassword(hashedPassword, sum[:])
34
+}

+ 58
- 0
irc/passwd/bcrypt_test.go Visa fil

@@ -0,0 +1,58 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package passwd
5
+
6
+import (
7
+	"testing"
8
+)
9
+
10
+func TestBasic(t *testing.T) {
11
+	hash, err := GenerateFromPassword([]byte("this is my passphrase"), DefaultCost)
12
+	if err != nil || len(hash) != 60 {
13
+		t.Errorf("bad password hash output: error %s, output %s, len %d", err, hash, len(hash))
14
+	}
15
+
16
+	if CompareHashAndPassword(hash, []byte("this is my passphrase")) != nil {
17
+		t.Errorf("hash comparison failed unexpectedly")
18
+	}
19
+
20
+	if CompareHashAndPassword(hash, []byte("this is not my passphrase")) == nil {
21
+		t.Errorf("hash comparison succeeded unexpectedly")
22
+	}
23
+}
24
+
25
+func TestLongPassphrases(t *testing.T) {
26
+	longPassphrase := make([]byte, 168)
27
+	for i := range longPassphrase {
28
+		longPassphrase[i] = 'a'
29
+	}
30
+	hash, err := GenerateFromPassword(longPassphrase, DefaultCost)
31
+	if err != nil {
32
+		t.Errorf("bad password hash output: error %s", err)
33
+	}
34
+
35
+	if CompareHashAndPassword(hash, longPassphrase) != nil {
36
+		t.Errorf("hash comparison failed unexpectedly")
37
+	}
38
+
39
+	// change a byte of the passphrase beyond the normal 80-character
40
+	// bcrypt truncation boundary:
41
+	longPassphrase[150] = 'b'
42
+	if CompareHashAndPassword(hash, longPassphrase) == nil {
43
+		t.Errorf("hash comparison succeeded unexpectedly")
44
+	}
45
+}
46
+
47
+// this could be useful for tuning the cost parameter on specific hardware
48
+func BenchmarkComparisons(b *testing.B) {
49
+	pass := []byte("passphrase for benchmarking")
50
+	hash, err := GenerateFromPassword(pass, DefaultCost)
51
+	if err != nil {
52
+		b.Errorf("bad output")
53
+	}
54
+	b.ResetTimer()
55
+	for i := 0; i < b.N; i++ {
56
+		CompareHashAndPassword(hash, pass)
57
+	}
58
+}

+ 0
- 66
irc/passwd/salted.go Visa fil

@@ -1,66 +0,0 @@
1
-// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
2
-// released under the MIT license
3
-
4
-package passwd
5
-
6
-import (
7
-	"crypto/rand"
8
-
9
-	"golang.org/x/crypto/bcrypt"
10
-)
11
-
12
-const (
13
-	// newSaltLen is how many bytes long newly-generated salts are.
14
-	newSaltLen = 30
15
-	// defaultPasswordCost is the bcrypt cost we use for passwords.
16
-	defaultPasswordCost = 14
17
-)
18
-
19
-// NewSalt returns a salt for crypto uses.
20
-func NewSalt() ([]byte, error) {
21
-	salt := make([]byte, newSaltLen)
22
-	_, err := rand.Read(salt)
23
-
24
-	if err != nil {
25
-		var emptySalt []byte
26
-		return emptySalt, err
27
-	}
28
-
29
-	return salt, nil
30
-}
31
-
32
-// SaltedManager supports the hashing and comparing of passwords with the given salt.
33
-type SaltedManager struct {
34
-	salt []byte
35
-}
36
-
37
-// NewSaltedManager returns a new SaltedManager with the given salt.
38
-func NewSaltedManager(salt []byte) SaltedManager {
39
-	return SaltedManager{
40
-		salt: salt,
41
-	}
42
-}
43
-
44
-// assemblePassword returns an assembled slice of bytes for the given password details.
45
-func (sm *SaltedManager) assemblePassword(specialSalt []byte, password string) []byte {
46
-	var assembledPasswordBytes []byte
47
-	assembledPasswordBytes = append(assembledPasswordBytes, sm.salt...)
48
-	assembledPasswordBytes = append(assembledPasswordBytes, '-')
49
-	assembledPasswordBytes = append(assembledPasswordBytes, specialSalt...)
50
-	assembledPasswordBytes = append(assembledPasswordBytes, '-')
51
-	assembledPasswordBytes = append(assembledPasswordBytes, []byte(password)...)
52
-	return assembledPasswordBytes
53
-}
54
-
55
-// GenerateFromPassword encrypts the given password.
56
-func (sm *SaltedManager) GenerateFromPassword(specialSalt []byte, password string) ([]byte, error) {
57
-	assembledPasswordBytes := sm.assemblePassword(specialSalt, password)
58
-	return bcrypt.GenerateFromPassword(assembledPasswordBytes, defaultPasswordCost)
59
-}
60
-
61
-// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
62
-// Returns nil on success, or an error on failure.
63
-func (sm *SaltedManager) CompareHashAndPassword(hashedPassword []byte, specialSalt []byte, password string) error {
64
-	assembledPasswordBytes := sm.assemblePassword(specialSalt, password)
65
-	return bcrypt.CompareHashAndPassword(hashedPassword, assembledPasswordBytes)
66
-}

+ 0
- 86
irc/passwd/salted_test.go Visa fil

@@ -1,86 +0,0 @@
1
-// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
2
-// released under the MIT license
3
-
4
-package passwd
5
-
6
-import (
7
-	"encoding/base64"
8
-	"testing"
9
-)
10
-
11
-type SaltedPasswordTest struct {
12
-	ManagerSalt string
13
-	Salt        string
14
-	Hash        string
15
-	Password    string
16
-}
17
-
18
-var SaltedPasswords = []SaltedPasswordTest{
19
-	{
20
-		ManagerSalt: "3TPITDVf/NGb4OlCyV1uZNW1H7zy3BFos+Dsu7dj",
21
-		Salt:        "b6oVqshJUfcm1zWEtqwKqUVylqLONAZfqt17ns+Y",
22
-		Hash:        "JDJhJDE0JFYuT28xOFFNZldaaTI1UWpzNENMeHVKdm5vS1lkL2tFL1lFVkQ2a0loUEY2Vzk3UTZSVDVP",
23
-		Password:    "test",
24
-	},
25
-	{
26
-		ManagerSalt: "iNGeNEfuPihM8kYDZ/C6qAJ0JERKeKkUYp6wYDU0",
27
-		Salt:        "U7TA6k6VLSLHfdjSsQH0vc3Jqq6cUezJNyd0DC9c",
28
-		Hash:        "JDJhJDE0JEguY2Rva3VOTVRrNm1VeGdXWjAwamViMGNvV0xYZFdHcTZjenFCRWE3Ymt2N1JiSFJDZlYy",
29
-		Password:    "test2",
30
-	},
31
-	{
32
-		ManagerSalt: "ghKJaaSNTjuFmgLRqrgY4FGfx8wXEGOBE02PZvbv",
33
-		Salt:        "NO/mtrMhGjX1FGDGdpGrDJIi4jxsb0aFa7ybId7r",
34
-		Hash:        "JDJhJDE0JEI0M055Z2NDcjNUanB5ZEJ5MzUybi5FT3o4Y1MyNXp2c1NDVS9hS0hOcUxSRDZTWmUxTnN5",
35
-		Password:    "supermono",
36
-	},
37
-}
38
-
39
-func TestSaltedPassword(t *testing.T) {
40
-	// check newly-generated password
41
-	managerSalt, err := NewSalt()
42
-	if err != nil {
43
-		t.Error("Could not generate manager salt")
44
-	}
45
-
46
-	salt, err := NewSalt()
47
-	if err != nil {
48
-		t.Error("Could not generate salt")
49
-	}
50
-
51
-	manager := NewSaltedManager(managerSalt)
52
-
53
-	passHash, err := manager.GenerateFromPassword(salt, "this is a test password")
54
-	if err != nil {
55
-		t.Error("Could not generate from password")
56
-	}
57
-
58
-	if manager.CompareHashAndPassword(passHash, salt, "this is a test password") != nil {
59
-		t.Error("Generated password does not match")
60
-	}
61
-
62
-	// check our stored passwords
63
-	for i, info := range SaltedPasswords {
64
-		// decode strings to bytes
65
-		managerSalt, err = base64.StdEncoding.DecodeString(info.ManagerSalt)
66
-		if err != nil {
67
-			t.Errorf("Could not decode manager salt for test %d", i)
68
-		}
69
-
70
-		salt, err := base64.StdEncoding.DecodeString(info.Salt)
71
-		if err != nil {
72
-			t.Errorf("Could not decode salt for test %d", i)
73
-		}
74
-
75
-		hash, err := base64.StdEncoding.DecodeString(info.Hash)
76
-		if err != nil {
77
-			t.Errorf("Could not decode hash for test %d", i)
78
-		}
79
-
80
-		// make sure our test values are still correct
81
-		manager := NewSaltedManager(managerSalt)
82
-		if manager.CompareHashAndPassword(hash, salt, info.Password) != nil {
83
-			t.Errorf("Password does not match for [%s]", info.Password)
84
-		}
85
-	}
86
-}

+ 0
- 53
irc/passwd/unsalted.go Visa fil

@@ -1,53 +0,0 @@
1
-// Copyright (c) 2012-2014 Jeremy Latt
2
-// released under the MIT license
3
-
4
-package passwd
5
-
6
-import (
7
-	"encoding/base64"
8
-	"errors"
9
-
10
-	"golang.org/x/crypto/bcrypt"
11
-)
12
-
13
-var (
14
-	// ErrEmptyPassword means that an empty password was given.
15
-	ErrEmptyPassword = errors.New("empty password")
16
-)
17
-
18
-// GenerateEncodedPasswordBytes returns an encrypted password, returning the bytes directly.
19
-func GenerateEncodedPasswordBytes(passwd string) (encoded []byte, err error) {
20
-	if passwd == "" {
21
-		err = ErrEmptyPassword
22
-		return
23
-	}
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)
31
-	encoded = base64.StdEncoding.EncodeToString(bcrypted)
32
-	return
33
-}
34
-
35
-// DecodePasswordHash takes a base64-encoded password hash and returns the appropriate bytes.
36
-func DecodePasswordHash(encoded string) (decoded []byte, err error) {
37
-	if encoded == "" {
38
-		err = ErrEmptyPassword
39
-		return
40
-	}
41
-	decoded, err = base64.StdEncoding.DecodeString(encoded)
42
-	return
43
-}
44
-
45
-// ComparePassword compares a given password with the given hash.
46
-func ComparePassword(hash, password []byte) error {
47
-	return bcrypt.CompareHashAndPassword(hash, password)
48
-}
49
-
50
-// ComparePasswordString compares a given password string with the given hash.
51
-func ComparePasswordString(hash []byte, password string) error {
52
-	return ComparePassword(hash, []byte(password))
53
-}

+ 0
- 54
irc/passwd/unsalted_test.go Visa fil

@@ -1,54 +0,0 @@
1
-// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
2
-// released under the MIT license
3
-
4
-package passwd
5
-
6
-import (
7
-	"testing"
8
-)
9
-
10
-var UnsaltedPasswords = map[string]string{
11
-	"test1":     "JDJhJDA0JFFwZ1V0RWZTMFVaMkFrdlRrTG9FZk9FNEZWbWkvVEhsdGFnSXlIUC5wVmpYTkNERFJPNlcu",
12
-	"test2":     "JDJhJDA0JHpQTGNqczlIanc3V2NFQ3JEOVlTM09aNkRTbGRsQzRyNmt3Q01aSUs2Y2xyWURVODZ1V0px",
13
-	"supernomo": "JDJhJDA0JHdJekhnQmk1VXQ4WUphL0pIL0tXQWVKVXJ6dXcvRDJ3WFljWW9XOGhzNllIbW1DRlFkL1VL",
14
-}
15
-
16
-func TestUnsaltedPassword(t *testing.T) {
17
-	for password, hash := range UnsaltedPasswords {
18
-		generatedHash, err := GenerateEncodedPassword(password)
19
-		if err != nil {
20
-			t.Errorf("Could not hash password for [%s]: %s", password, err.Error())
21
-		}
22
-
23
-		hashBytes, err := DecodePasswordHash(hash)
24
-		if err != nil {
25
-			t.Errorf("Could not decode hash for [%s]: %s", password, err.Error())
26
-		}
27
-
28
-		generatedHashBytes, err := DecodePasswordHash(generatedHash)
29
-		if err != nil {
30
-			t.Errorf("Could not decode generated hash for [%s]: %s", password, err.Error())
31
-		}
32
-
33
-		passwordBytes := []byte(password)
34
-
35
-		if ComparePassword(hashBytes, passwordBytes) != nil {
36
-			t.Errorf("Stored hash for [%s] did not match", password)
37
-		}
38
-		if ComparePassword(generatedHashBytes, passwordBytes) != nil {
39
-			t.Errorf("Generated hash for [%s] did not match", password)
40
-		}
41
-	}
42
-}
43
-
44
-func TestUnsaltedPasswordFailures(t *testing.T) {
45
-	_, err := GenerateEncodedPassword("")
46
-	if err != ErrEmptyPassword {
47
-		t.Error("Generating empty password did not fail as expected!")
48
-	}
49
-
50
-	_, err = DecodePasswordHash("")
51
-	if err != ErrEmptyPassword {
52
-		t.Error("Decoding empty password hash did not fail as expected!")
53
-	}
54
-}

+ 0
- 24
irc/server.go Visa fil

@@ -8,7 +8,6 @@ package irc
8 8
 import (
9 9
 	"bufio"
10 10
 	"crypto/tls"
11
-	"encoding/base64"
12 11
 	"fmt"
13 12
 	"log"
14 13
 	"math/rand"
@@ -31,7 +30,6 @@ import (
31 30
 	"github.com/oragono/oragono/irc/languages"
32 31
 	"github.com/oragono/oragono/irc/logger"
33 32
 	"github.com/oragono/oragono/irc/modes"
34
-	"github.com/oragono/oragono/irc/passwd"
35 33
 	"github.com/oragono/oragono/irc/sno"
36 34
 	"github.com/oragono/oragono/irc/utils"
37 35
 	"github.com/tidwall/buntdb"
@@ -90,7 +88,6 @@ type Server struct {
90 88
 	motdLines              []string
91 89
 	name                   string
92 90
 	nameCasefolded         string
93
-	passwords              *passwd.SaltedManager
94 91
 	rehashMutex            sync.Mutex // tier 4
95 92
 	rehashSignal           chan os.Signal
96 93
 	pprofServer            *http.Server
@@ -996,27 +993,6 @@ func (server *Server) loadDatastore(config *Config) error {
996 993
 	server.loadDLines()
997 994
 	server.loadKLines()
998 995
 
999
-	// load password manager
1000
-	server.logger.Debug("startup", "Loading passwords")
1001
-	err = server.store.View(func(tx *buntdb.Tx) error {
1002
-		saltString, err := tx.Get(keySalt)
1003
-		if err != nil {
1004
-			return fmt.Errorf("Could not retrieve salt string: %s", err.Error())
1005
-		}
1006
-
1007
-		salt, err := base64.StdEncoding.DecodeString(saltString)
1008
-		if err != nil {
1009
-			return err
1010
-		}
1011
-
1012
-		pwm := passwd.NewSaltedManager(salt)
1013
-		server.passwords = &pwm
1014
-		return nil
1015
-	})
1016
-	if err != nil {
1017
-		return fmt.Errorf("Could not load salt: %s", err.Error())
1018
-	}
1019
-
1020 996
 	server.channelRegistry = NewChannelRegistry(server)
1021 997
 
1022 998
 	server.accounts = NewAccountManager(server)

+ 3
- 3
oragono.go Visa fil

@@ -17,8 +17,8 @@ import (
17 17
 	"github.com/oragono/oragono/irc"
18 18
 	"github.com/oragono/oragono/irc/logger"
19 19
 	"github.com/oragono/oragono/irc/mkcerts"
20
-	"github.com/oragono/oragono/irc/passwd"
21 20
 	stackimpact "github.com/stackimpact/stackimpact-go"
21
+	"golang.org/x/crypto/bcrypt"
22 22
 	"golang.org/x/crypto/ssh/terminal"
23 23
 )
24 24
 
@@ -73,11 +73,11 @@ Options:
73 73
 		if confirm != password {
74 74
 			log.Fatal("passwords do not match")
75 75
 		}
76
-		encoded, err := passwd.GenerateEncodedPassword(password)
76
+		hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
77 77
 		if err != nil {
78 78
 			log.Fatal("encoding error:", err.Error())
79 79
 		}
80
-		fmt.Println(encoded)
80
+		fmt.Println(string(hash))
81 81
 	} else if arguments["initdb"].(bool) {
82 82
 		irc.InitDB(config.Datastore.Path)
83 83
 		if !arguments["--quiet"].(bool) {

+ 5
- 2
oragono.yaml Visa fil

@@ -76,7 +76,7 @@ server:
76 76
             fingerprint: 938dd33f4b76dcaf7ce5eb25c852369cb4b8fb47ba22fc235aa29c6623a5f182
77 77
 
78 78
             # password the gateway uses to connect, made with  oragono genpasswd
79
-            password: JDJhJDA0JG9rTTVERlNRa0hpOEZpNkhjZE95SU9Da1BseFdlcWtOTEQxNEFERVlqbEZNTkdhOVlYUkMu
79
+            password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee"
80 80
 
81 81
             # hosts that can use this webirc command
82 82
             # you should also add these addresses to the connection limits and throttling exemption lists
@@ -145,6 +145,9 @@ accounts:
145 145
         # can users register new accounts?
146 146
         enabled: true
147 147
 
148
+        # this is the bcrypt cost we'll use for account passwords
149
+        bcrypt-cost: 12
150
+
148 151
         # length of time a user has to verify their account before it can be re-registered
149 152
         verify-timeout: "32h"
150 153
 
@@ -304,7 +307,7 @@ opers:
304 307
 
305 308
         # password to login with /OPER command
306 309
         # generated using  "oragono genpasswd"
307
-        password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
310
+        password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu"
308 311
 
309 312
 # logging, takes inspiration from Insp
310 313
 logging:

Laddar…
Avbryt
Spara