summaryrefslogtreecommitdiffstats
path: root/pkg/crypto/crypto.go
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2025-11-23 10:07:55 +0000
committerClaude <noreply@anthropic.com>2025-11-23 10:07:55 +0000
commit69a75acea56bbff39fa1c81d4813ebcb4a617f06 (patch)
treeee1239320191bae0df39bdbe0d058069c27a0107 /pkg/crypto/crypto.go
parent9b4caf3e5ffad219ea55ea6db7ad19940c5f83d9 (diff)
downloadnoshitalk-69a75acea56bbff39fa1c81d4813ebcb4a617f06.tar.gz
noshitalk-69a75acea56bbff39fa1c81d4813ebcb4a617f06.tar.xz
noshitalk-69a75acea56bbff39fa1c81d4813ebcb4a617f06.zip
Refactor: modular architecture with shared packages
Major code reorganization to eliminate duplication and improve maintainability: - Created pkg/crypto: shared cryptographic functions (padding, AES-GCM, ECDH, HMAC auth) - Created pkg/protocol: unified message structures and JSON serialization - Created pkg/identity: persistent identity management (load/save/generate) - Created pkg/tor: Tor SOCKS5 proxy connection utilities - Refactored server to use shared packages (cmd/server) - Refactored CLI client with shared packages (cmd/cli-client) - Refactored GUI client with shared packages (cmd/gui-client) - Refactored web client with embedded HTML template (cmd/web-client) Security improvements: - Replaced math/rand with crypto/rand for randomDelay() - Added proper error handling for crypto operations Code reduction: eliminated ~500+ lines of duplicated code
Diffstat (limited to 'pkg/crypto/crypto.go')
-rw-r--r--pkg/crypto/crypto.go319
1 files changed, 319 insertions, 0 deletions
diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go
new file mode 100644
index 0000000..92cc459
--- /dev/null
+++ b/pkg/crypto/crypto.go
@@ -0,0 +1,319 @@
+// Package crypto provides shared cryptographic functions for NoshiTalk.
+// Includes message padding, AES-GCM encryption/decryption, and traffic obfuscation.
+package crypto
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/ecdh"
+ "crypto/hmac"
+ cryptorand "crypto/rand"
+ "crypto/sha256"
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net"
+ "time"
+
+ "github.com/awnumar/memguard"
+)
+
+const (
+ // MessageBlockSize is the padding block size for traffic analysis resistance
+ MessageBlockSize = 256
+
+ // MinRandomDelay is the minimum delay in milliseconds for timing attack mitigation
+ MinRandomDelay = 50
+
+ // MaxRandomDelay is the maximum delay in milliseconds for timing attack mitigation
+ MaxRandomDelay = 200
+
+ // NonceSize is the size of the GCM nonce
+ NonceSize = 12
+
+ // KeySize is the size of the AES-256 key
+ KeySize = 32
+
+ // ChallengeSize is the size of HMAC challenge/response
+ ChallengeSize = 32
+
+ // HandshakeTimeout is the timeout for key exchange and authentication
+ HandshakeTimeout = 30 * time.Second
+)
+
+// PadMessage pads a plaintext message to fixed block size to prevent traffic analysis.
+// Format: [2 bytes length][plaintext][random padding]
+func PadMessage(plaintext []byte) ([]byte, error) {
+ currentLen := len(plaintext)
+ paddedLen := ((currentLen / MessageBlockSize) + 1) * MessageBlockSize
+ padLen := paddedLen - currentLen
+
+ result := make([]byte, 2+paddedLen)
+ binary.BigEndian.PutUint16(result[0:2], uint16(currentLen))
+ copy(result[2:], plaintext)
+
+ if padLen > 0 {
+ padding := make([]byte, padLen)
+ if _, err := cryptorand.Read(padding); err != nil {
+ return nil, fmt.Errorf("failed to generate random padding: %w", err)
+ }
+ copy(result[2+currentLen:], padding)
+ }
+
+ return result, nil
+}
+
+// UnpadMessage removes padding from a padded message.
+func UnpadMessage(paddedData []byte) ([]byte, error) {
+ if len(paddedData) < 2 {
+ return nil, fmt.Errorf("padded data too short")
+ }
+
+ originalLen := binary.BigEndian.Uint16(paddedData[0:2])
+ if int(originalLen) > len(paddedData)-2 {
+ return nil, fmt.Errorf("invalid padding length")
+ }
+
+ return paddedData[2 : 2+originalLen], nil
+}
+
+// RandomDelay introduces a random delay for timing attack mitigation.
+// Uses crypto/rand for secure randomness.
+func RandomDelay() error {
+ var b [1]byte
+ if _, err := cryptorand.Read(b[:]); err != nil {
+ return err
+ }
+ // Map byte value to delay range
+ delay := MinRandomDelay + int(b[0])%(MaxRandomDelay-MinRandomDelay)
+ time.Sleep(time.Duration(delay) * time.Millisecond)
+ return nil
+}
+
+// DeriveIdentity derives a user identity string from a public key.
+// Format: "anon_" + first 8 bytes of SHA256(publicKey) in hex
+func DeriveIdentity(publicKey []byte) string {
+ hash := sha256.Sum256(publicKey)
+ return fmt.Sprintf("anon_%s", hex.EncodeToString(hash[:8]))
+}
+
+// DeriveFingerprint derives a fingerprint from a public key.
+// Returns first 16 bytes of SHA256 hash in hex.
+func DeriveFingerprint(publicKey []byte) string {
+ hash := sha256.Sum256(publicKey)
+ return hex.EncodeToString(hash[:16])
+}
+
+// SetupAESGCM creates an AES-GCM cipher from a shared secret.
+func SetupAESGCM(sharedSecret []byte) (cipher.AEAD, error) {
+ if len(sharedSecret) < KeySize {
+ return nil, fmt.Errorf("shared secret too short: need %d bytes, got %d", KeySize, len(sharedSecret))
+ }
+
+ block, err := aes.NewCipher(sharedSecret[:KeySize])
+ if err != nil {
+ return nil, fmt.Errorf("AES cipher creation failed: %w", err)
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, fmt.Errorf("GCM creation failed: %w", err)
+ }
+
+ return gcm, nil
+}
+
+// Encrypt encrypts a message using AES-GCM with random nonce.
+// Returns: [12-byte nonce][ciphertext]
+func Encrypt(gcm cipher.AEAD, plaintext []byte) ([]byte, error) {
+ nonce := make([]byte, NonceSize)
+ if _, err := io.ReadFull(cryptorand.Reader, nonce); err != nil {
+ return nil, fmt.Errorf("nonce generation failed: %w", err)
+ }
+
+ ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
+ return append(nonce, ciphertext...), nil
+}
+
+// Decrypt decrypts a message encrypted with Encrypt.
+// Expects: [12-byte nonce][ciphertext]
+func Decrypt(gcm cipher.AEAD, data []byte) ([]byte, error) {
+ if len(data) < NonceSize {
+ return nil, fmt.Errorf("data too short")
+ }
+
+ nonce := data[:NonceSize]
+ ciphertext := data[NonceSize:]
+
+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return nil, fmt.Errorf("decryption failed: %w", err)
+ }
+
+ return plaintext, nil
+}
+
+// EncryptMessage encrypts a string message with padding and traffic obfuscation.
+func EncryptMessage(gcm cipher.AEAD, message string) ([]byte, error) {
+ // Apply random delay for timing attack mitigation
+ if err := RandomDelay(); err != nil {
+ return nil, err
+ }
+
+ // Pad the message
+ padded, err := PadMessage([]byte(message))
+ if err != nil {
+ return nil, err
+ }
+
+ // Encrypt
+ return Encrypt(gcm, padded)
+}
+
+// DecryptMessage decrypts and unpads an encrypted message.
+func DecryptMessage(gcm cipher.AEAD, data []byte) (string, error) {
+ // Decrypt
+ padded, err := Decrypt(gcm, data)
+ if err != nil {
+ return "", err
+ }
+
+ // Unpad
+ plaintext, err := UnpadMessage(padded)
+ if err != nil {
+ return "", fmt.Errorf("unpad failed: %w", err)
+ }
+
+ return string(plaintext), nil
+}
+
+// GenerateX25519KeyPair generates a new X25519 key pair for ECDH.
+func GenerateX25519KeyPair() (*ecdh.PrivateKey, error) {
+ curve := ecdh.X25519()
+ return curve.GenerateKey(cryptorand.Reader)
+}
+
+// PerformECDH performs ECDH key exchange with a peer's public key.
+func PerformECDH(privateKey *ecdh.PrivateKey, peerPublicKeyBytes []byte) ([]byte, error) {
+ curve := ecdh.X25519()
+ peerPublicKey, err := curve.NewPublicKey(peerPublicKeyBytes)
+ if err != nil {
+ return nil, fmt.Errorf("invalid peer public key: %w", err)
+ }
+
+ sharedSecret, err := privateKey.ECDH(peerPublicKey)
+ if err != nil {
+ return nil, fmt.Errorf("ECDH failed: %w", err)
+ }
+
+ return sharedSecret, nil
+}
+
+// SecureBuffer wraps sensitive data in memguard for protection.
+func SecureBuffer(data []byte) *memguard.Enclave {
+ buf := memguard.NewBufferFromBytes(data)
+ enclave := buf.Seal()
+ return enclave
+}
+
+// PerformClientAuth performs client-side mutual authentication.
+// Protocol:
+// 1. Receive server challenge (32 bytes)
+// 2. Compute HMAC response
+// 3. Generate client challenge
+// 4. Send response + challenge
+// 5. Verify server response
+func PerformClientAuth(conn net.Conn, sharedSecret []byte) error {
+ conn.SetDeadline(time.Now().Add(HandshakeTimeout))
+ defer conn.SetDeadline(time.Time{})
+
+ // Receive server challenge
+ serverChallenge := make([]byte, ChallengeSize)
+ if _, err := io.ReadFull(conn, serverChallenge); err != nil {
+ return fmt.Errorf("receive server challenge failed: %w", err)
+ }
+
+ // Compute HMAC response
+ h := hmac.New(sha256.New, sharedSecret)
+ h.Write(serverChallenge)
+ clientResponse := h.Sum(nil)
+
+ // Generate client challenge
+ clientChallenge := make([]byte, ChallengeSize)
+ if _, err := cryptorand.Read(clientChallenge); err != nil {
+ return fmt.Errorf("challenge generation failed: %w", err)
+ }
+
+ // Send response + challenge
+ if _, err := conn.Write(clientResponse); err != nil {
+ return fmt.Errorf("send client response failed: %w", err)
+ }
+ if _, err := conn.Write(clientChallenge); err != nil {
+ return fmt.Errorf("send client challenge failed: %w", err)
+ }
+
+ // Receive and verify server response
+ serverResponse := make([]byte, ChallengeSize)
+ if _, err := io.ReadFull(conn, serverResponse); err != nil {
+ return fmt.Errorf("receive server response failed: %w", err)
+ }
+
+ h.Reset()
+ h.Write(clientChallenge)
+ expectedServerMAC := h.Sum(nil)
+
+ if !hmac.Equal(serverResponse, expectedServerMAC) {
+ return fmt.Errorf("server authentication failed - HMAC mismatch")
+ }
+
+ return nil
+}
+
+// PerformServerAuth performs server-side mutual authentication.
+func PerformServerAuth(conn net.Conn, sharedSecret []byte) error {
+ conn.SetDeadline(time.Now().Add(HandshakeTimeout))
+ defer conn.SetDeadline(time.Time{})
+
+ // Generate and send server challenge
+ serverChallenge := make([]byte, ChallengeSize)
+ if _, err := cryptorand.Read(serverChallenge); err != nil {
+ return fmt.Errorf("challenge generation failed: %w", err)
+ }
+
+ if _, err := conn.Write(serverChallenge); err != nil {
+ return fmt.Errorf("challenge send failed: %w", err)
+ }
+
+ // Receive client response
+ clientResponse := make([]byte, ChallengeSize)
+ if _, err := io.ReadFull(conn, clientResponse); err != nil {
+ return fmt.Errorf("client response read failed: %w", err)
+ }
+
+ // Receive client challenge
+ clientChallenge := make([]byte, ChallengeSize)
+ if _, err := io.ReadFull(conn, clientChallenge); err != nil {
+ return fmt.Errorf("client challenge read failed: %w", err)
+ }
+
+ // Verify client response
+ h := hmac.New(sha256.New, sharedSecret)
+ h.Write(serverChallenge)
+ expectedClientMAC := h.Sum(nil)
+
+ if !hmac.Equal(clientResponse, expectedClientMAC) {
+ return fmt.Errorf("client authentication failed")
+ }
+
+ // Send server response
+ h.Reset()
+ h.Write(clientChallenge)
+ serverResponse := h.Sum(nil)
+
+ if _, err := conn.Write(serverResponse); err != nil {
+ return fmt.Errorf("server response send failed: %w", err)
+ }
+
+ return nil
+}