diff --git a/captcha/prng.go b/captcha/prng.go new file mode 100644 index 0000000..f6748a3 --- /dev/null +++ b/captcha/prng.go @@ -0,0 +1,36 @@ +package captcha + +import ( + "fmt" + "strings" +) + +const prngMask = 0xFFFFFFFF + +// prng generates a deterministic lowercase-hex string of length chars from +// seed, matching the Cap.js widget PRNG exactly: FNV-1a seeds a 32-bit state, +// then xorshift32 produces the hex output. Must stay byte-for-byte compatible +// with the widget or solutions will never verify. +func prng(seed string, length int) string { + state := fnv1a(seed) + var b strings.Builder + for b.Len() < length { + state ^= (state << 13) & prngMask + state ^= state >> 17 + state ^= (state << 5) & prngMask + state &= prngMask + fmt.Fprintf(&b, "%08x", state) + } + return b.String()[:length] +} + +// fnv1a returns the 32-bit FNV-1a hash of s. Iterates over runes (code points) +// to match the JS/Python reference (which use charCodeAt / ord). +func fnv1a(s string) uint32 { + var h uint32 = 2166136261 + for _, ch := range s { + h ^= uint32(ch) + h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) & prngMask + } + return h +} diff --git a/captcha/prng_test.go b/captcha/prng_test.go new file mode 100644 index 0000000..175d030 --- /dev/null +++ b/captcha/prng_test.go @@ -0,0 +1,37 @@ +package captcha + +import "testing" + +func TestPrngKnownVectors(t *testing.T) { + cases := []struct { + seed string + length int + want string + }{ + {"hello", 8, "eb492c6e"}, + {"hello", 16, "eb492c6e1655ea8c"}, + {"test", 32, "9c7ca3730a4a283aa6e4bc1c1d83b14f"}, + {"a", 8, "441aaeb8"}, + {"z", 8, "da40eb31"}, + } + for _, c := range cases { + if got := prng(c.seed, c.length); got != c.want { + t.Errorf("prng(%q,%d)=%q want %q", c.seed, c.length, got, c.want) + } + } +} + +func TestPrngLengthAndDeterminism(t *testing.T) { + for _, n := range []int{1, 8, 16, 32, 64} { + if got := prng("x", n); len(got) != n { + t.Errorf("len(prng(\"x\",%d))=%d want %d", n, len(got), n) + } + } + if prng("seed1", 16) == prng("seed2", 16) { + t.Error("different seeds produced identical output") + } + a, b := prng("test", 16), prng("test", 16) + if a != b { + t.Error("prng not deterministic") + } +}