feat: Go port of the panel in panel-go/ — drop-in replacement for server.js
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.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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:])
|
||||
}
|
||||
Reference in New Issue
Block a user