This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user