Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes

This commit is contained in:
Shtorm
2026-06-04 01:47:50 +03:00
parent 9b3da79c32
commit 195a33379d
164 changed files with 16665 additions and 1332 deletions

View File

@@ -0,0 +1,150 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"golang.org/x/crypto/chacha20poly1305"
)
type AEADConn struct {
net.Conn
aead cipher.AEAD
readBuf bytes.Buffer
nonceSize int
}
func (cc *AEADConn) CloseWrite() error {
if cc == nil || cc.Conn == nil {
return nil
}
if cw, ok := cc.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (cc *AEADConn) CloseRead() error {
if cc == nil || cc.Conn == nil {
return nil
}
if cr, ok := cc.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewAEADConn(c net.Conn, key string, method string) (*AEADConn, error) {
if method == "none" {
return &AEADConn{Conn: c, aead: nil}, nil
}
h := sha256.New()
h.Write([]byte(key))
keyBytes := h.Sum(nil)
var aead cipher.AEAD
var err error
switch method {
case "aes-128-gcm":
block, _ := aes.NewCipher(keyBytes[:16])
aead, err = cipher.NewGCM(block)
case "chacha20-poly1305":
aead, err = chacha20poly1305.New(keyBytes)
default:
return nil, fmt.Errorf("unsupported cipher: %s", method)
}
if err != nil {
return nil, err
}
return &AEADConn{
Conn: c,
aead: aead,
nonceSize: aead.NonceSize(),
}, nil
}
func (cc *AEADConn) Write(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Write(p)
}
maxPayload := 65535 - cc.nonceSize - cc.aead.Overhead()
totalWritten := 0
var frameBuf bytes.Buffer
header := make([]byte, 2)
nonce := make([]byte, cc.nonceSize)
for len(p) > 0 {
chunkSize := len(p)
if chunkSize > maxPayload {
chunkSize = maxPayload
}
chunk := p[:chunkSize]
p = p[chunkSize:]
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return totalWritten, err
}
ciphertext := cc.aead.Seal(nil, nonce, chunk, nil)
frameLen := len(nonce) + len(ciphertext)
binary.BigEndian.PutUint16(header, uint16(frameLen))
frameBuf.Reset()
frameBuf.Write(header)
frameBuf.Write(nonce)
frameBuf.Write(ciphertext)
if _, err := cc.Conn.Write(frameBuf.Bytes()); err != nil {
return totalWritten, err
}
totalWritten += chunkSize
}
return totalWritten, nil
}
func (cc *AEADConn) Read(p []byte) (int, error) {
if cc.aead == nil {
return cc.Conn.Read(p)
}
if cc.readBuf.Len() > 0 {
return cc.readBuf.Read(p)
}
header := make([]byte, 2)
if _, err := io.ReadFull(cc.Conn, header); err != nil {
return 0, err
}
frameLen := int(binary.BigEndian.Uint16(header))
body := make([]byte, frameLen)
if _, err := io.ReadFull(cc.Conn, body); err != nil {
return 0, err
}
if len(body) < cc.nonceSize {
return 0, errors.New("frame too short")
}
nonce := body[:cc.nonceSize]
ciphertext := body[cc.nonceSize:]
plaintext, err := cc.aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return 0, errors.New("decryption failed")
}
cc.readBuf.Write(plaintext)
return cc.readBuf.Read(p)
}

View File

@@ -0,0 +1,116 @@
package crypto
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"filippo.io/edwards25519"
)
// KeyPair holds the scalar private key and point public key
type KeyPair struct {
Private *edwards25519.Scalar
Public *edwards25519.Point
}
// GenerateMasterKey generates a random master private key (scalar) and its public key (point)
func GenerateMasterKey() (*KeyPair, error) {
// 1. Generate random scalar x (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return nil, err
}
x, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return nil, err
}
// 2. Calculate Public Key P = x * G
P := new(edwards25519.Point).ScalarBaseMult(x)
return &KeyPair{Private: x, Public: P}, nil
}
// SplitPrivateKey takes a master private key x and returns a new random split key (r, k)
// such that x = r + k (mod L).
// Returns hex encoded string of r || k (64 bytes)
func SplitPrivateKey(x *edwards25519.Scalar) (string, error) {
// 1. Generate random r (32 bytes)
var seed [64]byte
if _, err := rand.Read(seed[:]); err != nil {
return "", err
}
r, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
if err != nil {
return "", err
}
// 2. Calculate k = x - r (mod L)
k := new(edwards25519.Scalar).Subtract(x, r)
// 3. Encode r and k
rBytes := r.Bytes()
kBytes := k.Bytes()
full := make([]byte, 64)
copy(full[:32], rBytes)
copy(full[32:], kBytes)
return hex.EncodeToString(full), nil
}
// RecoverPublicKey takes a split private key (r, k) or a master private key (x)
// and returns the public key P.
// Input can be:
// - 32 bytes hex (Master Scalar x)
// - 64 bytes hex (Split Key r || k)
func RecoverPublicKey(keyHex string) (*edwards25519.Point, error) {
keyBytes, err := hex.DecodeString(keyHex)
if err != nil {
return nil, fmt.Errorf("invalid hex: %w", err)
}
if len(keyBytes) == 32 {
// Master Key x
x, err := edwards25519.NewScalar().SetCanonicalBytes(keyBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar: %w", err)
}
return new(edwards25519.Point).ScalarBaseMult(x), nil
}
if len(keyBytes) == 64 {
// Split Key r || k
rBytes := keyBytes[:32]
kBytes := keyBytes[32:]
r, err := edwards25519.NewScalar().SetCanonicalBytes(rBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar r: %w", err)
}
k, err := edwards25519.NewScalar().SetCanonicalBytes(kBytes)
if err != nil {
return nil, fmt.Errorf("invalid scalar k: %w", err)
}
// sum = r + k
sum := new(edwards25519.Scalar).Add(r, k)
// P = sum * G
return new(edwards25519.Point).ScalarBaseMult(sum), nil
}
return nil, errors.New("invalid key length: must be 32 bytes (Master) or 64 bytes (Split)")
}
// EncodePoint returns the hex string of the compressed point
func EncodePoint(p *edwards25519.Point) string {
return hex.EncodeToString(p.Bytes())
}
// EncodeScalar returns the hex string of the scalar
func EncodeScalar(s *edwards25519.Scalar) string {
return hex.EncodeToString(s.Bytes())
}

View File

@@ -0,0 +1,452 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"sync/atomic"
"golang.org/x/crypto/chacha20poly1305"
)
// KeyUpdateAfterBytes controls automatic key rotation based on plaintext bytes.
// It is a package var (not config) to enable targeted tests with smaller thresholds.
var KeyUpdateAfterBytes int64 = 32 << 20 // 32 MiB
const (
recordHeaderSize = 12 // epoch(uint32) + seq(uint64) - also used as nonce+AAD.
maxFrameBodySize = 65535
)
type recordKeys struct {
baseSend []byte
baseRecv []byte
}
// RecordConn is a framed AEAD net.Conn with:
// - deterministic per-record nonce (epoch+seq)
// - per-direction key rotation (epoch), driven by plaintext byte counters
// - replay/out-of-order protection within the connection (strict seq check)
//
// Wire format per record:
// - uint16 bodyLen
// - header[12] = epoch(uint32 BE) || seq(uint64 BE) (plaintext)
// - ciphertext = AEAD(header as nonce, plaintext, header as AAD)
type RecordConn struct {
net.Conn
method string
writeMu sync.Mutex
readMu sync.Mutex
keys recordKeys
sendAEAD cipher.AEAD
sendAEADEpoch uint32
recvAEAD cipher.AEAD
recvAEADEpoch uint32
// Send direction state.
sendEpoch uint32
sendSeq uint64
sendBytes int64
sendEpochUpdates uint32
// Receive direction state.
recvEpoch uint32
recvSeq uint64
recvInitialized bool
readBuf bytes.Buffer
// writeFrame is a reusable buffer for [len||header||ciphertext] on the wire.
// Guarded by writeMu.
writeFrame []byte
}
func (c *RecordConn) CloseWrite() error {
if c == nil {
return nil
}
if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (c *RecordConn) CloseRead() error {
if c == nil {
return nil
}
if cr, ok := c.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewRecordConn(conn net.Conn, method string, baseSend, baseRecv []byte) (*RecordConn, error) {
if conn == nil {
return nil, fmt.Errorf("nil conn")
}
method = normalizeAEADMethod(method)
if method != "none" {
if err := validateBaseKey(baseSend); err != nil {
return nil, fmt.Errorf("invalid send base key: %w", err)
}
if err := validateBaseKey(baseRecv); err != nil {
return nil, fmt.Errorf("invalid recv base key: %w", err)
}
}
rc := &RecordConn{Conn: conn, method: method}
rc.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)}
if err := rc.resetTrafficState(); err != nil {
return nil, err
}
return rc, nil
}
func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error {
if c == nil {
return fmt.Errorf("nil conn")
}
if c.method != "none" {
if err := validateBaseKey(baseSend); err != nil {
return fmt.Errorf("invalid send base key: %w", err)
}
if err := validateBaseKey(baseRecv); err != nil {
return fmt.Errorf("invalid recv base key: %w", err)
}
}
c.writeMu.Lock()
c.readMu.Lock()
defer c.readMu.Unlock()
defer c.writeMu.Unlock()
c.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)}
if err := c.resetTrafficState(); err != nil {
return err
}
c.readBuf.Reset()
c.sendAEAD = nil
c.recvAEAD = nil
c.sendAEADEpoch = 0
c.recvAEADEpoch = 0
return nil
}
func (c *RecordConn) resetTrafficState() error {
sendEpoch, sendSeq, err := randomRecordCounters()
if err != nil {
return fmt.Errorf("initialize record counters: %w", err)
}
c.sendEpoch = sendEpoch
c.sendSeq = sendSeq
c.sendBytes = 0
c.sendEpochUpdates = 0
c.recvEpoch = 0
c.recvSeq = 0
c.recvInitialized = false
return nil
}
func normalizeAEADMethod(method string) string {
switch method {
case "", "chacha20-poly1305":
return "chacha20-poly1305"
case "aes-128-gcm", "none":
return method
default:
return method
}
}
func validateBaseKey(b []byte) error {
if len(b) < 32 {
return fmt.Errorf("need at least 32 bytes, got %d", len(b))
}
return nil
}
func cloneBytes(b []byte) []byte {
if len(b) == 0 {
return nil
}
return append([]byte(nil), b...)
}
func randomRecordCounters() (uint32, uint64, error) {
epoch, err := randomNonZeroUint32()
if err != nil {
return 0, 0, err
}
seq, err := randomNonZeroUint64()
if err != nil {
return 0, 0, err
}
return epoch, seq, nil
}
func randomNonZeroUint32() (uint32, error) {
var b [4]byte
for {
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
return 0, err
}
v := binary.BigEndian.Uint32(b[:])
if v != 0 && v != ^uint32(0) {
return v, nil
}
}
}
func randomNonZeroUint64() (uint64, error) {
var b [8]byte
for {
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
return 0, err
}
v := binary.BigEndian.Uint64(b[:])
if v != 0 && v != ^uint64(0) {
return v, nil
}
}
}
func (c *RecordConn) newAEADFor(base []byte, epoch uint32) (cipher.AEAD, error) {
if c.method == "none" {
return nil, nil
}
key := deriveEpochKey(base, epoch, c.method)
switch c.method {
case "aes-128-gcm":
block, err := aes.NewCipher(key[:16])
if err != nil {
return nil, err
}
a, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if a.NonceSize() != recordHeaderSize {
return nil, fmt.Errorf("unexpected gcm nonce size: %d", a.NonceSize())
}
return a, nil
case "chacha20-poly1305":
a, err := chacha20poly1305.New(key[:32])
if err != nil {
return nil, err
}
if a.NonceSize() != recordHeaderSize {
return nil, fmt.Errorf("unexpected chacha nonce size: %d", a.NonceSize())
}
return a, nil
default:
return nil, fmt.Errorf("unsupported cipher: %s", c.method)
}
}
func deriveEpochKey(base []byte, epoch uint32, method string) []byte {
var b [4]byte
binary.BigEndian.PutUint32(b[:], epoch)
mac := hmac.New(sha256.New, base)
_, _ = mac.Write([]byte("sudoku-record:"))
_, _ = mac.Write([]byte(method))
_, _ = mac.Write(b[:])
return mac.Sum(nil)
}
func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) error {
ku := atomic.LoadInt64(&KeyUpdateAfterBytes)
if ku <= 0 || c.method == "none" {
return nil
}
c.sendBytes += int64(addedPlain)
threshold := ku * int64(c.sendEpochUpdates+1)
if c.sendBytes < threshold {
return nil
}
c.sendEpoch++
c.sendEpochUpdates++
nextSeq, err := randomNonZeroUint64()
if err != nil {
return fmt.Errorf("rotate record seq: %w", err)
}
c.sendSeq = nextSeq
return nil
}
func (c *RecordConn) validateRecvPosition(epoch uint32, seq uint64) error {
if !c.recvInitialized {
return nil
}
if epoch < c.recvEpoch {
return fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch)
}
if epoch == c.recvEpoch && seq != c.recvSeq {
return fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq)
}
if epoch > c.recvEpoch {
const maxJump = 8
if epoch-c.recvEpoch > maxJump {
return fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump)
}
}
return nil
}
func (c *RecordConn) markRecvPosition(epoch uint32, seq uint64) {
c.recvEpoch = epoch
c.recvSeq = seq + 1
c.recvInitialized = true
}
func (c *RecordConn) Write(p []byte) (int, error) {
if c == nil || c.Conn == nil {
return 0, net.ErrClosed
}
if c.method == "none" {
return c.Conn.Write(p)
}
c.writeMu.Lock()
defer c.writeMu.Unlock()
total := 0
for len(p) > 0 {
if c.sendAEAD == nil || c.sendAEADEpoch != c.sendEpoch {
a, err := c.newAEADFor(c.keys.baseSend, c.sendEpoch)
if err != nil {
return total, err
}
c.sendAEAD = a
c.sendAEADEpoch = c.sendEpoch
}
aead := c.sendAEAD
maxPlain := maxFrameBodySize - recordHeaderSize - aead.Overhead()
if maxPlain <= 0 {
return total, errors.New("frame size too small")
}
n := len(p)
if n > maxPlain {
n = maxPlain
}
chunk := p[:n]
p = p[n:]
var header [recordHeaderSize]byte
binary.BigEndian.PutUint32(header[:4], c.sendEpoch)
binary.BigEndian.PutUint64(header[4:], c.sendSeq)
c.sendSeq++
cipherLen := n + aead.Overhead()
bodyLen := recordHeaderSize + cipherLen
frameLen := 2 + bodyLen
if bodyLen > maxFrameBodySize {
return total, errors.New("frame too large")
}
if cap(c.writeFrame) < frameLen {
c.writeFrame = make([]byte, frameLen)
}
frame := c.writeFrame[:frameLen]
binary.BigEndian.PutUint16(frame[:2], uint16(bodyLen))
copy(frame[2:2+recordHeaderSize], header[:])
dst := frame[2+recordHeaderSize : 2+recordHeaderSize : frameLen]
_ = aead.Seal(dst[:0], header[:], chunk, header[:])
if err := writeFull(c.Conn, frame); err != nil {
return total, err
}
total += n
if err := c.maybeBumpSendEpochLocked(n); err != nil {
return total, err
}
}
return total, nil
}
func (c *RecordConn) Read(p []byte) (int, error) {
if c == nil || c.Conn == nil {
return 0, net.ErrClosed
}
if c.method == "none" {
return c.Conn.Read(p)
}
c.readMu.Lock()
defer c.readMu.Unlock()
if c.readBuf.Len() > 0 {
return c.readBuf.Read(p)
}
var lenBuf [2]byte
if _, err := io.ReadFull(c.Conn, lenBuf[:]); err != nil {
return 0, err
}
bodyLen := int(binary.BigEndian.Uint16(lenBuf[:]))
if bodyLen < recordHeaderSize {
return 0, errors.New("frame too short")
}
if bodyLen > maxFrameBodySize {
return 0, errors.New("frame too large")
}
body := make([]byte, bodyLen)
if _, err := io.ReadFull(c.Conn, body); err != nil {
return 0, err
}
header := body[:recordHeaderSize]
ciphertext := body[recordHeaderSize:]
epoch := binary.BigEndian.Uint32(header[:4])
seq := binary.BigEndian.Uint64(header[4:])
if err := c.validateRecvPosition(epoch, seq); err != nil {
return 0, err
}
if c.recvAEAD == nil || c.recvAEADEpoch != epoch {
a, err := c.newAEADFor(c.keys.baseRecv, epoch)
if err != nil {
return 0, err
}
c.recvAEAD = a
c.recvAEADEpoch = epoch
}
aead := c.recvAEAD
plaintext, err := aead.Open(nil, header, ciphertext, header)
if err != nil {
return 0, fmt.Errorf("decryption failed: epoch=%d seq=%d: %w", epoch, seq, err)
}
c.markRecvPosition(epoch, seq)
c.readBuf.Write(plaintext)
return c.readBuf.Read(p)
}
func writeFull(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
b = b[n:]
}
return nil
}