exo_federation/
crypto.rs

1//! Post-quantum cryptography primitives
2//!
3//! This module provides cryptographic primitives for federation security:
4//! - CRYSTALS-Kyber-1024 key exchange (NIST FIPS 203)
5//! - ChaCha20-Poly1305 AEAD encryption
6//! - HKDF-SHA256 key derivation
7//! - Constant-time operations
8//! - Secure memory zeroization
9//!
10//! # Security Level
11//!
12//! All primitives provide 256-bit classical security and 128+ bit post-quantum security.
13//!
14//! # Threat Model
15//!
16//! See /docs/SECURITY.md for comprehensive threat model and security architecture.
17
18use serde::{Deserialize, Serialize};
19use crate::{Result, FederationError};
20use zeroize::{Zeroize, ZeroizeOnDrop};
21
22// Re-export for convenience
23pub use pqcrypto_kyber::kyber1024;
24use pqcrypto_traits::kem::{PublicKey, SecretKey, SharedSecret as PqSharedSecret, Ciphertext};
25
26/// Post-quantum cryptographic keypair
27///
28/// Uses CRYSTALS-Kyber-1024 for IND-CCA2 secure key encapsulation.
29///
30/// # Security Properties
31///
32/// - Public key: 1568 bytes (safe to distribute)
33/// - Secret key: 3168 bytes (MUST be protected, auto-zeroized on drop)
34/// - Post-quantum security: 256 bits (NIST Level 5)
35///
36/// # Example
37///
38/// ```ignore
39/// let keypair = PostQuantumKeypair::generate();
40/// let public_bytes = keypair.public_key();
41/// // Send public_bytes to peer
42/// ```
43#[derive(Clone)]
44pub struct PostQuantumKeypair {
45    /// Public key (safe to share)
46    pub public: Vec<u8>,
47    /// Secret key (automatically zeroized on drop)
48    secret: SecretKeyWrapper,
49}
50
51impl std::fmt::Debug for PostQuantumKeypair {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("PostQuantumKeypair")
54            .field("public", &format!("{}bytes", self.public.len()))
55            .field("secret", &"[REDACTED]")
56            .finish()
57    }
58}
59
60/// Wrapper for secret key with automatic zeroization
61#[derive(Clone, Zeroize, ZeroizeOnDrop)]
62struct SecretKeyWrapper(Vec<u8>);
63
64impl PostQuantumKeypair {
65    /// Generate a new post-quantum keypair using CRYSTALS-Kyber-1024
66    ///
67    /// # Security
68    ///
69    /// Uses OS CSPRNG (via `rand::thread_rng()`). Ensure OS has sufficient entropy.
70    ///
71    /// # Panics
72    ///
73    /// Never panics. Kyber key generation is deterministic after RNG sampling.
74    pub fn generate() -> Self {
75        let (public, secret) = kyber1024::keypair();
76
77        Self {
78            public: public.as_bytes().to_vec(),
79            secret: SecretKeyWrapper(secret.as_bytes().to_vec()),
80        }
81    }
82
83    /// Get the public key bytes
84    ///
85    /// Safe to transmit over insecure channels.
86    pub fn public_key(&self) -> &[u8] {
87        &self.public
88    }
89
90    /// Encapsulate: generate shared secret and ciphertext for recipient's public key
91    ///
92    /// # Arguments
93    ///
94    /// * `public_key` - Recipient's Kyber-1024 public key (1568 bytes)
95    ///
96    /// # Returns
97    ///
98    /// * `SharedSecret` - 32-byte shared secret (use for key derivation)
99    /// * `Vec<u8>` - 1568-byte ciphertext (send to recipient)
100    ///
101    /// # Errors
102    ///
103    /// Returns `CryptoError` if public key is invalid (wrong size or corrupted).
104    ///
105    /// # Security
106    ///
107    /// The shared secret is cryptographically strong (256-bit entropy).
108    /// The ciphertext is IND-CCA2 secure against quantum adversaries.
109    pub fn encapsulate(public_key: &[u8]) -> Result<(SharedSecret, Vec<u8>)> {
110        // Validate public key size (Kyber1024 = 1568 bytes)
111        if public_key.len() != 1568 {
112            return Err(FederationError::CryptoError(
113                format!("Invalid public key size: expected 1568 bytes, got {}", public_key.len())
114            ));
115        }
116
117        // Parse public key
118        let pk = kyber1024::PublicKey::from_bytes(public_key)
119            .map_err(|e| FederationError::CryptoError(
120                format!("Failed to parse Kyber public key: {:?}", e)
121            ))?;
122
123        // Perform KEM encapsulation
124        let (shared_secret, ciphertext) = kyber1024::encapsulate(&pk);
125
126        Ok((
127            SharedSecret(SecretBytes(shared_secret.as_bytes().to_vec())),
128            ciphertext.as_bytes().to_vec()
129        ))
130    }
131
132    /// Decapsulate: extract shared secret from ciphertext
133    ///
134    /// # Arguments
135    ///
136    /// * `ciphertext` - 1568-byte Kyber-1024 ciphertext
137    ///
138    /// # Returns
139    ///
140    /// * `SharedSecret` - 32-byte shared secret (same as encapsulator's)
141    ///
142    /// # Errors
143    ///
144    /// Returns `CryptoError` if:
145    /// - Ciphertext is wrong size
146    /// - Ciphertext is invalid or corrupted
147    /// - Decapsulation fails (should never happen with valid inputs)
148    ///
149    /// # Security
150    ///
151    /// Timing-safe: execution time independent of secret key or ciphertext validity.
152    pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<SharedSecret> {
153        // Validate ciphertext size
154        if ciphertext.len() != 1568 {
155            return Err(FederationError::CryptoError(
156                format!("Invalid ciphertext size: expected 1568 bytes, got {}", ciphertext.len())
157            ));
158        }
159
160        // Parse secret key
161        let sk = kyber1024::SecretKey::from_bytes(&self.secret.0)
162            .map_err(|e| FederationError::CryptoError(
163                format!("Failed to parse secret key: {:?}", e)
164            ))?;
165
166        // Parse ciphertext
167        let ct = kyber1024::Ciphertext::from_bytes(ciphertext)
168            .map_err(|e| FederationError::CryptoError(
169                format!("Failed to parse Kyber ciphertext: {:?}", e)
170            ))?;
171
172        // Perform KEM decapsulation
173        let shared_secret = kyber1024::decapsulate(&ct, &sk);
174
175        Ok(SharedSecret(SecretBytes(shared_secret.as_bytes().to_vec())))
176    }
177}
178
179/// Secret bytes wrapper with automatic zeroization
180#[derive(Clone, Zeroize, ZeroizeOnDrop)]
181struct SecretBytes(Vec<u8>);
182
183/// Shared secret derived from Kyber KEM
184///
185/// # Security
186///
187/// - Automatically zeroized on drop
188/// - 32 bytes of cryptographically strong key material
189/// - Suitable for HKDF key derivation
190#[derive(Clone, Zeroize, ZeroizeOnDrop)]
191pub struct SharedSecret(SecretBytes);
192
193impl std::fmt::Debug for SharedSecret {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        f.debug_struct("SharedSecret")
196            .field("bytes", &"[REDACTED]")
197            .finish()
198    }
199}
200
201impl SharedSecret {
202    /// Derive encryption and MAC keys from shared secret using HKDF-SHA256
203    ///
204    /// # Key Derivation
205    ///
206    /// ```text
207    /// shared_secret (32 bytes from Kyber)
208    ///     ↓
209    /// HKDF-Extract(salt=zeros, ikm=shared_secret) → PRK
210    ///     ↓
211    /// HKDF-Expand(PRK, info="encryption") → encryption_key (32 bytes)
212    /// HKDF-Expand(PRK, info="mac") → mac_key (32 bytes)
213    /// ```
214    ///
215    /// # Returns
216    ///
217    /// - Encryption key: 256-bit key for ChaCha20
218    /// - MAC key: 256-bit key for Poly1305
219    ///
220    /// # Security
221    ///
222    /// Keys are cryptographically independent. Compromise of one does not affect the other.
223    pub fn derive_keys(&self) -> (Vec<u8>, Vec<u8>) {
224        use hmac::{Hmac, Mac};
225        use sha2::Sha256;
226
227        type HmacSha256 = Hmac<Sha256>;
228
229        // HKDF-Extract: PRK = HMAC-SHA256(salt=zeros, ikm=shared_secret)
230        let salt = [0u8; 32]; // Zero salt is acceptable for Kyber output
231        let mut extract_hmac = HmacSha256::new_from_slice(&salt)
232            .expect("HMAC-SHA256 accepts any key size");
233        extract_hmac.update(&self.0.0);
234        let prk = extract_hmac.finalize().into_bytes();
235
236        // HKDF-Expand for encryption key
237        let mut enc_hmac = HmacSha256::new_from_slice(&prk)
238            .expect("PRK is valid HMAC key");
239        enc_hmac.update(b"encryption");
240        enc_hmac.update(&[1u8]); // Counter = 1
241        let encrypt_key = enc_hmac.finalize().into_bytes().to_vec();
242
243        // HKDF-Expand for MAC key
244        let mut mac_hmac = HmacSha256::new_from_slice(&prk)
245            .expect("PRK is valid HMAC key");
246        mac_hmac.update(b"mac");
247        mac_hmac.update(&[1u8]); // Counter = 1
248        let mac_key = mac_hmac.finalize().into_bytes().to_vec();
249
250        (encrypt_key, mac_key)
251    }
252}
253
254/// Encrypted communication channel using ChaCha20-Poly1305 AEAD
255///
256/// # Security Properties
257///
258/// - Confidentiality: ChaCha20 stream cipher (IND-CPA)
259/// - Integrity: Poly1305 MAC (SUF-CMA)
260/// - AEAD: Combined mode (IND-CCA2)
261/// - Nonce: 96-bit random + 32-bit counter (unique per message)
262///
263/// # Example
264///
265/// ```ignore
266/// let channel = EncryptedChannel::new(peer_id, shared_secret);
267/// let ciphertext = channel.encrypt(b"secret message")?;
268/// let plaintext = channel.decrypt(&ciphertext)?;
269/// ```
270#[derive(Debug, Serialize, Deserialize)]
271pub struct EncryptedChannel {
272    /// Peer identifier
273    pub peer_id: String,
274    /// Encryption key (not serialized - ephemeral)
275    #[serde(skip)]
276    encrypt_key: Vec<u8>,
277    /// MAC key for authentication (not serialized - ephemeral)
278    #[serde(skip)]
279    mac_key: Vec<u8>,
280    /// Message counter for nonce generation
281    #[serde(skip)]
282    counter: std::sync::atomic::AtomicU32,
283}
284
285impl Clone for EncryptedChannel {
286    fn clone(&self) -> Self {
287        Self {
288            peer_id: self.peer_id.clone(),
289            encrypt_key: self.encrypt_key.clone(),
290            mac_key: self.mac_key.clone(),
291            counter: std::sync::atomic::AtomicU32::new(
292                self.counter.load(std::sync::atomic::Ordering::SeqCst)
293            ),
294        }
295    }
296}
297
298impl EncryptedChannel {
299    /// Create a new encrypted channel from a shared secret
300    ///
301    /// # Arguments
302    ///
303    /// * `peer_id` - Identifier for the peer (for auditing/logging)
304    /// * `shared_secret` - Shared secret from Kyber KEM
305    ///
306    /// # Security
307    ///
308    /// Keys are derived using HKDF-SHA256 with domain separation.
309    pub fn new(peer_id: String, shared_secret: SharedSecret) -> Self {
310        let (encrypt_key, mac_key) = shared_secret.derive_keys();
311
312        Self {
313            peer_id,
314            encrypt_key,
315            mac_key,
316            counter: std::sync::atomic::AtomicU32::new(0),
317        }
318    }
319
320    /// Encrypt a message using ChaCha20-Poly1305
321    ///
322    /// # Arguments
323    ///
324    /// * `plaintext` - Message to encrypt
325    ///
326    /// # Returns
327    ///
328    /// Ciphertext format: `[nonce: 12 bytes][ciphertext][tag: 16 bytes]`
329    ///
330    /// # Errors
331    ///
332    /// Returns `CryptoError` if encryption fails (should never happen).
333    ///
334    /// # Security
335    ///
336    /// - Unique nonce per message (96-bit random + 32-bit counter)
337    /// - Authenticated encryption (modify ciphertext = detection)
338    /// - Quantum resistance: 128-bit security (Grover bound)
339    pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
340        use chacha20poly1305::{
341            aead::{Aead, KeyInit},
342            ChaCha20Poly1305, Nonce,
343        };
344
345        // Create cipher instance
346        let key_array: [u8; 32] = self.encrypt_key.as_slice().try_into()
347            .map_err(|_| FederationError::CryptoError("Invalid key size".into()))?;
348        let cipher = ChaCha20Poly1305::new(&key_array.into());
349
350        // Generate unique nonce: [random: 8 bytes][counter: 4 bytes]
351        let mut nonce_bytes = [0u8; 12];
352        nonce_bytes[0..8].copy_from_slice(&rand::random::<[u8; 8]>());
353        let counter = self.counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
354        nonce_bytes[8..12].copy_from_slice(&counter.to_le_bytes());
355        let nonce = Nonce::from_slice(&nonce_bytes);
356
357        // Encrypt with AEAD
358        let ciphertext = cipher.encrypt(nonce, plaintext)
359            .map_err(|e| FederationError::CryptoError(
360                format!("ChaCha20-Poly1305 encryption failed: {}", e)
361            ))?;
362
363        // Prepend nonce to ciphertext (needed for decryption)
364        let mut result = nonce_bytes.to_vec();
365        result.extend_from_slice(&ciphertext);
366
367        Ok(result)
368    }
369
370    /// Decrypt a message using ChaCha20-Poly1305
371    ///
372    /// # Arguments
373    ///
374    /// * `ciphertext` - Encrypted message (format: `[nonce: 12][ciphertext][tag: 16]`)
375    ///
376    /// # Returns
377    ///
378    /// Decrypted plaintext
379    ///
380    /// # Errors
381    ///
382    /// Returns `CryptoError` if:
383    /// - Ciphertext is too short (< 28 bytes)
384    /// - Authentication tag verification fails (tampering detected)
385    /// - Decryption fails
386    ///
387    /// # Security
388    ///
389    /// - **Constant-time**: Timing independent of plaintext content
390    /// - **Tamper-evident**: Any modification causes authentication failure
391    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
392        use chacha20poly1305::{
393            aead::{Aead, KeyInit},
394            ChaCha20Poly1305, Nonce,
395        };
396
397        // Validate minimum size: nonce(12) + tag(16) = 28 bytes
398        if ciphertext.len() < 28 {
399            return Err(FederationError::CryptoError(
400                format!("Ciphertext too short: {} bytes (minimum 28)", ciphertext.len())
401            ));
402        }
403
404        // Extract nonce and ciphertext
405        let (nonce_bytes, ct) = ciphertext.split_at(12);
406        let nonce = Nonce::from_slice(nonce_bytes);
407
408        // Create cipher instance
409        let key_array: [u8; 32] = self.encrypt_key.as_slice().try_into()
410            .map_err(|_| FederationError::CryptoError("Invalid key size".into()))?;
411        let cipher = ChaCha20Poly1305::new(&key_array.into());
412
413        // Decrypt with AEAD (authentication happens here)
414        let plaintext = cipher.decrypt(nonce, ct)
415            .map_err(|e| FederationError::CryptoError(
416                format!("ChaCha20-Poly1305 decryption failed (tampering?): {}", e)
417            ))?;
418
419        Ok(plaintext)
420    }
421
422    /// Sign a message with HMAC-SHA256
423    ///
424    /// # Arguments
425    ///
426    /// * `message` - Message to authenticate
427    ///
428    /// # Returns
429    ///
430    /// 32-byte HMAC tag
431    ///
432    /// # Security
433    ///
434    /// - PRF security: tag reveals nothing about key
435    /// - Quantum resistance: 128-bit security (Grover)
436    ///
437    /// # Note
438    ///
439    /// If using `encrypt()`, signatures are redundant (Poly1305 provides authentication).
440    /// Use this for non-encrypted authenticated messages.
441    pub fn sign(&self, message: &[u8]) -> Vec<u8> {
442        use hmac::{Hmac, Mac};
443        use sha2::Sha256;
444
445        let mut mac = Hmac::<Sha256>::new_from_slice(&self.mac_key)
446            .expect("HMAC-SHA256 accepts any key size");
447        mac.update(message);
448        mac.finalize().into_bytes().to_vec()
449    }
450
451    /// Verify a message signature using constant-time comparison
452    ///
453    /// # Arguments
454    ///
455    /// * `message` - Original message
456    /// * `signature` - HMAC tag to verify
457    ///
458    /// # Returns
459    ///
460    /// `true` if signature is valid, `false` otherwise
461    ///
462    /// # Security
463    ///
464    /// - **Constant-time**: Execution time independent of signature validity
465    /// - **Timing-attack resistant**: No early termination on mismatch
466    ///
467    /// # Critical Security Property
468    ///
469    /// This function MUST use constant-time comparison to prevent timing side-channels.
470    pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
471        use subtle::ConstantTimeEq;
472
473        let expected = self.sign(message);
474
475        // Constant-time comparison (critical for security)
476        if expected.len() != signature.len() {
477            return false;
478        }
479
480        expected.ct_eq(signature).into()
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    #[test]
489    fn test_keypair_generation() {
490        let keypair = PostQuantumKeypair::generate();
491        assert_eq!(keypair.public.len(), 1568); // Kyber-1024 public key size
492    }
493
494    #[test]
495    fn test_key_exchange() {
496        let alice = PostQuantumKeypair::generate();
497        let bob = PostQuantumKeypair::generate();
498
499        // Alice encapsulates to Bob
500        let (alice_secret, ciphertext) = PostQuantumKeypair::encapsulate(bob.public_key()).unwrap();
501
502        // Bob decapsulates
503        let bob_secret = bob.decapsulate(&ciphertext).unwrap();
504
505        // Derive keys and verify they match
506        let (alice_enc, alice_mac) = alice_secret.derive_keys();
507        let (bob_enc, bob_mac) = bob_secret.derive_keys();
508
509        assert_eq!(alice_enc, bob_enc, "Encryption keys must match");
510        assert_eq!(alice_mac, bob_mac, "MAC keys must match");
511    }
512
513    #[test]
514    fn test_encrypted_channel() {
515        let keypair = PostQuantumKeypair::generate();
516        let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
517
518        let channel = EncryptedChannel::new("peer1".to_string(), secret);
519
520        let plaintext = b"Hello, post-quantum federation!";
521        let ciphertext = channel.encrypt(plaintext).unwrap();
522
523        // Verify ciphertext is different
524        assert_ne!(&ciphertext[12..], plaintext);
525
526        // Decrypt and verify
527        let decrypted = channel.decrypt(&ciphertext).unwrap();
528        assert_eq!(plaintext, &decrypted[..]);
529    }
530
531    #[test]
532    fn test_message_signing() {
533        let keypair = PostQuantumKeypair::generate();
534        let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
535        let channel = EncryptedChannel::new("peer1".to_string(), secret);
536
537        let message = b"Important authenticated message";
538        let signature = channel.sign(message);
539
540        // Verify valid signature
541        assert!(channel.verify(message, &signature));
542
543        // Verify invalid signature
544        assert!(!channel.verify(b"Different message", &signature));
545
546        // Verify tampered signature
547        let mut bad_sig = signature.clone();
548        bad_sig[0] ^= 1; // Flip one bit
549        assert!(!channel.verify(message, &bad_sig));
550    }
551
552    #[test]
553    fn test_decryption_tamper_detection() {
554        let keypair = PostQuantumKeypair::generate();
555        let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
556        let channel = EncryptedChannel::new("peer1".to_string(), secret);
557
558        let plaintext = b"Secret message";
559        let mut ciphertext = channel.encrypt(plaintext).unwrap();
560
561        // Tamper with ciphertext (flip one bit in encrypted data)
562        ciphertext[20] ^= 1;
563
564        // Decryption should fail due to authentication
565        let result = channel.decrypt(&ciphertext);
566        assert!(result.is_err(), "Tampered ciphertext should fail authentication");
567    }
568
569    #[test]
570    fn test_invalid_public_key_size() {
571        let bad_pk = vec![0u8; 100]; // Wrong size
572        let result = PostQuantumKeypair::encapsulate(&bad_pk);
573        assert!(result.is_err());
574    }
575
576    #[test]
577    fn test_invalid_ciphertext_size() {
578        let keypair = PostQuantumKeypair::generate();
579        let bad_ct = vec![0u8; 100]; // Wrong size
580        let result = keypair.decapsulate(&bad_ct);
581        assert!(result.is_err());
582    }
583
584    #[test]
585    fn test_nonce_uniqueness() {
586        let keypair = PostQuantumKeypair::generate();
587        let (secret, _) = PostQuantumKeypair::encapsulate(keypair.public_key()).unwrap();
588        let channel = EncryptedChannel::new("peer1".to_string(), secret);
589
590        let plaintext = b"Test message";
591
592        // Encrypt same message twice
593        let ct1 = channel.encrypt(plaintext).unwrap();
594        let ct2 = channel.encrypt(plaintext).unwrap();
595
596        // Ciphertexts should be different (different nonces)
597        assert_ne!(ct1, ct2, "Nonces must be unique");
598
599        // Both should decrypt correctly
600        assert_eq!(channel.decrypt(&ct1).unwrap(), plaintext);
601        assert_eq!(channel.decrypt(&ct2).unwrap(), plaintext);
602    }
603}