Files
aaaamoooo 3951134940
CI / test (push) Canceled after 0s
CI / anytls-smoke (push) Canceled after 0s
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.
2026-10-20 08:21:54 +00:00

166 lines
4.6 KiB
Go

package main
// Self-contained scrypt (RFC 7914) with Node's crypto.scryptSync defaults:
// N=16384, r=8, p=1, keyLen=64. The stdlib has no scrypt, so PBKDF2-HMAC-SHA256,
// the Salsa20/8 core, BlockMix and ROMix are implemented here from the spec.
//
// Important Node quirk: crypto.scryptSync(secret, salt, 64) takes the salt as
// a string and uses its raw UTF-8 bytes — the hex-looking salt produced by
// HashSecret is NOT hex-decoded before use.
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"math/bits"
)
// ScryptHash derives 64 bytes from secret and salt with N=16384, r=8, p=1.
func ScryptHash(secret, salt string) []byte {
const (
n = 16384
r = 8
p = 1
keyLen = 64
)
password := []byte(secret)
saltBytes := []byte(salt)
b := pbkdf2SHA256(password, saltBytes, 1, p*128*r)
// Work in uint32 words for ROMix/BlockMix.
words := make([]uint32, len(b)/4)
for i := range words {
words[i] = binary.LittleEndian.Uint32(b[i*4:])
}
blockWords := 32 * r
for i := 0; i < p; i++ {
romix(words[i*blockWords:(i+1)*blockWords], n, r)
}
for i := range words {
binary.LittleEndian.PutUint32(b[i*4:], words[i])
}
return pbkdf2SHA256(password, b, 1, keyLen)
}
func pbkdf2SHA256(password, salt []byte, iter, keyLen int) []byte {
hashLen := sha256.Size
numBlocks := (keyLen + hashLen - 1) / hashLen
dk := make([]byte, 0, numBlocks*hashLen)
var counter [4]byte
for block := 1; block <= numBlocks; block++ {
mac := hmac.New(sha256.New, password)
mac.Write(salt)
binary.BigEndian.PutUint32(counter[:], uint32(block))
mac.Write(counter[:])
u := mac.Sum(nil)
t := make([]byte, len(u))
copy(t, u)
for i := 1; i < iter; i++ {
mac = hmac.New(sha256.New, password)
mac.Write(u)
u = mac.Sum(nil)
for j := range t {
t[j] ^= u[j]
}
}
dk = append(dk, t...)
}
return dk[:keyLen]
}
// salsa208Core applies the Salsa20/8 core function in place to 16 words.
func salsa208Core(x *[16]uint32) {
orig := *x
for i := 0; i < 8; i += 2 {
// Column round.
x[4] ^= bits.RotateLeft32(x[0]+x[12], 7)
x[8] ^= bits.RotateLeft32(x[4]+x[0], 9)
x[12] ^= bits.RotateLeft32(x[8]+x[4], 13)
x[0] ^= bits.RotateLeft32(x[12]+x[8], 18)
x[9] ^= bits.RotateLeft32(x[5]+x[1], 7)
x[13] ^= bits.RotateLeft32(x[9]+x[5], 9)
x[1] ^= bits.RotateLeft32(x[13]+x[9], 13)
x[5] ^= bits.RotateLeft32(x[1]+x[13], 18)
x[14] ^= bits.RotateLeft32(x[10]+x[6], 7)
x[2] ^= bits.RotateLeft32(x[14]+x[10], 9)
x[6] ^= bits.RotateLeft32(x[2]+x[14], 13)
x[10] ^= bits.RotateLeft32(x[6]+x[2], 18)
x[3] ^= bits.RotateLeft32(x[15]+x[11], 7)
x[7] ^= bits.RotateLeft32(x[3]+x[15], 9)
x[11] ^= bits.RotateLeft32(x[7]+x[3], 13)
x[15] ^= bits.RotateLeft32(x[11]+x[7], 18)
// Row round.
x[1] ^= bits.RotateLeft32(x[0]+x[3], 7)
x[2] ^= bits.RotateLeft32(x[1]+x[0], 9)
x[3] ^= bits.RotateLeft32(x[2]+x[1], 13)
x[0] ^= bits.RotateLeft32(x[3]+x[2], 18)
x[6] ^= bits.RotateLeft32(x[5]+x[4], 7)
x[7] ^= bits.RotateLeft32(x[6]+x[5], 9)
x[4] ^= bits.RotateLeft32(x[7]+x[6], 13)
x[5] ^= bits.RotateLeft32(x[4]+x[7], 18)
x[11] ^= bits.RotateLeft32(x[10]+x[9], 7)
x[8] ^= bits.RotateLeft32(x[11]+x[10], 9)
x[9] ^= bits.RotateLeft32(x[8]+x[11], 13)
x[10] ^= bits.RotateLeft32(x[9]+x[8], 18)
x[12] ^= bits.RotateLeft32(x[15]+x[14], 7)
x[13] ^= bits.RotateLeft32(x[12]+x[15], 9)
x[14] ^= bits.RotateLeft32(x[13]+x[12], 13)
x[15] ^= bits.RotateLeft32(x[14]+x[13], 18)
}
for i := range x {
x[i] += orig[i]
}
}
// blockMix performs scryptBlockMix over 2r 64-byte blocks held as uint32
// words, writing the result into out (which must not alias in). tmp is the
// running 64-byte block.
func blockMix(in, out []uint32, r int, tmp *[16]uint32) {
copy(tmp[:], in[(2*r-1)*16:(2*r)*16])
for i := 0; i < 2*r; i++ {
block := in[i*16 : (i+1)*16]
for j := 0; j < 16; j++ {
tmp[j] ^= block[j]
}
salsa208Core(tmp)
// Y_i goes to the even blocks first, then the odd blocks.
dest := i / 2
if i%2 == 1 {
dest = dest + r
}
copy(out[dest*16:(dest+1)*16], tmp[:])
}
}
// romix is scryptROMix over one 128*r-byte chunk held as uint32 words.
func romix(b []uint32, n, r int) {
blockWords := 32 * r
v := make([]uint32, n*blockWords)
x := make([]uint32, blockWords)
y := make([]uint32, blockWords)
copy(x, b)
for i := 0; i < n; i++ {
copy(v[i*blockWords:], x)
blockMix(x, y, r, &[16]uint32{})
copy(x, y)
}
for i := 0; i < n; i++ {
// Integerify: first 64 bits of the last 64-byte block, little-endian.
pos := (2*r - 1) * 16
j := int((uint64(x[pos]) | uint64(x[pos+1])<<32) % uint64(n))
vj := v[j*blockWords:]
for k := range x {
x[k] ^= vj[k]
}
blockMix(x, y, r, &[16]uint32{})
copy(x, y)
}
copy(b, x)
}