Upload files to "internal/crypto"

This commit is contained in:
ek0ms savi0r
2026-09-09 05:11:11 +00:00
parent 9f67369b47
commit 52efd57dd9
2 changed files with 262 additions and 0 deletions
+147
View File
@@ -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
}
+115
View File
@@ -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")
}
}