380 lines
10 KiB
Go
380 lines
10 KiB
Go
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))
|
|
}
|