diff --git a/argon2.go b/argon2.go index 779cf17..d91fd73 100644 --- a/argon2.go +++ b/argon2.go @@ -118,8 +118,12 @@ func (hasher *argon2Hasher) Verify(password, encoded string) bool { } func (hasher *argon2Hasher) MustUpdate(encoded string) bool { - // TODO(zhaowentao): 处理 Verify - return false + pi, err := hasher.Decode(encoded) + if err != nil { + return false + } + p := pi.Others.(*Argon2Params) + return *p != *hasher.params } func (hasher *argon2Hasher) Harden(password, encoded string) (string, error) { diff --git a/argon2_test.go b/argon2_test.go index f3dad0c..e6eb842 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -138,3 +138,39 @@ func TestArgon2(t *testing.T) { t.Errorf("Verify() should be false") } } + +func TestMustUpdateForArgon2(t *testing.T) { + opt := &HasherOption{ + Iterations: 1, + Algorithm: argon2Algo, + Params: &Argon2Params{ + memory: 32 * 1024, + iterations: 10, + parallelism: 2, + saltLength: 8, + keyLength: 32, + }, + } + hasher, _ := NewHasher(opt) + encoded, _ := hasher.Encode(password) + + if hasher.MustUpdate(encoded) { + t.Error("should not updated") + } + + opt2 := &HasherOption{ + Iterations: 1, + Algorithm: argon2Algo, + Params: &Argon2Params{ + memory: 32 * 1024, + iterations: 10, + parallelism: 2, + saltLength: 9, + keyLength: 32, + }, + } + hasher, _ = NewHasher(opt2) + if !hasher.MustUpdate(encoded) { + t.Error("should updated because of different param") + } +} diff --git a/bcrypt.go b/bcrypt.go index a28ebbb..62d855b 100644 --- a/bcrypt.go +++ b/bcrypt.go @@ -9,10 +9,11 @@ import ( type bcryptHasher struct { algo string + cost int } func (hasher *bcryptHasher) Encode(password string) (string, error) { - return hasher.encode(password, hasher.algo, bcrypt.DefaultCost) + return hasher.encode(password, hasher.algo, hasher.cost) } func (hasher *bcryptHasher) encode(password, algo string, cost int) (string, error) { @@ -72,8 +73,11 @@ func (hasher *bcryptHasher) Verify(password, encoded string) bool { } func (hasher *bcryptHasher) MustUpdate(encoded string) bool { - // TODO(zhaowentao) - return false + pi, err := hasher.Decode(encoded) + if err != nil { + return false + } + return pi.Iterations < hasher.cost } func (hasher *bcryptHasher) Harden(password, encoded string) (string, error) { @@ -82,5 +86,13 @@ func (hasher *bcryptHasher) Harden(password, encoded string) (string, error) { } func newBcryptHasher(opt *HasherOption) (Hasher, error) { - return &bcryptHasher{algo: opt.Algorithm}, nil + cost := bcrypt.DefaultCost + if opt.Iterations > cost { + cost = opt.Iterations + } + + return &bcryptHasher{ + algo: opt.Algorithm, + cost: cost, + }, nil } diff --git a/bcrypt_test.go b/bcrypt_test.go index 56b9321..998bd04 100644 --- a/bcrypt_test.go +++ b/bcrypt_test.go @@ -36,7 +36,6 @@ func TestBcrypt(t *testing.T) { Salt: "salt", Iterations: 1, } - password := "1qasw23" hasher, err := NewHasher(opt) if err != nil { t.Errorf("failed to new %s hasher: %s", opt.Algorithm, err) @@ -72,3 +71,46 @@ func TestBcrypt(t *testing.T) { t.Errorf("wrong algorithm: %s", opt.Algorithm) } } + +func TestMustUpdateForBcrypt(t *testing.T) { + opt := &HasherOption{ + Algorithm: bcryptAlgo, + Salt: "salt", + Iterations: 12, + } + hasher, _ := NewHasher(opt) + encoded, _ := hasher.Encode(password) + if hasher.MustUpdate(encoded) { + t.Error("should not update") + } + + opt2 := &HasherOption{ + Algorithm: bcryptAlgo, + Salt: "saltsaltsaltsalt", + Iterations: 12, + } + hasher, _ = NewHasher(opt2) + if hasher.MustUpdate(encoded) { + t.Error("should not update because of no use salt") + } + + opt3 := &HasherOption{ + Algorithm: bcryptAlgo, + Salt: "saltsaltsaltsa", + Iterations: 14, + } + hasher, _ = NewHasher(opt3) + if !hasher.MustUpdate(encoded) { + t.Error("should update because of bigger cost") + } + + opt4 := &HasherOption{ + Algorithm: bcryptAlgo, + Salt: "saltsaltsaltsa", + Iterations: 11, + } + hasher, _ = NewHasher(opt4) + if hasher.MustUpdate(encoded) { + t.Error("should not update because of smaller cost") + } +} diff --git a/hoption.go b/hoption.go index 884558e..021289b 100644 --- a/hoption.go +++ b/hoption.go @@ -4,7 +4,7 @@ import ( "strings" ) -const saltEntropy = 128 +const saltEntropy = 64 const ( md5Algo = "md5" diff --git a/md5.go b/md5.go index cc5a8d5..f415530 100644 --- a/md5.go +++ b/md5.go @@ -61,7 +61,17 @@ func (hasher *md5Hasher) MustUpdate(encoded string) bool { if err != nil { return false } - return mustUpdateSalt(pi.Salt, saltEntropy) + + if pi.Algorithm == unsaltedMd5Algo { + return false + } + + ret := mustUpdateSalt(pi.Salt, saltEntropy) + if ret { + return true + } + + return len(pi.Salt) < len(hasher.salt) } func (hasher *md5Hasher) Harden(password, encoded string) (string, error) { diff --git a/md5_test.go b/md5_test.go index be0d721..0daebaa 100644 --- a/md5_test.go +++ b/md5_test.go @@ -48,7 +48,6 @@ func TestUnsaltedMd5(t *testing.T) { t.Errorf("error should be nil, now %s", err) } - password := "1qsw23ed" encoded, err := hasher.Encode(password) if err != nil { t.Errorf("failed to Encode(password): %s", err) @@ -60,3 +59,42 @@ func TestUnsaltedMd5(t *testing.T) { t.Errorf("MD5 error") } } + +func TestMustUpdateForMd5(t *testing.T) { + opt := HasherOption{ + Algorithm: unsaltedMd5Algo, + Salt: "", + Iterations: 1, + } + hasher, _ := NewHasher(&opt) + encoded, _ := hasher.Encode(password) + + if hasher.MustUpdate(encoded) { + t.Error("should not update") + } + + opt1 := HasherOption{ + Algorithm: md5Algo, + Salt: "saltsaltsaltsalt", + Iterations: 1, + } + hasher, _ = NewHasher(&opt1) + encoded, _ = hasher.Encode(password) + if hasher.MustUpdate(encoded) { + t.Error("should not update") + } + wrongEncoded := "aa" + encoded + if hasher.MustUpdate(wrongEncoded) { + t.Error("should not update because of wrong encoded") + } + + opt2 := HasherOption{ + Algorithm: md5Algo, + Salt: "saltsaltsaltsa", + Iterations: 1, + } + hasher, _ = NewHasher(&opt2) + if hasher.MustUpdate(encoded) { + t.Error("should not update because of short salt") + } +} diff --git a/pbkdf2.go b/pbkdf2.go index 95a5107..4b7fe2a 100644 --- a/pbkdf2.go +++ b/pbkdf2.go @@ -76,8 +76,7 @@ func (hasher *pbkdf2Hasher) MustUpdate(encoded string) bool { } updateSalt := mustUpdateSalt(pi.Salt, saltEntropy) - - return pi.Iterations < hasher.iterCount || updateSalt + return pi.Iterations < hasher.iterCount || updateSalt || len(hasher.salt) > len(pi.Salt) } func (hasher *pbkdf2Hasher) Harden(password, encoded string) (string, error) { diff --git a/pbkdf2_test.go b/pbkdf2_test.go index c306a90..038b7f1 100644 --- a/pbkdf2_test.go +++ b/pbkdf2_test.go @@ -1,6 +1,14 @@ package password -import "testing" +import ( + "crypto/sha1" // #nosec + "crypto/sha256" + "reflect" + "strings" + "testing" +) + +const password = "1qasw23ed" func TestPbkdf2Sha1Hasher(t *testing.T) { opt := HasherOption{ @@ -8,12 +16,24 @@ func TestPbkdf2Sha1Hasher(t *testing.T) { Salt: "salt", Iterations: 10000, } - password := "1qasw23ed" hasher, err := NewHasher(&opt) if err != nil { t.Errorf("failed to new %s hasher: %s", pbkdf2Sha1Algo, err) } + + pHasher := hasher.(*pbkdf2Hasher) + size, newFunc := pHasher.getSizeAndNew() + if opt.Algorithm == pbkdf2Sha1Algo { + if size != sha1.Size { + t.Error("wrong size") + } + + if reflect.ValueOf(newFunc).Pointer() != reflect.ValueOf(sha1.New).Pointer() { + t.Error("wrong newFunc") + } + } + encoded, err := hasher.Encode(password) if err != nil { t.Errorf("failed to encode password with %s: %s", pbkdf2Sha1Algo, err) @@ -31,12 +51,24 @@ func TestPbkdf2Sha256Hasher(t *testing.T) { Salt: "salt", Iterations: 10000, } - password := "1qasw23ed" hasher, err := NewHasher(&opt) if err != nil { t.Errorf("failed to new %s hasher: %s", pbkdf2Sha256Algo, err) } + + pHasher := hasher.(*pbkdf2Hasher) + size, newFunc := pHasher.getSizeAndNew() + if opt.Algorithm == pbkdf2Sha256Algo { + if size != sha256.Size { + t.Error("wrong size") + } + + if reflect.ValueOf(newFunc).Pointer() != reflect.ValueOf(sha256.New).Pointer() { + t.Error("wrong newFunc") + } + } + encoded, err := hasher.Encode(password) if err != nil { t.Errorf("failed to encode password with %s: %s", pbkdf2Sha256Algo, err) @@ -47,6 +79,13 @@ func TestPbkdf2Sha256Hasher(t *testing.T) { t.Error("Decode(wrongEncoded) should error") } + pp := strings.SplitN(encoded, sep, 4) + pp[1] = "aa" + wrongEncoded2 := strings.Join(pp, sep) + if _, err = hasher.Decode(wrongEncoded2); err == nil { + t.Error("Decode(wrongEncoded2) should error") + } + if hasher.Verify(password, wrongEncoded) { t.Error("Verify(password, wrongEncoded) error") } @@ -54,3 +93,51 @@ func TestPbkdf2Sha256Hasher(t *testing.T) { t.Errorf("Algo %s error", pbkdf2Sha256Algo) } } + +func TestMustUpdateForPbkdf2Sha256(t *testing.T) { + opt := HasherOption{ + Algorithm: pbkdf2Sha256Algo, + Salt: "saltsaltsalt", + Iterations: 10000, + } + + hasher, _ := NewHasher(&opt) + encoded, err := hasher.Encode(password) + if err != nil { + t.Errorf("failed to encode password with %s: %s", pbkdf2Sha256Algo, err) + } + + if hasher.MustUpdate(encoded) { + t.Error("should not update") + } + + opt2 := HasherOption{ + Algorithm: pbkdf2Sha256Algo, + Salt: "saltsaltsalt", + Iterations: 10001, + } + hasher2, _ := NewHasher(&opt2) + if !hasher2.MustUpdate(encoded) { + t.Error("should update because of Iterations") + } + + opt3 := HasherOption{ + Algorithm: pbkdf2Sha256Algo, + Salt: "saltsaltsaltsaltsalt11", + Iterations: 10000, + } + hasher3, _ := NewHasher(&opt3) + if !hasher3.MustUpdate(encoded) { + t.Error("should update because of Salt") + } + + opt4 := HasherOption{ + Algorithm: pbkdf2Sha256Algo, + Salt: "saltsaltsalt", + Iterations: 9000, + } + hasher4, _ := NewHasher(&opt4) + if hasher4.MustUpdate(encoded) { + t.Error("should not update because of less Iterations") + } +} diff --git a/scrypt.go b/scrypt.go index 46d3722..547e45a 100644 --- a/scrypt.go +++ b/scrypt.go @@ -76,7 +76,11 @@ func (hasher *scryptHasher) Verify(password, encoded string) bool { } func (hasher *scryptHasher) MustUpdate(encoded string) bool { - return false + pi, err := hasher.Decode(encoded) + if err != nil { + return false + } + return mustUpdateSalt(pi.Salt, saltEntropy) || len(pi.Salt) < len(hasher.salt) } func (hasher *scryptHasher) Harden(password, encoded string) (string, error) { diff --git a/scrypt_test.go b/scrypt_test.go index e051842..95254b2 100644 --- a/scrypt_test.go +++ b/scrypt_test.go @@ -5,10 +5,9 @@ import "testing" func TestScrypt(t *testing.T) { opt := &HasherOption{ Algorithm: scryptAlgo, - Salt: "salt", + Salt: "saltsaltsalt", Iterations: 1, } - password := "1qasw23ed" hasher, err := NewHasher(opt) if err != nil { @@ -39,3 +38,49 @@ func TestScrypt(t *testing.T) { t.Error("scrypt verify error") } } + +func TestMustUpdateForScrypt(t *testing.T) { + opt := &HasherOption{ + Algorithm: scryptAlgo, + Salt: "saltsaltsalt", + Iterations: 1, + } + hasher, err := NewHasher(opt) + if err != nil { + t.Errorf("failed to new scrypt hasher: %s", err) + } + + encoded, err := hasher.Encode(password) + if err != nil { + t.Errorf("failed to encode password with scrypt: %s", err) + } + + if hasher.MustUpdate(encoded) { + t.Error("should not update") + } + + wrongEncoded := "aa" + encoded + if hasher.MustUpdate(wrongEncoded) { + t.Error("should not update because of wrong encoded") + } + + opt2 := &HasherOption{ + Algorithm: scryptAlgo, + Salt: "saltsaltsalt2", + Iterations: 1, + } + hasher, _ = NewHasher(opt2) + if !hasher.MustUpdate(encoded) { + t.Error("should update because of longer salt") + } + + opt3 := &HasherOption{ + Algorithm: scryptAlgo, + Salt: "saltsaltsal", + Iterations: 1, + } + hasher, _ = NewHasher(opt3) + if hasher.MustUpdate(encoded) { + t.Error("should not update because of shorter salt") + } +} diff --git a/sha1.go b/sha1.go index d9ceff9..0ec5f3e 100644 --- a/sha1.go +++ b/sha1.go @@ -46,7 +46,11 @@ func (hasher *sha1Hasher) Verify(password, encoded string) bool { } func (hasher *sha1Hasher) MustUpdate(encoded string) bool { - return false + pi, err := hasher.Decode(encoded) + if err != nil { + return false + } + return mustUpdateSalt(pi.Salt, saltEntropy) || len(pi.Salt) < len(hasher.salt) } func (hasher *sha1Hasher) Harden(password, encoded string) (string, error) { diff --git a/sha1_test.go b/sha1_test.go index cc62489..32f766c 100644 --- a/sha1_test.go +++ b/sha1_test.go @@ -21,14 +21,13 @@ func TestSha1WithNoSalt(t *testing.T) { } func TestSha1(t *testing.T) { - salt := "sha1" + salt := "sha1sha1sha1" opt := HasherOption{ Algorithm: sha1Algo, Salt: salt, Iterations: 1, } - password := "1qasw23ed" hasher, err := NewHasher(&opt) if err != nil { t.Errorf("failed to new sha1 hasher: %s", err) @@ -60,3 +59,43 @@ func TestSha1(t *testing.T) { t.Error("failed to verify password with sha1") } } + +func TestMustUpdateForSha1(t *testing.T) { + opt := HasherOption{ + Algorithm: sha1Algo, + Salt: "sha1sha1sha1", + Iterations: 1, + } + + hasher, _ := NewHasher(&opt) + encoded, _ := hasher.Encode(password) + + if hasher.MustUpdate(encoded) { + t.Error("should not updated") + } + + wrongEncoded := "aa" + encoded + if hasher.MustUpdate(wrongEncoded) { + t.Error("should not updated because of wrong encoded") + } + + opt2 := HasherOption{ + Algorithm: sha1Algo, + Salt: "sha1sha1sha12", + Iterations: 1, + } + hasher, _ = NewHasher(&opt2) + if !hasher.MustUpdate(encoded) { + t.Error("should updated because of longer salt") + } + + opt3 := HasherOption{ + Algorithm: sha1Algo, + Salt: "sha1sha1sha", + Iterations: 1, + } + hasher, _ = NewHasher(&opt3) + if hasher.MustUpdate(encoded) { + t.Error("should not updated because of shorter salt") + } +} diff --git a/utils.go b/utils.go index c90808c..dc83424 100644 --- a/utils.go +++ b/utils.go @@ -5,8 +5,10 @@ import ( "math" ) +const randomChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + func mustUpdateSalt(salt string, entropy int) bool { - clen := float64(len("RANDOM_STRING_CHARS")) + clen := float64(len(randomChars)) return float64(len(salt))*math.Log2(clen) < float64(entropy) } diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..fee5ec6 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,42 @@ +package password + +import "testing" + +func TestMustUpdateSalt(t *testing.T) { + data := []struct { + name string + salt string + shouldUpdate bool + }{ + { + name: "salt is too short", + salt: "salt", + shouldUpdate: true, + }, + { + name: "correct salt", + salt: "saltsaltsaltsaltsaltsa", + shouldUpdate: false, + }, + { + name: "correct salt, max length is 11", + salt: "saltsaltsaltsaltsalts", + shouldUpdate: false, + }, + } + + for _, d := range data { + t.Run(d.name, func(t *testing.T) { + ret := mustUpdateSalt(d.salt, saltEntropy) + if d.shouldUpdate { + if d.shouldUpdate != ret { + t.Error("bad salt") + } + } else { + if d.shouldUpdate != ret { + t.Error("bad salt should be updated") + } + } + }) + } +} diff --git a/validator_test.go b/validator_test.go index 64bfb3d..f5e0323 100644 --- a/validator_test.go +++ b/validator_test.go @@ -32,7 +32,6 @@ func TestValidatorOptionWithCommonPasswordURL(t *testing.T) { if err != nil { t.Errorf("should be nil, now %s", err) } - t.Logf("CommonPasswords: %+v\n", voption.CommonPasswords) if len(voption.CommonPasswords) != 2 { t.Errorf("get CommonPasswordURL error")