Skip to main content

cp_sync/
crypto.rs

1//! Cryptographic operations for CP
2
3use cp_core::{CPError, CognitiveDiff, Result};
4use chacha20poly1305::{
5    aead::{Aead, KeyInit},
6    XChaCha20Poly1305, XNonce,
7};
8use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
9use rand::RngCore;
10
11use crate::{serialize_diff, deserialize_diff, EncryptedPayload};
12
13/// Cryptographic engine for encrypting and signing diffs
14pub struct CryptoEngine {
15    /// Symmetric encryption key
16    symmetric_key: [u8; 32],
17    /// Ed25519 signing key
18    signing_key: SigningKey,
19}
20
21impl CryptoEngine {
22    /// Create a new crypto engine with random keys
23    pub fn new() -> Self {
24        let mut rng = rand::thread_rng();
25        
26        let mut symmetric_key = [0u8; 32];
27        rng.fill_bytes(&mut symmetric_key);
28        
29        let mut signing_seed = [0u8; 32];
30        rng.fill_bytes(&mut signing_seed);
31        let signing_key = SigningKey::from_bytes(&signing_seed);
32        
33        Self {
34            symmetric_key,
35            signing_key,
36        }
37    }
38
39    /// Create from existing keys
40    pub fn from_keys(symmetric_key: [u8; 32], signing_key_bytes: [u8; 32]) -> Self {
41        Self {
42            symmetric_key,
43            signing_key: SigningKey::from_bytes(&signing_key_bytes),
44        }
45    }
46
47    /// Create from seed (derives keys deterministically)
48    pub fn new_with_seed(seed: [u8; 32]) -> Self {
49        // Simple derivation for MVP:
50        // symmetric = hash(seed || "enc")
51        // signing = hash(seed || "sign")
52        
53        let mut hasher = blake3::Hasher::new();
54        hasher.update(&seed);
55        hasher.update(b"enc");
56        let symmetric_key = *hasher.finalize().as_bytes();
57        
58        let mut hasher = blake3::Hasher::new();
59        hasher.update(&seed);
60        hasher.update(b"sign");
61        let signing_seed = *hasher.finalize().as_bytes();
62        
63        Self {
64            symmetric_key,
65            signing_key: SigningKey::from_bytes(&signing_seed),
66        }
67    }
68
69    /// Get the public key for verification
70    pub fn public_key(&self) -> [u8; 32] {
71        self.signing_key.verifying_key().to_bytes()
72    }
73
74    /// Encrypt and sign a cognitive diff
75    pub fn encrypt_diff(&self, diff: &CognitiveDiff) -> Result<EncryptedPayload> {
76        // Serialize and compress
77        let plaintext = serialize_diff(diff)?;
78        
79        // Generate random nonce
80        let mut nonce_bytes = [0u8; 24];
81        rand::thread_rng().fill_bytes(&mut nonce_bytes);
82        let nonce = XNonce::from_slice(&nonce_bytes);
83        
84        // Encrypt
85        let cipher = XChaCha20Poly1305::new_from_slice(&self.symmetric_key)
86            .map_err(|e| CPError::Crypto(e.to_string()))?;
87        
88        let ciphertext = cipher
89            .encrypt(nonce, plaintext.as_ref())
90            .map_err(|e| CPError::Crypto(e.to_string()))?;
91        
92        // Sign the ciphertext
93        let signature = self.signing_key.sign(&ciphertext);
94        
95        Ok(EncryptedPayload {
96            ciphertext,
97            nonce: nonce_bytes,
98            signature: signature.to_bytes(),
99            public_key: self.public_key(),
100        })
101    }
102
103    /// Sign arbitrary data
104    pub fn sign(&self, data: &[u8]) -> Signature {
105        self.signing_key.sign(data)
106    }
107
108    /// Decrypt and verify a payload
109    pub fn decrypt_diff(&self, payload: &EncryptedPayload) -> Result<CognitiveDiff> {
110        // Verify signature
111        let verifying_key = VerifyingKey::from_bytes(&payload.public_key)
112            .map_err(|e| CPError::Crypto(e.to_string()))?;
113        
114        let signature = Signature::from_bytes(&payload.signature);
115        
116        verifying_key
117            .verify(&payload.ciphertext, &signature)
118            .map_err(|_| CPError::Verification("Invalid signature".into()))?;
119        
120        // Decrypt
121        let nonce = XNonce::from_slice(&payload.nonce);
122        let cipher = XChaCha20Poly1305::new_from_slice(&self.symmetric_key)
123            .map_err(|e| CPError::Crypto(e.to_string()))?;
124        
125        let plaintext = cipher
126            .decrypt(nonce, payload.ciphertext.as_ref())
127            .map_err(|_| CPError::Crypto("Decryption failed".into()))?;
128        
129        // Deserialize
130        deserialize_diff(&plaintext)
131    }
132}
133
134impl Default for CryptoEngine {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use cp_core::{CognitiveDiff, Document, Hlc};
144    use uuid::Uuid;
145    use ed25519_dalek::{Signature, Verifier, VerifyingKey};
146
147    // CP-013 Test Vectors (known values for interoperability testing)
148    // These are sample values - in production, use values from CP-013 spec
149    const TEST_SEED: [u8; 32] = [
150        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
151        0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
152        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
153        0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
154    ];
155
156    const TEST_MESSAGE: &[u8] = b"Canon Protocol v1.0 - Test Message";
157
158    #[test]
159    fn test_crypto_generate_keypair() {
160        let engine = CryptoEngine::new();
161
162        // Verify that a valid public key was generated (32 bytes)
163        let public_key = engine.public_key();
164        assert_eq!(public_key.len(), 32);
165
166        // Different engine instances should have different keys
167        let engine2 = CryptoEngine::new();
168        assert_ne!(engine.public_key(), engine2.public_key());
169    }
170
171    #[test]
172    fn test_crypto_sign() {
173        let engine = CryptoEngine::new();
174        let message = b"Test message for signing";
175
176        let signature = engine.sign(message);
177
178        // Ed25519 signature should be 64 bytes
179        assert_eq!(signature.to_bytes().len(), 64);
180    }
181
182    #[test]
183    fn test_crypto_verify_valid() {
184        let engine = CryptoEngine::new();
185        let message = b"Test message for verification";
186
187        let signature = engine.sign(message);
188        let public_key = engine.public_key();
189
190        // Verify the signature
191        let verifying_key = VerifyingKey::from_bytes(&public_key).unwrap();
192        let sig = Signature::from_bytes(&signature.to_bytes());
193
194        assert!(verifying_key.verify(message, &sig).is_ok());
195    }
196
197    #[test]
198    fn test_crypto_verify_invalid() {
199        let engine = CryptoEngine::new();
200        let message = b"Original message";
201
202        let signature = engine.sign(message);
203
204        // Try to verify with a different message - should fail
205        let public_key = engine.public_key();
206        let verifying_key = VerifyingKey::from_bytes(&public_key).unwrap();
207        let sig = Signature::from_bytes(&signature.to_bytes());
208
209        let wrong_message = b"Different message";
210        assert!(verifying_key.verify(wrong_message, &sig).is_err());
211    }
212
213    #[test]
214    fn test_crypto_encrypt_xchacha20() {
215        let engine = CryptoEngine::new();
216        let diff = CognitiveDiff::empty(
217            [0u8; 32],
218            Uuid::new_v4(),
219            0,
220            Hlc::new(1000, [0u8; 16]),
221        );
222
223        let encrypted = engine.encrypt_diff(&diff).unwrap();
224
225        // Verify encrypted payload structure
226        assert_eq!(encrypted.nonce.len(), 24);
227        assert_eq!(encrypted.signature.len(), 64);
228        assert_eq!(encrypted.public_key.len(), 32);
229        // Ciphertext should be different from plaintext
230        assert!(!encrypted.ciphertext.is_empty());
231    }
232
233    #[test]
234    fn test_crypto_decrypt_xchacha20() {
235        let engine = CryptoEngine::new();
236        let diff = CognitiveDiff::empty(
237            [0u8; 32],
238            Uuid::new_v4(),
239            0,
240            Hlc::new(1000, [0u8; 16]),
241        );
242
243        let encrypted = engine.encrypt_diff(&diff).unwrap();
244        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
245
246        // Verify metadata is preserved
247        assert_eq!(diff.metadata.device_id, decrypted.metadata.device_id);
248    }
249
250    #[test]
251    fn test_crypto_encrypt_decrypt_roundtrip() {
252        let engine = CryptoEngine::new();
253        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
254
255        let encrypted = engine.encrypt_diff(&diff).unwrap();
256        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
257
258        assert_eq!(diff.metadata.device_id, decrypted.metadata.device_id);
259    }
260
261    #[test]
262    fn test_crypto_nonce_uniqueness() {
263        let engine = CryptoEngine::new();
264        let diff = CognitiveDiff::empty(
265            [0u8; 32],
266            Uuid::new_v4(),
267            0,
268            Hlc::new(1000, [0u8; 16]),
269        );
270
271        // Generate multiple encrypted diffs
272        let encrypted1 = engine.encrypt_diff(&diff).unwrap();
273        let encrypted2 = engine.encrypt_diff(&diff).unwrap();
274        let encrypted3 = engine.encrypt_diff(&diff).unwrap();
275
276        // Nonces should be unique (highly unlikely to collide)
277        assert_ne!(encrypted1.nonce, encrypted2.nonce);
278        assert_ne!(encrypted2.nonce, encrypted3.nonce);
279        assert_ne!(encrypted1.nonce, encrypted3.nonce);
280    }
281
282    #[test]
283    fn test_crypto_key_derivation_hkdf() {
284        // Test deterministic key derivation from seed
285        let engine1 = CryptoEngine::new_with_seed(TEST_SEED);
286        let engine2 = CryptoEngine::new_with_seed(TEST_SEED);
287
288        // Same seed should produce same keys
289        assert_eq!(engine1.public_key(), engine2.public_key());
290
291        // Different seed should produce different keys
292        let mut different_seed = TEST_SEED;
293        different_seed[0] ^= 0xFF;
294        let engine3 = CryptoEngine::new_with_seed(different_seed);
295
296        assert_ne!(engine1.public_key(), engine3.public_key());
297    }
298
299    #[test]
300    fn test_crypto_test_vector_encryption() {
301        // Test encryption with known test vector inputs
302        let symmetric_key = [0x42u8; 32];
303        let signing_key_bytes = [0x24u8; 32];
304        let engine = CryptoEngine::from_keys(symmetric_key, signing_key_bytes);
305
306        let diff = CognitiveDiff::empty(
307            [0u8; 32],
308            Uuid::nil(),
309            0,
310            Hlc::new(0, [0u8; 16]),
311        );
312
313        // Should be able to encrypt and decrypt
314        let encrypted = engine.encrypt_diff(&diff).unwrap();
315        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
316
317        assert_eq!(decrypted.metadata.device_id, Uuid::nil());
318    }
319
320    #[test]
321    fn test_crypto_test_vector_signature() {
322        // Test signature with known test vector
323        let engine = CryptoEngine::from_keys([0x42u8; 32], [0x24u8; 32]);
324
325        let signature = engine.sign(TEST_MESSAGE);
326
327        // Verify signature can be parsed and verified
328        let public_key = engine.public_key();
329        let verifying_key = VerifyingKey::from_bytes(&public_key).unwrap();
330        let sig = Signature::from_bytes(&signature.to_bytes());
331
332        assert!(verifying_key.verify(TEST_MESSAGE, &sig).is_ok());
333    }
334
335    #[test]
336    fn test_crypto_tampering_detection() {
337        let engine = CryptoEngine::new();
338        let diff = CognitiveDiff::empty(
339            [0u8; 32],
340            Uuid::new_v4(),
341            0,
342            Hlc::new(1000, [0u8; 16]),
343        );
344
345        let mut encrypted = engine.encrypt_diff(&diff).unwrap();
346
347        // Tamper with the ciphertext
348        encrypted.ciphertext[0] ^= 0xFF;
349
350        // Decryption should fail due to authentication tag mismatch
351        assert!(engine.decrypt_diff(&encrypted).is_err());
352    }
353
354    #[test]
355    fn test_crypto_wrong_key_rejected() {
356        let engine1 = CryptoEngine::new();
357        let engine2 = CryptoEngine::new();
358
359        let diff = CognitiveDiff::empty(
360            [0u8; 32],
361            Uuid::new_v4(),
362            0,
363            Hlc::new(1000, [0u8; 16]),
364        );
365
366        // Encrypt with engine1
367        let encrypted = engine1.encrypt_diff(&diff).unwrap();
368
369        // Try to decrypt with engine2 - should fail
370        assert!(engine2.decrypt_diff(&encrypted).is_err());
371    }
372
373    #[test]
374    fn test_crypto_signature_verification() {
375        let engine = CryptoEngine::new();
376        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
377
378        let mut encrypted = engine.encrypt_diff(&diff).unwrap();
379
380        // Tamper with signature
381        encrypted.signature[0] ^= 0xFF;
382
383        assert!(engine.decrypt_diff(&encrypted).is_err());
384    }
385
386    #[test]
387    fn test_crypto_from_keys_deterministic() {
388        // Test that from_keys produces deterministic results
389        let symmetric_key = [0xAAu8; 32];
390        let signing_key_bytes = [0x55u8; 32];
391
392        let engine1 = CryptoEngine::from_keys(symmetric_key, signing_key_bytes);
393        let engine2 = CryptoEngine::from_keys(symmetric_key, signing_key_bytes);
394
395        assert_eq!(engine1.public_key(), engine2.public_key());
396    }
397
398    #[test]
399    fn test_crypto_large_diff_encryption() {
400        let engine = CryptoEngine::new();
401
402        // Create a diff with actual data
403        let mut diff = CognitiveDiff::empty(
404            [0u8; 32],
405            Uuid::new_v4(),
406            0,
407            Hlc::new(1000, [0u8; 16]),
408        );
409
410        // Add multiple documents to make it larger
411        for i in 0..10 {
412            let mut doc = Document::new(
413                std::path::PathBuf::from(format!("test{}.md", i)),
414                b"Test content for encryption",
415                0,
416            );
417            diff.added_docs.push(doc);
418        }
419
420        let encrypted = engine.encrypt_diff(&diff).unwrap();
421        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
422
423        assert_eq!(diff.added_docs.len(), decrypted.added_docs.len());
424    }
425}