package security import ( "errors" "strings" "testing" ) // testPepper builds a pepper with two versions so rotation paths can be exercised. Active is v2. func testPepper(t *testing.T, active int) *Pepper { t.Helper() p, err := NewPepper(active, map[int][]byte{ 1: []byte("pepper key version one"), 2: []byte("pepper key version two"), }) if err != nil { t.Fatalf("NewPepper: %v", err) } return p } // testHasher uses a low iteration count so the suite stays fast; production uses PBKDF2Iterations. func testHasher(t *testing.T, pepper *Pepper, iters int) *Hasher { t.Helper() h, err := NewHasher(pepper, iters) if err != nil { t.Fatalf("NewHasher: %v", err) } return h } func TestPasswordHashVerifyRoundTrip(t *testing.T) { h := testHasher(t, testPepper(t, 2), 1000) encoded, version, err := h.Hash("correct horse battery") if err != nil { t.Fatalf("Hash: %v", err) } if version != 2 { t.Fatalf("version = %d, want active 2", version) } if !strings.HasPrefix(encoded, pbkdf2Prefix+"$") { t.Fatalf("encoded missing prefix: %q", encoded) } ok, needsRehash, err := h.Verify("correct horse battery", encoded, version) if err != nil { t.Fatalf("Verify: %v", err) } if !ok { t.Fatal("Verify rejected the correct password") } if needsRehash { t.Fatal("needsRehash true for a hash at current policy") } } func TestPasswordVerifyWrongPassword(t *testing.T) { h := testHasher(t, testPepper(t, 2), 1000) encoded, version, _ := h.Hash("the right one") ok, _, err := h.Verify("the wrong one", encoded, version) if err != nil { t.Fatalf("Verify: %v", err) } if ok { t.Fatal("Verify accepted a wrong password") } } func TestPasswordLengthCap(t *testing.T) { h := testHasher(t, testPepper(t, 2), 1000) long := strings.Repeat("a", MaxPasswordLength+1) if _, _, err := h.Hash(long); !errors.Is(err, ErrPasswordTooLong) { t.Fatalf("Hash(65 chars) err = %v, want ErrPasswordTooLong", err) } // A valid hash of a normal password must never match an over length input. encoded, version, _ := h.Hash("normal") ok, _, err := h.Verify(long, encoded, version) if err != nil { t.Fatalf("Verify(long): %v", err) } if ok { t.Fatal("Verify accepted an over length password") } } func TestPasswordRehashOnIterationUpgrade(t *testing.T) { pepper := testPepper(t, 2) old := testHasher(t, pepper, 1000) encoded, version, _ := old.Hash("rotate me") upgraded := testHasher(t, pepper, 2000) ok, needsRehash, err := upgraded.Verify("rotate me", encoded, version) if err != nil { t.Fatalf("Verify: %v", err) } if !ok { t.Fatal("Verify rejected a valid password after an iteration bump") } if !needsRehash { t.Fatal("needsRehash false despite a higher current iteration count") } } func TestPasswordRehashOnPepperRotation(t *testing.T) { // Hash under a hasher whose active pepper is v1, then verify under one whose active is v2. pepper1 := testPepper(t, 1) h1 := testHasher(t, pepper1, 1000) encoded, version, _ := h1.Hash("pepper rotate") if version != 1 { t.Fatalf("version = %d, want 1", version) } pepper2 := testPepper(t, 2) h2 := testHasher(t, pepper2, 1000) ok, needsRehash, err := h2.Verify("pepper rotate", encoded, version) if err != nil { t.Fatalf("Verify: %v", err) } if !ok { t.Fatal("Verify rejected a valid password written under an older pepper version") } if !needsRehash { t.Fatal("needsRehash false despite the pepper version trailing the active one") } } func TestPasswordUnknownPepperVersion(t *testing.T) { h := testHasher(t, testPepper(t, 2), 1000) encoded, _, _ := h.Hash("whatever") _, _, err := h.Verify("whatever", encoded, 99) if !errors.Is(err, ErrUnknownPepperVersion) { t.Fatalf("Verify(unknown version) err = %v, want ErrUnknownPepperVersion", err) } } func TestPasswordMalformedHash(t *testing.T) { h := testHasher(t, testPepper(t, 2), 1000) for _, bad := range []string{ "", "notpbkdf2$1000$abc$def", pbkdf2Prefix + "$notanumber$abc$def", pbkdf2Prefix + "$1000$abc", } { if _, _, err := h.Verify("x", bad, 2); !errors.Is(err, ErrMalformedHash) { t.Fatalf("Verify(%q) err = %v, want ErrMalformedHash", bad, err) } } }