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 identity;
12mod merkle;
13
14pub use crypto::CryptoEngine;
15pub use identity::{DeviceIdentity, PairedDevice, PairingConfirmation, PairingRequest};
16pub use merkle::MerkleTree;
17
18use cp_core::{CPError, CognitiveDiff, 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, Verifier, VerifyingKey};
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(
122                "Device ID doesn't match public key".into(),
123            ));
124        }
125
126        let signing_data = Self::compute_signing_data(
127            &self.encrypted_diff.nonce,
128            &self.encrypted_diff.ciphertext,
129            &self.sender_device_id,
130            &self.target_device_id,
131            self.sequence,
132        );
133
134        let signature = Signature::from_bytes(&self.signature);
135
136        verifying_key
137            .verify(&signing_data, &signature)
138            .map_err(|_| CPError::Verification("Invalid SignedDiff signature".into()))
139    }
140
141    /// Get sender device ID as hex string
142    pub fn sender_device_id_hex(&self) -> String {
143        use std::fmt::Write;
144        self.sender_device_id
145            .iter()
146            .fold(String::new(), |mut s, b| {
147                write!(s, "{b:02x}").unwrap();
148                s
149            })
150    }
151
152    /// Get target device ID as hex string
153    pub fn target_device_id_hex(&self) -> String {
154        use std::fmt::Write;
155        self.target_device_id
156            .iter()
157            .fold(String::new(), |mut s, b| {
158                write!(s, "{b:02x}").unwrap();
159                s
160            })
161    }
162}
163
164/// Serialize a diff to CBOR and compress
165pub fn serialize_diff(diff: &CognitiveDiff) -> Result<Vec<u8>> {
166    let mut cbor_bytes = Vec::new();
167    ciborium::into_writer(diff, &mut cbor_bytes)
168        .map_err(|e| CPError::Serialization(e.to_string()))?;
169
170    let compressed = zstd::encode_all(cbor_bytes.as_slice(), 3)
171        .map_err(|e| CPError::Serialization(e.to_string()))?;
172
173    Ok(compressed)
174}
175
176/// Decompress and deserialize a diff from CBOR
177pub fn deserialize_diff(data: &[u8]) -> Result<CognitiveDiff> {
178    let decompressed = zstd::decode_all(data).map_err(|e| CPError::Serialization(e.to_string()))?;
179
180    let diff: CognitiveDiff = ciborium::from_reader(decompressed.as_slice())
181        .map_err(|e| CPError::Serialization(e.to_string()))?;
182
183    Ok(diff)
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use cp_core::Hlc;
190    use uuid::Uuid;
191
192    #[test]
193    fn test_signed_diff_roundtrip() {
194        let alice = DeviceIdentity::generate();
195        let bob = DeviceIdentity::generate();
196
197        // Create a simple diff
198        let diff = CognitiveDiff::empty(
199            [0u8; 32],
200            Uuid::from_bytes(alice.device_id),
201            1,
202            Hlc::new(1000, alice.device_id),
203        );
204
205        // Alice encrypts and signs for Bob
206        let crypto = CryptoEngine::new();
207        let encrypted = crypto.encrypt_diff(&diff).unwrap();
208
209        let signed = SignedDiff::new(&alice, encrypted, bob.device_id, 1);
210
211        // Verify signature
212        assert!(signed.verify().is_ok());
213
214        // Check device IDs
215        assert_eq!(signed.sender_device_id, alice.device_id);
216        assert_eq!(signed.target_device_id, bob.device_id);
217        assert_eq!(signed.sequence, 1);
218    }
219
220    #[test]
221    fn test_signed_diff_verification_fails_on_tamper() {
222        let alice = DeviceIdentity::generate();
223        let bob = DeviceIdentity::generate();
224
225        let diff = CognitiveDiff::empty(
226            [0u8; 32],
227            Uuid::from_bytes(alice.device_id),
228            1,
229            Hlc::new(1000, alice.device_id),
230        );
231
232        let crypto = CryptoEngine::new();
233        let encrypted = crypto.encrypt_diff(&diff).unwrap();
234
235        let mut signed = SignedDiff::new(&alice, encrypted, bob.device_id, 1);
236
237        // Tamper with the sequence
238        signed.sequence = 2;
239
240        // Verification should fail
241        assert!(signed.verify().is_err());
242    }
243}