Pure-stdlib Go implementation of the whole control plane: same nexusgate.json schema (unknown fields preserved verbatim, explicit null/false tri-states modeled), same HTTP API and error strings, embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt auth, X25519 Reality keys, panel-hosted keyStore and heartbeat reconciliation included. Version stays 0.6.8. Verified: Go tests 33/33 (scrypt + Reality vectors generated from the Node implementation, subscription goldens byte-identical), Node suite 52/52 untouched, differential E2E against the Node panel 50/50 including two-way DB swap. See panel-go/PARITY.md and panel-go/INSTALL-GO.md.
121 lines
3.0 KiB
Go
121 lines
3.0 KiB
Go
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:])
|
|
}
|