package main import ( "encoding/json" "os" "testing" ) type scryptVector struct { Password string `json:"password"` Salt string `json:"salt"` Encoded string `json:"encoded"` } func loadScryptVectors(t *testing.T) []scryptVector { t.Helper() raw, err := os.ReadFile("testdata/scrypt_vectors.json") if err != nil { t.Fatalf("read vectors: %v", err) } var vectors []scryptVector if err := json.Unmarshal(raw, &vectors); err != nil { t.Fatalf("parse vectors: %v", err) } if len(vectors) == 0 { t.Fatal("no scrypt vectors") } return vectors } func TestHashSecretWithSaltMatchesVectors(t *testing.T) { for i, v := range loadScryptVectors(t) { got := HashSecretWithSalt(v.Password, v.Salt) if got != v.Encoded { t.Errorf("vector %d: HashSecretWithSalt(%q, %q)\n got %s\nwant %s", i, v.Password, v.Salt, got, v.Encoded) } } } func TestVerifySecretAgainstVectors(t *testing.T) { for i, v := range loadScryptVectors(t) { if !VerifySecret(v.Password, v.Encoded) { t.Errorf("vector %d: VerifySecret(correct) = false, want true", i) } if VerifySecret(v.Password+"wrong", v.Encoded) { t.Errorf("vector %d: VerifySecret(wrong password) = true, want false", i) } } } func TestVerifySecretMalformed(t *testing.T) { cases := []string{ "", "scrypt", "scrypt:", "scrypt::", "scrypt:salt:", "scrypt::abcd", "bcrypt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:abcd", "scrypt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:not-hex!!", "scrypt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:abcd", // wrong length "scrypt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:zz", } for _, c := range cases { if VerifySecret("password", c) { t.Errorf("VerifySecret(%q) = true, want false", c) } } } func TestHashSecretRoundTrip(t *testing.T) { encoded := HashSecret("s3cret") parts := len(encoded) _ = parts if !VerifySecret("s3cret", encoded) { t.Errorf("VerifySecret with fresh HashSecret = false") } if VerifySecret("other", encoded) { t.Errorf("VerifySecret wrong secret = true") } // Format: scrypt:<32 hex salt chars>:<128 hex hash chars> var kind, salt, hash string n, err := parseEncoded(encoded, &kind, &salt, &hash) if err != nil || n != 3 { t.Fatalf("encoded format: n=%d err=%v encoded=%q", n, err, encoded) } if kind != "scrypt" || len(salt) != 32 || len(hash) != 128 { t.Errorf("encoded parts: kind=%q salt len=%d hash len=%d", kind, len(salt), len(hash)) } // Two hashes of the same secret differ (random salt). if HashSecret("s3cret") == encoded { t.Errorf("HashSecret produced identical output twice; salt is not random") } } func parseEncoded(encoded string, kind, salt, hash *string) (int, error) { parts := splitColon(encoded) if len(parts) > 0 { *kind = parts[0] } if len(parts) > 1 { *salt = parts[1] } if len(parts) > 2 { *hash = parts[2] } return len(parts), nil } func splitColon(s string) []string { var out []string start := 0 for i := 0; i < len(s); i++ { if s[i] == ':' { out = append(out, s[start:i]) start = i + 1 } } return append(out, s[start:]) }