Skip to main content

cp_sync/
crypto.rs

1//! Cryptographic operations for CP
2
3use chacha20poly1305::{
4    aead::{Aead, KeyInit},
5    XChaCha20Poly1305, XNonce,
6};
7use cp_core::{CPError, CognitiveDiff, Result};
8use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
9use rand::RngCore;
10
11use crate::{deserialize_diff, serialize_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 ed25519_dalek::{Signature, Verifier, VerifyingKey};
145    use uuid::Uuid;
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, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
151        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
152        0x1e, 0x1f,
153    ];
154
155    const TEST_MESSAGE: &[u8] = b"Canon Protocol v1.0 - Test Message";
156
157    #[test]
158    fn test_crypto_generate_keypair() {
159        let engine = CryptoEngine::new();
160
161        // Verify that a valid public key was generated (32 bytes)
162        let public_key = engine.public_key();
163        assert_eq!(public_key.len(), 32);
164
165        // Different engine instances should have different keys
166        let engine2 = CryptoEngine::new();
167        assert_ne!(engine.public_key(), engine2.public_key());
168    }
169
170    #[test]
171    fn test_crypto_sign() {
172        let engine = CryptoEngine::new();
173        let message = b"Test message for signing";
174
175        let signature = engine.sign(message);
176
177        // Ed25519 signature should be 64 bytes
178        assert_eq!(signature.to_bytes().len(), 64);
179    }
180
181    #[test]
182    fn test_crypto_verify_valid() {
183        let engine = CryptoEngine::new();
184        let message = b"Test message for verification";
185
186        let signature = engine.sign(message);
187        let public_key = engine.public_key();
188
189        // Verify the signature
190        let verifying_key = VerifyingKey::from_bytes(&public_key).unwrap();
191        let sig = Signature::from_bytes(&signature.to_bytes());
192
193        assert!(verifying_key.verify(message, &sig).is_ok());
194    }
195
196    #[test]
197    fn test_crypto_verify_invalid() {
198        let engine = CryptoEngine::new();
199        let message = b"Original message";
200
201        let signature = engine.sign(message);
202
203        // Try to verify with a different message - should fail
204        let public_key = engine.public_key();
205        let verifying_key = VerifyingKey::from_bytes(&public_key).unwrap();
206        let sig = Signature::from_bytes(&signature.to_bytes());
207
208        let wrong_message = b"Different message";
209        assert!(verifying_key.verify(wrong_message, &sig).is_err());
210    }
211
212    #[test]
213    fn test_crypto_encrypt_xchacha20() {
214        let engine = CryptoEngine::new();
215        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
216
217        let encrypted = engine.encrypt_diff(&diff).unwrap();
218
219        // Verify encrypted payload structure
220        assert_eq!(encrypted.nonce.len(), 24);
221        assert_eq!(encrypted.signature.len(), 64);
222        assert_eq!(encrypted.public_key.len(), 32);
223        // Ciphertext should be different from plaintext
224        assert!(!encrypted.ciphertext.is_empty());
225    }
226
227    #[test]
228    fn test_crypto_decrypt_xchacha20() {
229        let engine = CryptoEngine::new();
230        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
231
232        let encrypted = engine.encrypt_diff(&diff).unwrap();
233        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
234
235        // Verify metadata is preserved
236        assert_eq!(diff.metadata.device_id, decrypted.metadata.device_id);
237    }
238
239    #[test]
240    fn test_crypto_encrypt_decrypt_roundtrip() {
241        let engine = CryptoEngine::new();
242        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
243
244        let encrypted = engine.encrypt_diff(&diff).unwrap();
245        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
246
247        assert_eq!(diff.metadata.device_id, decrypted.metadata.device_id);
248    }
249
250    #[test]
251    fn test_crypto_nonce_uniqueness() {
252        let engine = CryptoEngine::new();
253        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
254
255        // Generate multiple encrypted diffs
256        let encrypted1 = engine.encrypt_diff(&diff).unwrap();
257        let encrypted2 = engine.encrypt_diff(&diff).unwrap();
258        let encrypted3 = engine.encrypt_diff(&diff).unwrap();
259
260        // Nonces should be unique (highly unlikely to collide)
261        assert_ne!(encrypted1.nonce, encrypted2.nonce);
262        assert_ne!(encrypted2.nonce, encrypted3.nonce);
263        assert_ne!(encrypted1.nonce, encrypted3.nonce);
264    }
265
266    #[test]
267    fn test_crypto_key_derivation_hkdf() {
268        // Test deterministic key derivation from seed
269        let engine1 = CryptoEngine::new_with_seed(TEST_SEED);
270        let engine2 = CryptoEngine::new_with_seed(TEST_SEED);
271
272        // Same seed should produce same keys
273        assert_eq!(engine1.public_key(), engine2.public_key());
274
275        // Different seed should produce different keys
276        let mut different_seed = TEST_SEED;
277        different_seed[0] ^= 0xFF;
278        let engine3 = CryptoEngine::new_with_seed(different_seed);
279
280        assert_ne!(engine1.public_key(), engine3.public_key());
281    }
282
283    #[test]
284    fn test_crypto_test_vector_encryption() {
285        // Test encryption with known test vector inputs
286        let symmetric_key = [0x42u8; 32];
287        let signing_key_bytes = [0x24u8; 32];
288        let engine = CryptoEngine::from_keys(symmetric_key, signing_key_bytes);
289
290        let diff = CognitiveDiff::empty([0u8; 32], Uuid::nil(), 0, Hlc::new(0, [0u8; 16]));
291
292        // Should be able to encrypt and decrypt
293        let encrypted = engine.encrypt_diff(&diff).unwrap();
294        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
295
296        assert_eq!(decrypted.metadata.device_id, Uuid::nil());
297    }
298
299    #[test]
300    fn test_crypto_test_vector_signature() {
301        // Test signature with known test vector
302        let engine = CryptoEngine::from_keys([0x42u8; 32], [0x24u8; 32]);
303
304        let signature = engine.sign(TEST_MESSAGE);
305
306        // Verify signature can be parsed and verified
307        let public_key = engine.public_key();
308        let verifying_key = VerifyingKey::from_bytes(&public_key).unwrap();
309        let sig = Signature::from_bytes(&signature.to_bytes());
310
311        assert!(verifying_key.verify(TEST_MESSAGE, &sig).is_ok());
312    }
313
314    #[test]
315    fn test_crypto_tampering_detection() {
316        let engine = CryptoEngine::new();
317        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
318
319        let mut encrypted = engine.encrypt_diff(&diff).unwrap();
320
321        // Tamper with the ciphertext
322        encrypted.ciphertext[0] ^= 0xFF;
323
324        // Decryption should fail due to authentication tag mismatch
325        assert!(engine.decrypt_diff(&encrypted).is_err());
326    }
327
328    #[test]
329    fn test_crypto_wrong_key_rejected() {
330        let engine1 = CryptoEngine::new();
331        let engine2 = CryptoEngine::new();
332
333        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
334
335        // Encrypt with engine1
336        let encrypted = engine1.encrypt_diff(&diff).unwrap();
337
338        // Try to decrypt with engine2 - should fail
339        assert!(engine2.decrypt_diff(&encrypted).is_err());
340    }
341
342    #[test]
343    fn test_crypto_signature_verification() {
344        let engine = CryptoEngine::new();
345        let diff = CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
346
347        let mut encrypted = engine.encrypt_diff(&diff).unwrap();
348
349        // Tamper with signature
350        encrypted.signature[0] ^= 0xFF;
351
352        assert!(engine.decrypt_diff(&encrypted).is_err());
353    }
354
355    #[test]
356    fn test_crypto_from_keys_deterministic() {
357        // Test that from_keys produces deterministic results
358        let symmetric_key = [0xAAu8; 32];
359        let signing_key_bytes = [0x55u8; 32];
360
361        let engine1 = CryptoEngine::from_keys(symmetric_key, signing_key_bytes);
362        let engine2 = CryptoEngine::from_keys(symmetric_key, signing_key_bytes);
363
364        assert_eq!(engine1.public_key(), engine2.public_key());
365    }
366
367    #[test]
368    fn test_crypto_large_diff_encryption() {
369        let engine = CryptoEngine::new();
370
371        // Create a diff with actual data
372        let mut diff =
373            CognitiveDiff::empty([0u8; 32], Uuid::new_v4(), 0, Hlc::new(1000, [0u8; 16]));
374
375        // Add multiple documents to make it larger
376        for i in 0..10 {
377            let doc = Document::new(
378                std::path::PathBuf::from(format!("test{i}.md")),
379                b"Test content for encryption",
380                0,
381            );
382            diff.added_docs.push(doc);
383        }
384
385        let encrypted = engine.encrypt_diff(&diff).unwrap();
386        let decrypted = engine.decrypt_diff(&encrypted).unwrap();
387
388        assert_eq!(diff.added_docs.len(), decrypted.added_docs.len());
389    }
390}