mod crypto;
mod identity;
mod merkle;
pub use crypto::CryptoEngine;
pub use identity::{DeviceIdentity, PairedDevice, PairingConfirmation, PairingRequest};
pub use merkle::MerkleTree;
use cp_core::{CPError, CognitiveDiff, Result};
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedPayload {
pub ciphertext: Vec<u8>,
#[serde(with = "BigArray")]
pub nonce: [u8; 24],
#[serde(with = "BigArray")]
pub signature: [u8; 64],
pub public_key: [u8; 32],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedDiff {
pub encrypted_diff: EncryptedPayload,
#[serde(with = "BigArray")]
pub signature: [u8; 64],
pub sender_public_key: [u8; 32],
pub sender_device_id: [u8; 16],
pub target_device_id: [u8; 16],
pub sequence: u64,
}
impl SignedDiff {
pub fn new(
identity: &DeviceIdentity,
encrypted_diff: EncryptedPayload,
target_device_id: [u8; 16],
sequence: u64,
) -> Self {
let signing_data = Self::compute_signing_data(
&encrypted_diff.nonce,
&encrypted_diff.ciphertext,
&identity.device_id,
&target_device_id,
sequence,
);
let signature = identity.sign(&signing_data);
Self {
encrypted_diff,
signature,
sender_public_key: identity.public_key,
sender_device_id: identity.device_id,
target_device_id,
sequence,
}
}
fn compute_signing_data(
nonce: &[u8; 24],
ciphertext: &[u8],
sender_id: &[u8; 16],
target_id: &[u8; 16],
sequence: u64,
) -> Vec<u8> {
let mut data = Vec::with_capacity(24 + ciphertext.len() + 16 + 16 + 8);
data.extend_from_slice(nonce);
data.extend_from_slice(ciphertext);
data.extend_from_slice(sender_id);
data.extend_from_slice(target_id);
data.extend_from_slice(&sequence.to_le_bytes());
data
}
pub fn verify(&self) -> Result<()> {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let verifying_key = VerifyingKey::from_bytes(&self.sender_public_key)
.map_err(|e| CPError::Crypto(format!("Invalid public key: {e}")))?;
let expected_device_id: [u8; 16] = blake3::hash(&self.sender_public_key).as_bytes()[0..16]
.try_into()
.unwrap();
if expected_device_id != self.sender_device_id {
return Err(CPError::Verification(
"Device ID doesn't match public key".into(),
));
}
let signing_data = Self::compute_signing_data(
&self.encrypted_diff.nonce,
&self.encrypted_diff.ciphertext,
&self.sender_device_id,
&self.target_device_id,
self.sequence,
);
let signature = Signature::from_bytes(&self.signature);
verifying_key
.verify(&signing_data, &signature)
.map_err(|_| CPError::Verification("Invalid SignedDiff signature".into()))
}
pub fn sender_device_id_hex(&self) -> String {
use std::fmt::Write;
self.sender_device_id
.iter()
.fold(String::new(), |mut s, b| {
write!(s, "{b:02x}").unwrap();
s
})
}
pub fn target_device_id_hex(&self) -> String {
use std::fmt::Write;
self.target_device_id
.iter()
.fold(String::new(), |mut s, b| {
write!(s, "{b:02x}").unwrap();
s
})
}
}
pub fn serialize_diff(diff: &CognitiveDiff) -> Result<Vec<u8>> {
let mut cbor_bytes = Vec::new();
ciborium::into_writer(diff, &mut cbor_bytes)
.map_err(|e| CPError::Serialization(e.to_string()))?;
let compressed = zstd::encode_all(cbor_bytes.as_slice(), 3)
.map_err(|e| CPError::Serialization(e.to_string()))?;
Ok(compressed)
}
pub fn deserialize_diff(data: &[u8]) -> Result<CognitiveDiff> {
let decompressed = zstd::decode_all(data).map_err(|e| CPError::Serialization(e.to_string()))?;
let diff: CognitiveDiff = ciborium::from_reader(decompressed.as_slice())
.map_err(|e| CPError::Serialization(e.to_string()))?;
Ok(diff)
}
#[cfg(test)]
mod tests {
use super::*;
use cp_core::Hlc;
use uuid::Uuid;
#[test]
fn test_signed_diff_roundtrip() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let diff = CognitiveDiff::empty(
[0u8; 32],
Uuid::from_bytes(alice.device_id),
1,
Hlc::new(1000, alice.device_id),
);
let crypto = CryptoEngine::new();
let encrypted = crypto.encrypt_diff(&diff).unwrap();
let signed = SignedDiff::new(&alice, encrypted, bob.device_id, 1);
assert!(signed.verify().is_ok());
assert_eq!(signed.sender_device_id, alice.device_id);
assert_eq!(signed.target_device_id, bob.device_id);
assert_eq!(signed.sequence, 1);
}
#[test]
fn test_signed_diff_verification_fails_on_tamper() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let diff = CognitiveDiff::empty(
[0u8; 32],
Uuid::from_bytes(alice.device_id),
1,
Hlc::new(1000, alice.device_id),
);
let crypto = CryptoEngine::new();
let encrypted = crypto.encrypt_diff(&diff).unwrap();
let mut signed = SignedDiff::new(&alice, encrypted, bob.device_id, 1);
signed.sequence = 2;
assert!(signed.verify().is_err());
}
}