package cryptext import ( "encoding/json" "git.blackforestbytes.com/BlackForestBytes/goext/tst" "strings" "testing" ) func TestPassHashInvalidEmpty(t *testing.T) { ph := PassHash("") tst.AssertFalse(t, ph.Valid()) tst.AssertFalse(t, ph.HasTOTP()) tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) } func TestPassHashInvalidGarbage(t *testing.T) { for _, raw := range []string{ "garbage", "99|nope", "abc|payload", "3|onlytwo", "4|onlytwo", "5|onlytwo", "2|notbase64!|notbase64!", "1|!!!notbase64!!!", "3|!!notb64|!!notb64|0", "3|abc|!!notb64|0", } { ph := PassHash(raw) if ph.Valid() { t.Errorf("expected %q to be invalid", raw) } } } func TestPassHashVerifyInvalid(t *testing.T) { ph := PassHash("garbage-value") tst.AssertFalse(t, ph.Verify("anything", nil)) } func TestPassHashUpgradeInvalid(t *testing.T) { ph := PassHash("garbage-value") _, err := ph.Upgrade("anything") if err == nil { t.Errorf("expected error for invalid PassHash upgrade") } } func TestPassHashStringRoundtrip(t *testing.T) { ph, err := HashPassword("hunter2", nil) tst.AssertNoErr(t, err) tst.AssertEqual(t, ph.String(), string(ph)) } func TestPassHashMarshalJSONEmpty(t *testing.T) { ph := PassHash("") data, err := json.Marshal(ph) tst.AssertNoErr(t, err) tst.AssertEqual(t, string(data), `""`) } func TestPassHashMarshalJSONMasked(t *testing.T) { ph, err := HashPassword("hunter2", nil) tst.AssertNoErr(t, err) data, err := json.Marshal(ph) tst.AssertNoErr(t, err) tst.AssertEqual(t, string(data), `"*****"`) } func TestPassHashDataV0(t *testing.T) { ph, err := HashPasswordV0("test123") tst.AssertNoErr(t, err) v, seed, payload, hastotp, totpsecret, valid := ph.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, 0) tst.AssertEqual(t, len(seed), 0) tst.AssertEqual(t, string(payload), "test123") tst.AssertFalse(t, hastotp) tst.AssertEqual(t, len(totpsecret), 0) } func TestPassHashDataV1(t *testing.T) { ph, err := HashPasswordV1("test123") tst.AssertNoErr(t, err) v, seed, payload, hastotp, _, valid := ph.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, 1) tst.AssertEqual(t, len(seed), 0) tst.AssertEqual(t, len(payload), 32) // sha-256 is 32 bytes tst.AssertFalse(t, hastotp) } func TestPassHashDataV2(t *testing.T) { ph, err := HashPasswordV2("test123") tst.AssertNoErr(t, err) v, seed, payload, hastotp, _, valid := ph.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, 2) tst.AssertEqual(t, len(seed), 32) tst.AssertEqual(t, len(payload), 32) tst.AssertFalse(t, hastotp) } func TestPassHashDataV3(t *testing.T) { ph, err := HashPasswordV3("test123", nil) tst.AssertNoErr(t, err) v, seed, payload, hastotp, _, valid := ph.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, 3) tst.AssertEqual(t, len(seed), 32) tst.AssertEqual(t, len(payload), 32) tst.AssertFalse(t, hastotp) } func TestPassHashDataV4(t *testing.T) { ph, err := HashPasswordV4("test123", nil) tst.AssertNoErr(t, err) v, _, _, hastotp, _, valid := ph.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, 4) tst.AssertFalse(t, hastotp) } func TestPassHashDataV5(t *testing.T) { ph, err := HashPasswordV5("test123", nil) tst.AssertNoErr(t, err) v, _, _, hastotp, _, valid := ph.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, 5) tst.AssertFalse(t, hastotp) } func TestPassHashLatestIsV5(t *testing.T) { ph, err := HashPassword("test", nil) tst.AssertNoErr(t, err) v, _, _, _, _, valid := ph.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, LatestPassHashVersion) tst.AssertEqual(t, v, 5) } func TestPassHashUpgradeLatestIsNoop(t *testing.T) { ph, err := HashPassword("test", nil) tst.AssertNoErr(t, err) tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) ph2, err := ph.Upgrade("test") tst.AssertNoErr(t, err) tst.AssertEqual(t, string(ph), string(ph2)) } func TestPassHashClearTOTPInvalid(t *testing.T) { _, err := PassHash("garbage").ClearTOTP() if err == nil { t.Errorf("expected error from ClearTOTP on invalid") } } func TestPassHashClearTOTPV0V1V2Noop(t *testing.T) { ph0, _ := HashPasswordV0("x") r0, err := ph0.ClearTOTP() tst.AssertNoErr(t, err) tst.AssertEqual(t, string(r0), string(ph0)) ph1, _ := HashPasswordV1("x") r1, err := ph1.ClearTOTP() tst.AssertNoErr(t, err) tst.AssertEqual(t, string(r1), string(ph1)) ph2, _ := HashPasswordV2("x") r2, err := ph2.ClearTOTP() tst.AssertNoErr(t, err) tst.AssertEqual(t, string(r2), string(ph2)) } func TestPassHashClearTOTPV3(t *testing.T) { secret := []byte{0x01, 0x02, 0x03} ph, err := HashPasswordV3("test123", secret) tst.AssertNoErr(t, err) tst.AssertTrue(t, ph.HasTOTP()) cleared, err := ph.ClearTOTP() tst.AssertNoErr(t, err) tst.AssertFalse(t, cleared.HasTOTP()) tst.AssertTrue(t, cleared.Valid()) tst.AssertTrue(t, cleared.Verify("test123", nil)) } func TestPassHashClearTOTPV4(t *testing.T) { secret := []byte{0x01, 0x02, 0x03} ph, err := HashPasswordV4("test123", secret) tst.AssertNoErr(t, err) tst.AssertTrue(t, ph.HasTOTP()) cleared, err := ph.ClearTOTP() tst.AssertNoErr(t, err) tst.AssertFalse(t, cleared.HasTOTP()) tst.AssertTrue(t, cleared.Verify("test123", nil)) } func TestPassHashClearTOTPV5(t *testing.T) { secret := []byte{0x01, 0x02, 0x03} ph, err := HashPasswordV5("test123", secret) tst.AssertNoErr(t, err) tst.AssertTrue(t, ph.HasTOTP()) cleared, err := ph.ClearTOTP() tst.AssertNoErr(t, err) tst.AssertFalse(t, cleared.HasTOTP()) tst.AssertTrue(t, cleared.Verify("test123", nil)) } func TestPassHashWithTOTPInvalid(t *testing.T) { _, err := PassHash("garbage").WithTOTP([]byte{0x01}) if err == nil { t.Errorf("expected error for WithTOTP on invalid") } } func TestPassHashWithTOTPV0V1V2Errors(t *testing.T) { ph0, _ := HashPasswordV0("x") if _, err := ph0.WithTOTP([]byte{0x01}); err == nil { t.Errorf("expected v0 not to support TOTP") } ph1, _ := HashPasswordV1("x") if _, err := ph1.WithTOTP([]byte{0x01}); err == nil { t.Errorf("expected v1 not to support TOTP") } ph2, _ := HashPasswordV2("x") if _, err := ph2.WithTOTP([]byte{0x01}); err == nil { t.Errorf("expected v2 not to support TOTP") } } func TestPassHashWithTOTPV3V4V5(t *testing.T) { secret := []byte{0xDE, 0xAD, 0xBE, 0xEF} ph3, _ := HashPasswordV3("pw", nil) tst.AssertFalse(t, ph3.HasTOTP()) r3, err := ph3.WithTOTP(secret) tst.AssertNoErr(t, err) tst.AssertTrue(t, r3.HasTOTP()) ph4, _ := HashPasswordV4("pw", nil) tst.AssertFalse(t, ph4.HasTOTP()) r4, err := ph4.WithTOTP(secret) tst.AssertNoErr(t, err) tst.AssertTrue(t, r4.HasTOTP()) ph5, _ := HashPasswordV5("pw", nil) tst.AssertFalse(t, ph5.HasTOTP()) r5, err := ph5.WithTOTP(secret) tst.AssertNoErr(t, err) tst.AssertTrue(t, r5.HasTOTP()) } func TestPassHashChangeInvalid(t *testing.T) { _, err := PassHash("garbage").Change("new-pw") if err == nil { t.Errorf("expected error from Change on invalid") } } func TestPassHashChangeKeepsVersion(t *testing.T) { cases := []struct { name string hashed func() (PassHash, error) version int }{ {"V0", func() (PassHash, error) { return HashPasswordV0("old") }, 0}, {"V1", func() (PassHash, error) { return HashPasswordV1("old") }, 1}, {"V2", func() (PassHash, error) { return HashPasswordV2("old") }, 2}, {"V3", func() (PassHash, error) { return HashPasswordV3("old", nil) }, 3}, {"V4", func() (PassHash, error) { return HashPasswordV4("old", nil) }, 4}, {"V5", func() (PassHash, error) { return HashPasswordV5("old", nil) }, 5}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { ph, err := c.hashed() tst.AssertNoErr(t, err) changed, err := ph.Change("new-pw") tst.AssertNoErr(t, err) v, _, _, _, _, valid := changed.Data() tst.AssertTrue(t, valid) tst.AssertEqual(t, v, c.version) tst.AssertTrue(t, changed.Verify("new-pw", nil)) tst.AssertFalse(t, changed.Verify("old", nil)) }) } } func TestPassHashChangeKeepsTOTPV3(t *testing.T) { secret := []byte{0xAB, 0xCD} ph, err := HashPasswordV3("old", secret) tst.AssertNoErr(t, err) tst.AssertTrue(t, ph.HasTOTP()) changed, err := ph.Change("new") tst.AssertNoErr(t, err) tst.AssertTrue(t, changed.HasTOTP()) } func TestPassHashV0Format(t *testing.T) { ph, err := HashPasswordV0("plaintext-pw") tst.AssertNoErr(t, err) tst.AssertTrue(t, strings.HasPrefix(string(ph), "0|")) tst.AssertEqual(t, string(ph), "0|plaintext-pw") } func TestPassHashV1Format(t *testing.T) { ph, err := HashPasswordV1("test") tst.AssertNoErr(t, err) tst.AssertTrue(t, strings.HasPrefix(string(ph), "1|")) } func TestPassHashV2Format(t *testing.T) { ph, err := HashPasswordV2("test") tst.AssertNoErr(t, err) tst.AssertTrue(t, strings.HasPrefix(string(ph), "2|")) tst.AssertEqual(t, strings.Count(string(ph), "|"), 2) } func TestPassHashV3Format(t *testing.T) { ph, err := HashPasswordV3("test", nil) tst.AssertNoErr(t, err) tst.AssertTrue(t, strings.HasPrefix(string(ph), "3|")) tst.AssertEqual(t, strings.Count(string(ph), "|"), 3) tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0")) } func TestPassHashV4Format(t *testing.T) { ph, err := HashPasswordV4("test", nil) tst.AssertNoErr(t, err) tst.AssertTrue(t, strings.HasPrefix(string(ph), "4|")) tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0")) } func TestPassHashV5Format(t *testing.T) { ph, err := HashPasswordV5("test", nil) tst.AssertNoErr(t, err) tst.AssertTrue(t, strings.HasPrefix(string(ph), "5|")) tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0")) } func TestPassHashV5VerifyLongPassword(t *testing.T) { // V5 hashes via sha512 first → bcrypt's 72-byte limit shouldn't apply longPw := strings.Repeat("a", 200) ph, err := HashPasswordV5(longPw, nil) tst.AssertNoErr(t, err) tst.AssertTrue(t, ph.Verify(longPw, nil)) tst.AssertFalse(t, ph.Verify(longPw+"x", nil)) } func TestPassHashV5DifferentEachCall(t *testing.T) { ph1, err := HashPasswordV5("samepw", nil) tst.AssertNoErr(t, err) ph2, err := HashPasswordV5("samepw", nil) tst.AssertNoErr(t, err) // Bcrypt salts internally — same password should produce different hashes tst.AssertNotEqual(t, string(ph1), string(ph2)) // Both must verify tst.AssertTrue(t, ph1.Verify("samepw", nil)) tst.AssertTrue(t, ph2.Verify("samepw", nil)) }