Skip to main content

cp_sync/
lib.rs

1//! CP Sync - Merkle trees, cognitive diffs, and crypto
2//!
3//! Provides:
4//! - Diff generation and application
5//! - Encryption (XChaCha20-Poly1305)
6//! - Signatures (Ed25519)
7//! - Device identity and pairing (X25519 key agreement)
8//! - Serialization (CBOR + zstd compression)
9
10mod crypto;
11mod merkle;
12mod identity;
13
14pub use crypto::CryptoEngine;
15pub use merkle::MerkleTree;
16pub use identity::{DeviceIdentity, PairedDevice, PairingRequest, PairingConfirmation};
17
18use cp_core::{CognitiveDiff, CPError, Result};
19
20use serde::{Deserialize, Serialize};
21use serde_big_array::BigArray;
22
23/// Encrypted payload for transmission
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct EncryptedPayload {
26    /// Encrypted CBOR data
27    pub ciphertext: Vec<u8>,
28    /// Nonce used for encryption
29    #[serde(with = "BigArray")]
30    pub nonce: [u8; 24],
31    /// Signature over the ciphertext
32    #[serde(with = "BigArray")]
33    pub signature: [u8; 64],
34    /// Signer's public key
35    pub public_key: [u8; 32],
36}
37
38/// Signed diff wrapper for relay transmission
39///
40/// Per CP-013: Wraps an encrypted diff with authentication data
41/// for secure relay-based synchronization.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct SignedDiff {
44    /// The encrypted diff payload
45    pub encrypted_diff: EncryptedPayload,
46
47    /// Ed25519 signature over: nonce || ciphertext || sender_id || target_id || sequence
48    #[serde(with = "BigArray")]
49    pub signature: [u8; 64],
50
51    /// Sender's Ed25519 public key
52    pub sender_public_key: [u8; 32],
53
54    /// Sender's device ID (BLAKE3-16 of public key)
55    pub sender_device_id: [u8; 16],
56
57    /// Target device ID (for routing)
58    pub target_device_id: [u8; 16],
59
60    /// Sequence number for ordering
61    pub sequence: u64,
62}
63
64impl SignedDiff {
65    /// Create a signed diff from an encrypted payload
66    pub fn new(
67        identity: &DeviceIdentity,
68        encrypted_diff: EncryptedPayload,
69        target_device_id: [u8; 16],
70        sequence: u64,
71    ) -> Self {
72        let signing_data = Self::compute_signing_data(
73            &encrypted_diff.nonce,
74            &encrypted_diff.ciphertext,
75            &identity.device_id,
76            &target_device_id,
77            sequence,
78        );
79
80        let signature = identity.sign(&signing_data);
81
82        Self {
83            encrypted_diff,
84            signature,
85            sender_public_key: identity.public_key,
86            sender_device_id: identity.device_id,
87            target_device_id,
88            sequence,
89        }
90    }
91
92    /// Compute the data to be signed
93    fn compute_signing_data(
94        nonce: &[u8; 24],
95        ciphertext: &[u8],
96        sender_id: &[u8; 16],
97        target_id: &[u8; 16],
98        sequence: u64,
99    ) -> Vec<u8> {
100        let mut data = Vec::with_capacity(24 + ciphertext.len() + 16 + 16 + 8);
101        data.extend_from_slice(nonce);
102        data.extend_from_slice(ciphertext);
103        data.extend_from_slice(sender_id);
104        data.extend_from_slice(target_id);
105        data.extend_from_slice(&sequence.to_le_bytes());
106        data
107    }
108
109    /// Verify the signature on this signed diff
110    pub fn verify(&self) -> Result<()> {
111        use ed25519_dalek::{Signature, VerifyingKey, Verifier};
112
113        let verifying_key = VerifyingKey::from_bytes(&self.sender_public_key)
114            .map_err(|e| CPError::Crypto(format!("Invalid public key: {}", e)))?;
115
116        // Verify that sender_device_id matches public key
117        let expected_device_id: [u8; 16] = blake3::hash(&self.sender_public_key).as_bytes()[0..16]
118            .try_into()
119            .unwrap();
120        if expected_device_id != self.sender_device_id {
121            return Err(CPError::Verification("Device ID doesn't match public key".into()));
122        }
123
124        let signing_data = Self::compute_signing_data(
125            &self.encrypted_diff.nonce,
126            &self.encrypted_diff.ciphertext,
127            &self.sender_device_id,
128            &self.target_device_id,
129            self.sequence,
130        );
131
132        let signature = Signature::from_bytes(&self.signature);
133
134        verifying_key
135            .verify(&signing_data, &signature)
136            .map_err(|_| CPError::Verification("Invalid SignedDiff signature".into()))
137    }
138
139    /// Get sender device ID as hex string
140    pub fn sender_device_id_hex(&self) -> String {
141        self.sender_device_id
142            .iter()
143            .map(|b| format!("{:02x}", b))
144            .collect()
145    }
146
147    /// Get target device ID as hex string
148    pub fn target_device_id_hex(&self) -> String {
149        self.target_device_id
150            .iter()
151            .map(|b| format!("{:02x}", b))
152            .collect()
153    }
154}
155
156/// Serialize a diff to CBOR and compress
157pub fn serialize_diff(diff: &CognitiveDiff) -> Result<Vec<u8>> {
158    let mut cbor_bytes = Vec::new();
159    ciborium::into_writer(diff, &mut cbor_bytes)
160        .map_err(|e| cp_core::CPError::Serialization(e.to_string()))?;
161    
162    let compressed = zstd::encode_all(cbor_bytes.as_slice(), 3)
163        .map_err(|e| cp_core::CPError::Serialization(e.to_string()))?;
164    
165    Ok(compressed)
166}
167
168/// Decompress and deserialize a diff from CBOR
169pub fn deserialize_diff(data: &[u8]) -> Result<CognitiveDiff> {
170    let decompressed = zstd::decode_all(data)
171        .map_err(|e| cp_core::CPError::Serialization(e.to_string()))?;
172
173    let diff: CognitiveDiff = ciborium::from_reader(decompressed.as_slice())
174        .map_err(|e| cp_core::CPError::Serialization(e.to_string()))?;
175
176    Ok(diff)
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use cp_core::Hlc;
183    use uuid::Uuid;
184
185    #[test]
186    fn test_signed_diff_roundtrip() {
187        let alice = DeviceIdentity::generate();
188        let bob = DeviceIdentity::generate();
189
190        // Create a simple diff
191        let diff = CognitiveDiff::empty(
192            [0u8; 32],
193            Uuid::from_bytes(alice.device_id),
194            1,
195            Hlc::new(1000, alice.device_id),
196        );
197
198        // Alice encrypts and signs for Bob
199        let crypto = CryptoEngine::new();
200        let encrypted = crypto.encrypt_diff(&diff).unwrap();
201
202        let signed = SignedDiff::new(&alice, encrypted, bob.device_id, 1);
203
204        // Verify signature
205        assert!(signed.verify().is_ok());
206
207        // Check device IDs
208        assert_eq!(signed.sender_device_id, alice.device_id);
209        assert_eq!(signed.target_device_id, bob.device_id);
210        assert_eq!(signed.sequence, 1);
211    }
212
213    #[test]
214    fn test_signed_diff_verification_fails_on_tamper() {
215        let alice = DeviceIdentity::generate();
216        let bob = DeviceIdentity::generate();
217
218        let diff = CognitiveDiff::empty(
219            [0u8; 32],
220            Uuid::from_bytes(alice.device_id),
221            1,
222            Hlc::new(1000, alice.device_id),
223        );
224
225        let crypto = CryptoEngine::new();
226        let encrypted = crypto.encrypt_diff(&diff).unwrap();
227
228        let mut signed = SignedDiff::new(&alice, encrypted, bob.device_id, 1);
229
230        // Tamper with the sequence
231        signed.sequence = 2;
232
233        // Verification should fail
234        assert!(signed.verify().is_err());
235    }
236}