use cp_core::{CPError, Result};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use hkdf::Hkdf;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use sha2::Sha256;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
#[derive(Clone)]
pub struct DeviceIdentity {
pub device_id: [u8; 16],
pub public_key: [u8; 32],
signing_key: SigningKey,
x25519_secret: StaticSecret,
x25519_public: X25519PublicKey,
}
impl DeviceIdentity {
pub fn generate() -> Self {
let mut rng = rand::thread_rng();
let mut seed = [0u8; 32];
rng.fill_bytes(&mut seed);
Self::from_seed(seed)
}
pub fn from_seed(seed: [u8; 32]) -> Self {
let signing_key = SigningKey::from_bytes(&seed);
let verifying_key = signing_key.verifying_key();
let public_key = verifying_key.to_bytes();
let mut device_id = [0u8; 16];
device_id.copy_from_slice(&blake3::hash(&public_key).as_bytes()[0..16]);
let hk = Hkdf::<Sha256>::new(None, &seed);
let mut x25519_seed = [0u8; 32];
hk.expand(b"cp-x25519-key", &mut x25519_seed)
.expect("HKDF expand failed");
let x25519_secret = StaticSecret::from(x25519_seed);
let x25519_public = X25519PublicKey::from(&x25519_secret);
Self {
device_id,
public_key,
signing_key,
x25519_secret,
x25519_public,
}
}
pub fn sign(&self, data: &[u8]) -> [u8; 64] {
let signature = self.signing_key.sign(data);
signature.to_bytes()
}
pub fn x25519_public_key(&self) -> [u8; 32] {
self.x25519_public.to_bytes()
}
pub fn agree(&self, remote_x25519_public: &[u8; 32]) -> Result<[u8; 32]> {
let remote_key = X25519PublicKey::from(*remote_x25519_public);
let shared_secret = self.x25519_secret.diffie_hellman(&remote_key);
let bytes = *shared_secret.as_bytes();
if bytes == [0u8; 32] {
return Err(CPError::Crypto(
"X25519 DH produced all-zero shared secret".into(),
));
}
Ok(bytes)
}
pub fn pair_with(
&self,
remote_public_key: &[u8; 32],
remote_x25519_public: &[u8; 32],
) -> Result<PairedDevice> {
let mut remote_device_id = [0u8; 16];
remote_device_id.copy_from_slice(&blake3::hash(remote_public_key).as_bytes()[0..16]);
let shared_secret = self.agree(remote_x25519_public)?;
let (id_a, id_b) = if self.device_id < remote_device_id {
(self.device_id, remote_device_id)
} else {
(remote_device_id, self.device_id)
};
let mut info = Vec::with_capacity(32);
info.extend_from_slice(&id_a);
info.extend_from_slice(&id_b);
let hk = Hkdf::<Sha256>::new(None, &shared_secret);
let mut encryption_key = [0u8; 32];
hk.expand(&info, &mut encryption_key)
.map_err(|_| CPError::Crypto("HKDF expand failed".into()))?;
Ok(PairedDevice {
device_id: remote_device_id,
public_key: *remote_public_key,
x25519_public_key: *remote_x25519_public,
encryption_key,
last_synced_seq: 0,
})
}
pub fn export_seed(&self) -> [u8; 32] {
self.signing_key.to_bytes()
}
}
impl std::fmt::Debug for DeviceIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeviceIdentity")
.field("device_id", &hex::encode(self.device_id))
.field("public_key", &hex::encode(self.public_key))
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairedDevice {
pub device_id: [u8; 16],
pub public_key: [u8; 32],
pub x25519_public_key: [u8; 32],
pub encryption_key: [u8; 32],
pub last_synced_seq: u64,
}
impl PairedDevice {
pub fn update_last_synced(&mut self, seq: u64) {
self.last_synced_seq = seq;
}
pub fn device_id_hex(&self) -> String {
hex::encode(self.device_id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairingRequest {
pub device_id: [u8; 16],
pub public_key: [u8; 32],
pub x25519_public_key: [u8; 32],
pub device_name: Option<String>,
}
impl PairingRequest {
pub fn from_identity(identity: &DeviceIdentity, device_name: Option<String>) -> Self {
Self {
device_id: identity.device_id,
public_key: identity.public_key,
x25519_public_key: identity.x25519_public_key(),
device_name,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairingConfirmation {
pub request: PairingRequest,
#[serde(with = "BigArray")]
pub signature: [u8; 64],
pub confirmer_public_key: [u8; 32],
}
impl PairingConfirmation {
pub fn create(identity: &DeviceIdentity, request: &PairingRequest) -> Self {
let mut data = Vec::new();
data.extend_from_slice(&request.device_id);
data.extend_from_slice(&request.public_key);
data.extend_from_slice(&request.x25519_public_key);
let signature = identity.sign(&data);
Self {
request: request.clone(),
signature,
confirmer_public_key: identity.public_key,
}
}
pub fn verify(&self) -> Result<()> {
let verifying_key = VerifyingKey::from_bytes(&self.confirmer_public_key)
.map_err(|e| CPError::Crypto(format!("Invalid public key: {e}")))?;
let mut data = Vec::new();
data.extend_from_slice(&self.request.device_id);
data.extend_from_slice(&self.request.public_key);
data.extend_from_slice(&self.request.x25519_public_key);
let signature = Signature::from_bytes(&self.signature);
verifying_key
.verify_strict(&data, &signature)
.map_err(|_| CPError::Verification("Invalid pairing confirmation signature".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signature, VerifyingKey};
#[test]
fn test_device_identity_generation() {
let id1 = DeviceIdentity::generate();
let id2 = DeviceIdentity::generate();
assert_ne!(id1.device_id, id2.device_id);
assert_ne!(id1.public_key, id2.public_key);
}
#[test]
fn test_device_identity_from_seed() {
let seed = [42u8; 32];
let id1 = DeviceIdentity::from_seed(seed);
let id2 = DeviceIdentity::from_seed(seed);
assert_eq!(id1.device_id, id2.device_id);
assert_eq!(id1.public_key, id2.public_key);
}
#[test]
fn test_device_pairing_symmetric() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let alice_view_of_bob = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
let bob_view_of_alice = bob
.pair_with(&alice.public_key, &alice.x25519_public_key())
.unwrap();
assert_eq!(
alice_view_of_bob.encryption_key,
bob_view_of_alice.encryption_key
);
}
#[test]
fn test_signing_and_verification() {
let identity = DeviceIdentity::generate();
let data = b"test message";
let signature = identity.sign(data);
let verifying_key = VerifyingKey::from_bytes(&identity.public_key).unwrap();
let sig = Signature::from_bytes(&signature);
assert!(verifying_key.verify_strict(data, &sig).is_ok());
}
#[test]
fn test_pairing_request_and_confirmation() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let request = PairingRequest::from_identity(&alice, Some("Alice's Phone".into()));
let confirmation = PairingConfirmation::create(&bob, &request);
assert!(confirmation.verify().is_ok());
}
#[test]
fn test_identity_generate() {
let identity = DeviceIdentity::generate();
assert_ne!(identity.device_id, [0u8; 16]);
assert_ne!(identity.public_key, [0u8; 32]);
let x25519_pub = identity.x25519_public_key();
assert_ne!(x25519_pub, [0u8; 32]);
}
#[test]
fn test_identity_public_key_derivation() {
let seed = [1u8; 32];
let identity = DeviceIdentity::from_seed(seed);
let verifying_key = VerifyingKey::from_bytes(&identity.public_key);
assert!(verifying_key.is_ok());
let derived_pubkey = verifying_key.unwrap().to_bytes();
assert_eq!(identity.public_key, derived_pubkey);
}
#[test]
fn test_identity_device_id_derivation() {
let identity = DeviceIdentity::generate();
let expected_device_id: [u8; 16] = blake3::hash(&identity.public_key).as_bytes()[0..16]
.try_into()
.unwrap();
assert_eq!(identity.device_id, expected_device_id);
}
#[test]
fn test_identity_serialization() {
let identity = DeviceIdentity::generate();
let paired = identity.pair_with(&[2u8; 32], &[3u8; 32]).unwrap();
let mut serialized = Vec::new();
ciborium::ser::into_writer(&paired, &mut serialized).unwrap();
assert!(!serialized.is_empty());
let deserialized: PairedDevice = ciborium::de::from_reader(serialized.as_slice()).unwrap();
assert_eq!(paired.device_id, deserialized.device_id);
assert_eq!(paired.public_key, deserialized.public_key);
}
#[test]
fn test_identity_persistence() {
let seed = [7u8; 32];
let original = DeviceIdentity::from_seed(seed);
let restored = DeviceIdentity::from_seed(seed);
assert_eq!(original.device_id, restored.device_id);
assert_eq!(original.public_key, restored.public_key);
assert_eq!(original.export_seed(), restored.export_seed());
}
#[test]
fn test_identity_pairing_x25519() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let alice_x25519 = alice.x25519_public_key();
let bob_x25519 = bob.x25519_public_key();
assert_eq!(alice_x25519.len(), 32);
assert_eq!(bob_x25519.len(), 32);
let shared_alice = alice.agree(&bob_x25519).unwrap();
let shared_bob = bob.agree(&alice_x25519).unwrap();
assert_eq!(shared_alice, shared_bob);
}
#[test]
fn test_identity_shared_key_derivation() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let alice_paired = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
let bob_paired = bob
.pair_with(&alice.public_key, &alice.x25519_public_key())
.unwrap();
assert_eq!(alice_paired.encryption_key, bob_paired.encryption_key);
let direct_shared = alice.agree(&bob.x25519_public_key()).unwrap();
assert_ne!(alice_paired.encryption_key, direct_shared);
}
#[test]
fn test_identity_unpairing() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let paired = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
assert_eq!(paired.device_id, bob.device_id);
let charlie = DeviceIdentity::generate();
let paired_charlie = alice
.pair_with(&charlie.public_key, &charlie.x25519_public_key())
.unwrap();
assert_ne!(paired.encryption_key, paired_charlie.encryption_key);
}
#[test]
fn test_identity_pairing_device_id_computation() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let paired = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
assert_eq!(paired.device_id, bob.device_id);
assert_eq!(paired.public_key, bob.public_key);
assert_eq!(paired.x25519_public_key, bob.x25519_public_key());
}
#[test]
fn test_identity_pairing_order_independence() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let paired_ab = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
let paired_ba = bob
.pair_with(&alice.public_key, &alice.x25519_public_key())
.unwrap();
assert_eq!(paired_ab.encryption_key, paired_ba.encryption_key);
}
#[test]
fn test_identity_sign_deterministic() {
let identity = DeviceIdentity::from_seed([5u8; 32]);
let data = b"test data";
let sig1 = identity.sign(data);
let sig2 = identity.sign(data);
assert_eq!(sig1, sig2);
}
#[test]
fn test_identity_sign_different_data() {
let identity = DeviceIdentity::generate();
let data1 = b"data one";
let data2 = b"data two";
let sig1 = identity.sign(data1);
let sig2 = identity.sign(data2);
assert_ne!(sig1, sig2);
}
#[test]
fn test_identity_x25519_public_key_format() {
let identity = DeviceIdentity::generate();
let pubkey = identity.x25519_public_key();
assert_eq!(pubkey.len(), 32);
assert!(pubkey[31] != 0 || pubkey.iter().all(|&b| b == 0)); }
#[test]
fn test_identity_agree_invalid_key() {
let identity = DeviceIdentity::generate();
let invalid_key = [0u8; 32];
let result = identity.agree(&invalid_key);
assert!(result.is_err(), "All-zero key should produce an error");
}
#[test]
fn test_pairing_request_from_identity() {
let identity = DeviceIdentity::generate();
let request_no_name = PairingRequest::from_identity(&identity, None);
assert_eq!(request_no_name.device_id, identity.device_id);
assert_eq!(request_no_name.public_key, identity.public_key);
assert_eq!(
request_no_name.x25519_public_key,
identity.x25519_public_key()
);
assert!(request_no_name.device_name.is_none());
let request_with_name =
PairingRequest::from_identity(&identity, Some("Test Device".to_string()));
assert_eq!(
request_with_name.device_name,
Some("Test Device".to_string())
);
}
#[test]
fn test_pairing_confirmation_verify_fails_wrong_key() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let _charlie = DeviceIdentity::generate();
let request = PairingRequest::from_identity(&alice, None);
let confirmation = PairingConfirmation::create(&bob, &request);
let mut modified_request = request.clone();
modified_request.device_name = Some("Modified".to_string());
let _confirmation_wrong = PairingConfirmation::create(&bob, &modified_request);
assert!(confirmation.verify().is_ok());
}
#[test]
fn test_paired_device_update_last_synced() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let mut paired = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
assert_eq!(paired.last_synced_seq, 0);
paired.update_last_synced(10);
assert_eq!(paired.last_synced_seq, 10);
paired.update_last_synced(20);
assert_eq!(paired.last_synced_seq, 20);
}
#[test]
fn test_paired_device_device_id_hex() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let paired = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
let hex = paired.device_id_hex();
assert_eq!(hex.len(), 32);
assert_eq!(hex, hex::encode(paired.device_id));
}
#[test]
fn test_identity_export_seed() {
let identity = DeviceIdentity::generate();
let seed = identity.export_seed();
assert_eq!(seed.len(), 32);
let recreated = DeviceIdentity::from_seed(seed);
assert_eq!(identity.device_id, recreated.device_id);
assert_eq!(identity.public_key, recreated.public_key);
}
#[test]
fn test_identity_debug_format() {
let identity = DeviceIdentity::generate();
let debug_str = format!("{identity:?}");
assert!(debug_str.contains("DeviceIdentity"));
}
#[test]
fn test_identity_pairing_with_different_seeds() {
let seed_a = [1u8; 32];
let seed_b = [2u8; 32];
let alice = DeviceIdentity::from_seed(seed_a);
let bob = DeviceIdentity::from_seed(seed_b);
let paired = alice
.pair_with(&bob.public_key, &bob.x25519_public_key())
.unwrap();
assert_ne!(paired.encryption_key, [0u8; 32]);
}
#[test]
fn test_confirmation_serialization() {
let alice = DeviceIdentity::generate();
let bob = DeviceIdentity::generate();
let request = PairingRequest::from_identity(&alice, None);
let confirmation = PairingConfirmation::create(&bob, &request);
let serialized = serde_json::to_vec(&confirmation).unwrap();
assert!(!serialized.is_empty());
let deserialized: PairingConfirmation = serde_json::from_slice(&serialized).unwrap();
assert_eq!(
confirmation.request.device_id,
deserialized.request.device_id
);
assert_eq!(confirmation.signature, deserialized.signature);
assert_eq!(
confirmation.confirmer_public_key,
deserialized.confirmer_public_key
);
}
}