From 52efd57dd956b39b77f836ee04adf3097c10bb92 Mon Sep 17 00:00:00 2001 From: ek0ms savi0r <4+ek0mssavi0r@noreply.git.churchofmalware.org> Date: Wed, 9 Sep 2026 05:11:11 +0000 Subject: [PATCH] Upload files to "internal/crypto" --- internal/crypto/crypto.go | 147 +++++++++++++++++++++++++++++++++ internal/crypto/crypto_test.go | 115 ++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 internal/crypto/crypto.go create mode 100644 internal/crypto/crypto_test.go diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..246e7f2 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,147 @@ +// Package crypto provides cryptographic primitives for the C2 framework. +package crypto + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "io" + "time" + + "golang.org/x/crypto/chacha20poly1305" +) + +// KeyPair holds an Ed25519 signing keypair. +type KeyPair struct { + Private ed25519.PrivateKey + Public ed25519.PublicKey +} + +// GenerateKeyPair creates a new Ed25519 signing keypair. +func GenerateKeyPair() (*KeyPair, error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate keypair: %w", err) + } + return &KeyPair{Private: priv, Public: pub}, nil +} + +// Sign signs data with the private key, including a timestamp and nonce to prevent replay. +func (kp *KeyPair) Sign(data []byte) (signature []byte, nonce string, ts int64, err error) { + nonceBytes := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, nonceBytes); err != nil { + return nil, "", 0, err + } + nonce = base64.RawStdEncoding.EncodeToString(nonceBytes) + ts = time.Now().Unix() + msg := append(data, []byte(fmt.Sprintf("%d%s", ts, nonce))...) + sig := ed25519.Sign(kp.Private, msg) + return sig, nonce, ts, nil +} + +// Verify checks an Ed25519 signature with optional replay protection. +// If nonce is empty, only timestamp window is checked. +func Verify(publicKey ed25519.PublicKey, data, sig []byte, nonce string, ts int64, seenNonces map[string]bool) error { + now := time.Now().Unix() + if abs(now-ts) > 300 { + return errors.New("signature timestamp out of window") + } + if nonce != "" { + if len(nonce) < 8 { + return errors.New("nonce too short") + } + if seenNonces != nil { + if seenNonces[nonce] { + return errors.New("nonce replay detected") + } + seenNonces[nonce] = true + } + } + msg := append(data, []byte(fmt.Sprintf("%d%s", ts, nonce))...) + if !ed25519.Verify(publicKey, msg, sig) { + return errors.New("invalid signature") + } + return nil +} + +// EncryptWithAEAD encrypts plaintext using XChaCha20-Poly1305. +// Returns nonce || ciphertext. +func EncryptWithAEAD(key []byte, plaintext []byte) ([]byte, error) { + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return nil, err + } + nonce := make([]byte, aead.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + return aead.Seal(nonce, nonce, plaintext, nil), nil +} + +// DecryptWithAEAD decrypts using XChaCha20-Poly1305. +// Expects nonce || ciphertext. +func DecryptWithAEAD(key []byte, data []byte) ([]byte, error) { + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return nil, err + } + nonceSize := aead.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext too short") + } + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + return aead.Open(nil, nonce, ciphertext, nil) +} + +// DeriveSessionKey derives a 32-byte session key from a shared secret. +func DeriveSessionKey(secret []byte, salt []byte) []byte { + h := sha256.Sum256(append(secret, salt...)) + return h[:] +} + +// MarshalPublicKey PEM-encodes an Ed25519 public key. +func MarshalPublicKey(pub ed25519.PublicKey) ([]byte, error) { + der, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: der, + }), nil +} + +// UnmarshalPublicKey decodes a PEM-encoded Ed25519 public key. +func UnmarshalPublicKey(pemData []byte) (ed25519.PublicKey, error) { + block, _ := pem.Decode(pemData) + if block == nil { + return nil, errors.New("failed to decode PEM") + } + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + edPub, ok := pub.(ed25519.PublicKey) + if !ok { + return nil, errors.New("not an Ed25519 public key") + } + return edPub, nil +} + +// HexDecode decodes a hex string into bytes. +func HexDecode(s string) ([]byte, error) { + return hex.DecodeString(s) +} + +func abs(x int64) int64 { + if x < 0 { + return -x + } + return x +} diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go new file mode 100644 index 0000000..fbe7c1e --- /dev/null +++ b/internal/crypto/crypto_test.go @@ -0,0 +1,115 @@ +package crypto + +import ( + "bytes" + "crypto/ed25519" + "testing" +) + +func TestAEADRoundTrip(t *testing.T) { + key := make([]byte, 32) + for i := range key { + key[i] = byte(i) + } + msg := []byte("ranger c3 encrypted channel") + + ct, err := EncryptWithAEAD(key, msg) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + if bytes.Equal(ct, msg) { + t.Fatal("ciphertext equals plaintext") + } + pt, err := DecryptWithAEAD(key, ct) + if err != nil { + t.Fatalf("decrypt: %v", err) + } + if !bytes.Equal(pt, msg) { + t.Fatalf("round-trip mismatch: %q", pt) + } +} + +func TestAEADTamperDetected(t *testing.T) { + key := make([]byte, 32) + msg := []byte("integrity check") + + ct, err := EncryptWithAEAD(key, msg) + if err != nil { + t.Fatal(err) + } + ct[len(ct)-1] ^= 0xff + if _, err := DecryptWithAEAD(key, ct); err == nil { + t.Fatal("tampered ciphertext accepted") + } + + badKey := make([]byte, 32) + badKey[0] = 0x42 + ct2, _ := EncryptWithAEAD(key, msg) + if _, err := DecryptWithAEAD(badKey, ct2); err == nil { + t.Fatal("wrong key accepted") + } +} + +func TestKeyPairSignVerify(t *testing.T) { + kp, err := GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + + data := []byte("mesh heartbeat payload") + sig, nonce, ts, err := kp.Sign(data) + if err != nil { + t.Fatal(err) + } + + seen := map[string]bool{} + if err := Verify(kp.Public, data, sig, nonce, ts, seen); err != nil { + t.Fatalf("verify: %v", err) + } + + // Replay with same nonce must fail. + if err := Verify(kp.Public, data, sig, nonce, ts, seen); err == nil { + t.Fatal("replay with same nonce accepted") + } + + // Tampered data must fail. + if err := Verify(kp.Public, []byte("tampered"), sig, nonce, ts, seen); err == nil { + t.Fatal("tampered data accepted") + } +} + +func TestDeriveSessionKeyDeterministic(t *testing.T) { + secret := []byte("shared-secret") + salt := []byte("dns-tunnel") + + a := DeriveSessionKey(secret, salt) + b := DeriveSessionKey(secret, salt) + if !bytes.Equal(a, b) { + t.Fatal("derivation not deterministic") + } + if len(a) != 32 { + t.Fatalf("derived key length %d", len(a)) + } + c := DeriveSessionKey(secret, []byte("other")) + if bytes.Equal(a, c) { + t.Fatal("different salt produced same key") + } +} + +func TestPublicKeyMarshal(t *testing.T) { + kp, err := GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + pemBytes, err := MarshalPublicKey(kp.Public) + if err != nil { + t.Fatal(err) + } + pub, err := UnmarshalPublicKey(pemBytes) + if err != nil { + t.Fatal(err) + } + if !pub.Equal(ed25519.PublicKey(kp.Public)) { + t.Fatal("public key round-trip mismatch") + } +}