This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAESSimpleEmptyData(t *testing.T) {
|
||||
pw := []byte("password")
|
||||
enc, err := EncryptAESSimple(pw, []byte{}, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertNotEqual(t, enc, "")
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, len(dec), 0)
|
||||
}
|
||||
|
||||
func TestAESSimpleEmptyPassword(t *testing.T) {
|
||||
pw := []byte{}
|
||||
plain := []byte("some content")
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(dec), string(plain))
|
||||
}
|
||||
|
||||
func TestAESSimpleWrongPassword(t *testing.T) {
|
||||
plain := []byte("Hello World")
|
||||
enc, err := EncryptAESSimple([]byte("right"), plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
_, err = DecryptAESSimple([]byte("wrong"), enc)
|
||||
if err == nil {
|
||||
t.Errorf("expected error when decrypting with wrong password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleInvalidBase32(t *testing.T) {
|
||||
_, err := DecryptAESSimple([]byte("pw"), "!!!not-base32!!!")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on invalid base32 input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleInvalidJSON(t *testing.T) {
|
||||
// "AAAAAAAA" decodes to valid base32 but not valid JSON
|
||||
_, err := DecryptAESSimple([]byte("pw"), "AAAAAAAA")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on invalid JSON payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleEmptyEncText(t *testing.T) {
|
||||
_, err := DecryptAESSimple([]byte("pw"), "")
|
||||
if err == nil {
|
||||
t.Errorf("expected error on empty text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleLargeData(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte(strings.Repeat("ABCDEFGHIJ", 1024))
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(dec), string(plain))
|
||||
}
|
||||
|
||||
func TestAESSimpleBinaryData(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte{0x00, 0x01, 0x02, 0x7F, 0x80, 0xFE, 0xFF, 0x00, 0xAA, 0x55}
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertArrayEqual(t, dec, plain)
|
||||
}
|
||||
|
||||
func TestAESSimpleDifferentRoundsForEachCall(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte("Hello")
|
||||
|
||||
enc1, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
enc2, err := EncryptAESSimple(pw, plain, 256)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
// Two separate encrypt calls on same plaintext should differ (random salt + IV)
|
||||
tst.AssertNotEqual(t, enc1, enc2)
|
||||
|
||||
// Both should decrypt back to the same plaintext
|
||||
d1, err := DecryptAESSimple(pw, enc1)
|
||||
tst.AssertNoErr(t, err)
|
||||
d2, err := DecryptAESSimple(pw, enc2)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(d1), string(plain))
|
||||
tst.AssertEqual(t, string(d2), string(plain))
|
||||
}
|
||||
|
||||
func TestAESSimpleVariableRounds(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte("rounds-test")
|
||||
|
||||
for _, r := range []int{16, 32, 64, 128, 256, 512, 1024} {
|
||||
enc, err := EncryptAESSimple(pw, plain, r)
|
||||
tst.AssertNoErr(t, err)
|
||||
dec, err := DecryptAESSimple(pw, enc)
|
||||
tst.AssertNoErr(t, err)
|
||||
tst.AssertEqual(t, string(dec), string(plain))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESSimpleResultIsBase32(t *testing.T) {
|
||||
pw := []byte("hunter12")
|
||||
plain := []byte("Hello World")
|
||||
|
||||
enc, err := EncryptAESSimple(pw, plain, 64)
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
for _, c := range enc {
|
||||
isUpper := c >= 'A' && c <= 'Z'
|
||||
isDigit := c >= '2' && c <= '7'
|
||||
if !(isUpper || isDigit) {
|
||||
t.Errorf("non-base32 character %q in output", c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStrSha256SameAsBytesSha256(t *testing.T) {
|
||||
inputs := []string{"", "a", "Hello World", "lorem ipsum dolor sit amet", "🎉 unicode"}
|
||||
for _, in := range inputs {
|
||||
tst.AssertEqual(t, StrSha256(in), BytesSha256([]byte(in)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrSha256Length(t *testing.T) {
|
||||
// SHA-256 hex output must always be 64 characters
|
||||
tst.AssertEqual(t, len(StrSha256("")), 64)
|
||||
tst.AssertEqual(t, len(StrSha256("x")), 64)
|
||||
tst.AssertEqual(t, len(StrSha256(strings.Repeat("x", 10000))), 64)
|
||||
}
|
||||
|
||||
func TestStrSha256Deterministic(t *testing.T) {
|
||||
v := "deterministic input"
|
||||
a := StrSha256(v)
|
||||
b := StrSha256(v)
|
||||
tst.AssertEqual(t, a, b)
|
||||
}
|
||||
|
||||
func TestStrSha256DifferentInputs(t *testing.T) {
|
||||
tst.AssertNotEqual(t, StrSha256("a"), StrSha256("b"))
|
||||
tst.AssertNotEqual(t, StrSha256("Hello"), StrSha256("hello"))
|
||||
tst.AssertNotEqual(t, StrSha256("Hello World"), StrSha256("Hello World "))
|
||||
}
|
||||
|
||||
func TestStrSha256IsHex(t *testing.T) {
|
||||
out := StrSha256("anything")
|
||||
for _, c := range out {
|
||||
isLowerHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
|
||||
if !isLowerHex {
|
||||
t.Errorf("non-hex char %q in StrSha256 output", c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesSha256NilSameAsEmpty(t *testing.T) {
|
||||
tst.AssertEqual(t, BytesSha256(nil), BytesSha256([]byte{}))
|
||||
}
|
||||
|
||||
func TestBytesSha256KnownVectors(t *testing.T) {
|
||||
// "abc" => sha-256 standard vector
|
||||
tst.AssertEqual(t, StrSha256("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
mathrand "math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestPronouncablePasswordLength(t *testing.T) {
|
||||
for _, n := range []int{1, 2, 3, 5, 8, 13, 21, 50, 128} {
|
||||
pw := PronouncablePassword(n)
|
||||
tst.AssertEqual(t, len(pw), n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordZeroOrNegative(t *testing.T) {
|
||||
tst.AssertEqual(t, PronouncablePassword(0), "")
|
||||
tst.AssertEqual(t, PronouncablePassword(-1), "")
|
||||
tst.AssertEqual(t, PronouncablePassword(-1000), "")
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordSeededDeterministic(t *testing.T) {
|
||||
pw1 := PronouncablePasswordSeeded(42, 16)
|
||||
pw2 := PronouncablePasswordSeeded(42, 16)
|
||||
tst.AssertEqual(t, pw1, pw2)
|
||||
tst.AssertEqual(t, len(pw1), 16)
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordSeededDifferentSeeds(t *testing.T) {
|
||||
pw1 := PronouncablePasswordSeeded(1, 16)
|
||||
pw2 := PronouncablePasswordSeeded(2, 16)
|
||||
tst.AssertNotEqual(t, pw1, pw2)
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordExtEntropy(t *testing.T) {
|
||||
rng := mathrand.New(mathrand.NewSource(1))
|
||||
pw, entropy := PronouncablePasswordExt(rng, 32)
|
||||
tst.AssertEqual(t, len(pw), 32)
|
||||
if entropy <= 0 {
|
||||
t.Errorf("expected positive entropy, got %f", entropy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordExtZeroLen(t *testing.T) {
|
||||
rng := mathrand.New(mathrand.NewSource(1))
|
||||
pw, entropy := PronouncablePasswordExt(rng, 0)
|
||||
tst.AssertEqual(t, pw, "")
|
||||
tst.AssertEqual(t, entropy, float64(0))
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordCharacters(t *testing.T) {
|
||||
// Output should be only ASCII letters
|
||||
for i := range 50 {
|
||||
pw := PronouncablePasswordSeeded(int64(i), 32)
|
||||
for _, c := range pw {
|
||||
if !unicode.IsLetter(c) || c > unicode.MaxASCII {
|
||||
t.Errorf("non-letter or non-ASCII rune %q in password %q", c, pw)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordStartsUpper(t *testing.T) {
|
||||
for i := range 50 {
|
||||
pw := PronouncablePasswordSeeded(int64(i), 16)
|
||||
if pw == "" {
|
||||
continue
|
||||
}
|
||||
first := rune(pw[0])
|
||||
if !unicode.IsUpper(first) {
|
||||
t.Errorf("expected first letter uppercase in %q (seed %d)", pw, i)
|
||||
}
|
||||
if !strings.ContainsRune(ppStartChar, first) {
|
||||
t.Errorf("expected first letter from start-set in %q (seed %d)", pw, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPpMakeSet(t *testing.T) {
|
||||
set := ppMakeSet("ABC")
|
||||
tst.AssertTrue(t, set['A'])
|
||||
tst.AssertTrue(t, set['B'])
|
||||
tst.AssertTrue(t, set['C'])
|
||||
tst.AssertFalse(t, set['D'])
|
||||
tst.AssertEqual(t, len(set), 3)
|
||||
}
|
||||
|
||||
func TestPpMakeSetEmpty(t *testing.T) {
|
||||
set := ppMakeSet("")
|
||||
tst.AssertEqual(t, len(set), 0)
|
||||
}
|
||||
|
||||
func TestPpCharType(t *testing.T) {
|
||||
v, c := ppCharType('A')
|
||||
tst.AssertTrue(t, v)
|
||||
tst.AssertFalse(t, c)
|
||||
|
||||
v, c = ppCharType('B')
|
||||
tst.AssertFalse(t, v)
|
||||
tst.AssertTrue(t, c)
|
||||
|
||||
v, c = ppCharType('Y')
|
||||
tst.AssertTrue(t, v)
|
||||
tst.AssertFalse(t, c)
|
||||
|
||||
v, c = ppCharType('1')
|
||||
tst.AssertFalse(t, v)
|
||||
tst.AssertFalse(t, c)
|
||||
}
|
||||
|
||||
func TestPpCharsetRemove(t *testing.T) {
|
||||
set := ppMakeSet("AEIOU")
|
||||
out := ppCharsetRemove("ABCDEFG", set, false)
|
||||
tst.AssertEqual(t, out, "BCDFG")
|
||||
}
|
||||
|
||||
func TestPpCharsetRemoveEmptyDisallowed(t *testing.T) {
|
||||
set := ppMakeSet("AB")
|
||||
out := ppCharsetRemove("AB", set, false)
|
||||
// when result would be empty and allowEmpty=false, it returns the original
|
||||
tst.AssertEqual(t, out, "AB")
|
||||
}
|
||||
|
||||
func TestPpCharsetRemoveEmptyAllowed(t *testing.T) {
|
||||
set := ppMakeSet("AB")
|
||||
out := ppCharsetRemove("AB", set, true)
|
||||
tst.AssertEqual(t, out, "")
|
||||
}
|
||||
|
||||
func TestPpCharsetFilter(t *testing.T) {
|
||||
set := ppMakeSet("AEIOU")
|
||||
out := ppCharsetFilter("ABCDEFG", set, false)
|
||||
tst.AssertEqual(t, out, "AE")
|
||||
}
|
||||
|
||||
func TestPpCharsetFilterEmptyDisallowed(t *testing.T) {
|
||||
set := ppMakeSet("XYZ")
|
||||
out := ppCharsetFilter("ABC", set, false)
|
||||
tst.AssertEqual(t, out, "ABC") // returns original when result empty & not allowed
|
||||
}
|
||||
|
||||
func TestPpCharsetFilterEmptyAllowed(t *testing.T) {
|
||||
set := ppMakeSet("XYZ")
|
||||
out := ppCharsetFilter("ABC", set, true)
|
||||
tst.AssertEqual(t, out, "")
|
||||
}
|
||||
|
||||
func TestPronouncablePasswordContinuationFollowsRules(t *testing.T) {
|
||||
// Make sure each continuation pair (lowercased) appears in ppContinuation
|
||||
// Note: when a new segment starts (uppercase letter mid-string), the continuation
|
||||
// check does not apply across the segment boundary.
|
||||
for s := range 30 {
|
||||
seed := int64(s)
|
||||
pw := PronouncablePasswordSeeded(seed, 32)
|
||||
if len(pw) < 2 {
|
||||
continue
|
||||
}
|
||||
runes := []byte(strings.ToUpper(pw))
|
||||
for i := 1; i < len(runes); i++ {
|
||||
// Detect new segment (original char was uppercase and it's not the first char)
|
||||
origUpper := pw[i] >= 'A' && pw[i] <= 'Z'
|
||||
if origUpper && i > 0 {
|
||||
continue
|
||||
}
|
||||
prev := runes[i-1]
|
||||
cur := runes[i]
|
||||
cont, ok := ppContinuation[prev]
|
||||
if !ok {
|
||||
t.Errorf("no continuation map for %q (pw=%q)", prev, pw)
|
||||
continue
|
||||
}
|
||||
if !strings.ContainsRune(cont, rune(cur)) {
|
||||
t.Errorf("invalid continuation %q -> %q in %q (seed %d)", prev, cur, pw, seed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user