//! Cryptographic helpers for every.channel. use chacha20poly1305::{aead::Aead, KeyInit, XChaCha20Poly1305, XNonce}; use ec_core::{ManifestBody, ManifestSignature}; use ec_eth::{manifest_body_eip712_signing_hash, ETH_MANIFEST_SIG_ALG}; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use k256::ecdsa::{ RecoveryId as SecpRecoveryId, Signature as SecpSignature, SigningKey as SecpSigningKey, VerifyingKey as SecpVerifyingKey, }; use sha3::{Digest, Keccak256}; use std::env; use std::fs; pub const MANIFEST_SIG_ALG: &str = "ed25519"; pub const ETH_MANIFEST_SIGNING_KEY_ENV: &str = "EVERY_CHANNEL_ETH_MANIFEST_SIGNING_KEY"; pub const ENCRYPTION_ALG: &str = "xchacha20poly1305"; /// Derive a stream encryption key from a stream id and optional network secret. /// /// This is deterministic: identical stream ids produce identical keys. pub fn derive_stream_key(stream_id: &str, network_secret: Option<&[u8]>) -> [u8; 32] { let mut input = Vec::new(); if let Some(secret) = network_secret { input.extend_from_slice(secret); input.push(0); } input.extend_from_slice(stream_id.as_bytes()); blake3::derive_key("every.channel stream key v1", &input) } /// Derive a deterministic nonce for a stream chunk. pub fn derive_stream_nonce(stream_id: &str, chunk_index: u64) -> [u8; 24] { let mut hasher = blake3::Hasher::new(); hasher.update(b"every.channel stream nonce v1"); hasher.update(stream_id.as_bytes()); hasher.update(&chunk_index.to_be_bytes()); let hash = hasher.finalize(); let mut nonce = [0u8; 24]; nonce.copy_from_slice(&hash.as_bytes()[..24]); nonce } #[derive(Debug, Clone)] pub struct EncryptedPayload { pub ciphertext: Vec, pub nonce: [u8; 24], pub alg: &'static str, } pub fn encrypt_stream_data( stream_id: &str, chunk_index: u64, plaintext: &[u8], network_secret: Option<&[u8]>, ) -> EncryptedPayload { let key_bytes = derive_stream_key(stream_id, network_secret); let cipher = XChaCha20Poly1305::new_from_slice(&key_bytes).expect("key size"); let nonce_bytes = derive_stream_nonce(stream_id, chunk_index); let nonce = XNonce::from_slice(&nonce_bytes); let ciphertext = cipher .encrypt(nonce, plaintext) .expect("encryption failure"); EncryptedPayload { ciphertext, nonce: nonce_bytes, alg: ENCRYPTION_ALG, } } pub fn decrypt_stream_data( stream_id: &str, chunk_index: u64, ciphertext: &[u8], network_secret: Option<&[u8]>, ) -> Option> { let key_bytes = derive_stream_key(stream_id, network_secret); let cipher = XChaCha20Poly1305::new_from_slice(&key_bytes).expect("key size"); let nonce_bytes = derive_stream_nonce(stream_id, chunk_index); let nonce = XNonce::from_slice(&nonce_bytes); cipher.decrypt(nonce, ciphertext).ok() } #[derive(Debug, Clone)] pub struct ManifestKeypair { pub signing_key: SigningKey, pub verifying_key: VerifyingKey, } #[derive(Debug, Clone)] pub struct EthereumManifestKeypair { pub signing_key: SecpSigningKey, pub verifying_key: SecpVerifyingKey, } fn decode_env_hex_or_file(value: &str) -> Result, String> { let trimmed = value.trim(); if std::path::Path::new(trimmed).exists() { let text = fs::read_to_string(trimmed).map_err(|err| err.to_string())?; hex::decode(text.trim().trim_start_matches("0x")).map_err(|err| err.to_string()) } else { hex::decode(trimmed.trim_start_matches("0x")).map_err(|err| err.to_string()) } } pub fn load_manifest_keypair_from_env() -> Result, String> { let value = match env::var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY") { Ok(value) => value, Err(env::VarError::NotPresent) => return Ok(None), Err(err) => return Err(err.to_string()), }; let key_bytes = decode_env_hex_or_file(&value)?; let bytes = if key_bytes.len() == 32 { key_bytes } else if key_bytes.len() == 64 { key_bytes[..32].to_vec() } else { return Err("manifest signing key must be 32 or 64 hex bytes".to_string()); }; let mut secret = [0u8; 32]; secret.copy_from_slice(&bytes[..32]); let signing_key = SigningKey::from_bytes(&secret); let verifying_key = signing_key.verifying_key(); Ok(Some(ManifestKeypair { signing_key, verifying_key, })) } pub fn load_ethereum_manifest_keypair_from_env() -> Result, String> { let value = match env::var(ETH_MANIFEST_SIGNING_KEY_ENV) { Ok(value) => value, Err(env::VarError::NotPresent) => return Ok(None), Err(err) => return Err(err.to_string()), }; let key_bytes = decode_env_hex_or_file(&value)?; if key_bytes.len() != 32 { return Err("ethereum manifest signing key must be exactly 32 hex bytes".to_string()); } let signing_key = SecpSigningKey::from_bytes((&key_bytes[..]).into()).map_err(|err| err.to_string())?; let verifying_key = *signing_key.verifying_key(); Ok(Some(EthereumManifestKeypair { signing_key, verifying_key, })) } pub fn signer_id_from_key(key: &VerifyingKey) -> String { format!("ed25519:{}", hex::encode(key.to_bytes())) } pub fn ethereum_signer_id_from_key(key: &SecpVerifyingKey) -> String { let encoded = key.to_encoded_point(false); let digest = Keccak256::digest(&encoded.as_bytes()[1..]); format!("eth:0x{}", hex::encode(&digest[12..])) } pub fn sign_manifest_id(manifest_id: &str, keypair: &ManifestKeypair) -> ManifestSignature { let signature: Signature = keypair.signing_key.sign(manifest_id.as_bytes()); ManifestSignature { signer_id: signer_id_from_key(&keypair.verifying_key), alg: MANIFEST_SIG_ALG.to_string(), signature: hex::encode(signature.to_bytes()), } } pub fn sign_manifest_body_eip712( body: &ManifestBody, keypair: &EthereumManifestKeypair, ) -> Result { let digest = manifest_body_eip712_signing_hash(body).map_err(|err| err.to_string())?; let (signature, recovery_id) = keypair .signing_key .sign_prehash_recoverable(digest.as_slice()) .map_err(|err| err.to_string())?; let mut bytes = Vec::with_capacity(65); bytes.extend_from_slice(&signature.to_bytes()); bytes.push(recovery_id.to_byte()); Ok(ManifestSignature { signer_id: ethereum_signer_id_from_key(&keypair.verifying_key), alg: ETH_MANIFEST_SIG_ALG.to_string(), signature: hex::encode(bytes), }) } pub fn verify_manifest_signature(manifest_id: &str, sig: &ManifestSignature) -> bool { if sig.alg != MANIFEST_SIG_ALG { return false; } let signer_id = sig .signer_id .strip_prefix("ed25519:") .unwrap_or(&sig.signer_id); let Ok(pk_bytes) = hex::decode(signer_id) else { return false; }; if pk_bytes.len() != 32 { return false; } let mut pk = [0u8; 32]; pk.copy_from_slice(&pk_bytes); let Ok(verifying_key) = VerifyingKey::from_bytes(&pk) else { return false; }; let Ok(sig_bytes) = hex::decode(&sig.signature) else { return false; }; let Ok(signature) = Signature::from_slice(&sig_bytes) else { return false; }; verifying_key .verify(manifest_id.as_bytes(), &signature) .is_ok() } fn normalize_eth_signer_id(value: &str) -> Option { let trimmed = value .strip_prefix("eth:") .or_else(|| value.strip_prefix("ETH:")) .unwrap_or(value) .trim(); let address = trimmed.trim_start_matches("0x"); if address.len() != 40 || !address.chars().all(|c| c.is_ascii_hexdigit()) { return None; } Some(format!("eth:0x{}", address.to_ascii_lowercase())) } pub fn verify_manifest_body_eip712_signature(body: &ManifestBody, sig: &ManifestSignature) -> bool { if sig.alg != ETH_MANIFEST_SIG_ALG { return false; } let Some(expected_signer_id) = normalize_eth_signer_id(&sig.signer_id) else { return false; }; let Ok(sig_bytes) = hex::decode(sig.signature.trim().trim_start_matches("0x")) else { return false; }; if sig_bytes.len() != 65 { return false; } let Ok(signature) = SecpSignature::from_slice(&sig_bytes[..64]) else { return false; }; let Ok(recovery_id) = SecpRecoveryId::try_from(sig_bytes[64]) else { return false; }; let Ok(digest) = manifest_body_eip712_signing_hash(body) else { return false; }; let Ok(verifying_key) = SecpVerifyingKey::recover_from_prehash(digest.as_slice(), &signature, recovery_id) else { return false; }; ethereum_signer_id_from_key(&verifying_key) == expected_signer_id } pub fn verify_manifest_signature_with_body( manifest_id: &str, body: &ManifestBody, sig: &ManifestSignature, ) -> bool { verify_manifest_signature(manifest_id, sig) || verify_manifest_body_eip712_signature(body, sig) } #[cfg(test)] mod tests { use super::*; #[test] fn stream_key_is_deterministic_and_secret_sensitive() { let k1 = derive_stream_key("s1", None); let k2 = derive_stream_key("s1", None); assert_eq!(k1, k2); let k3 = derive_stream_key("s2", None); assert_ne!(k1, k3); let secret = [7u8; 32]; let ks1 = derive_stream_key("s1", Some(&secret)); assert_ne!(k1, ks1); let ks2 = derive_stream_key("s1", Some(&secret)); assert_eq!(ks1, ks2); } #[test] fn nonce_changes_per_chunk_index() { let n1 = derive_stream_nonce("s", 1); let n2 = derive_stream_nonce("s", 2); assert_ne!(n1, n2); } #[test] fn encrypt_decrypt_roundtrip() { let plaintext = b"hello world"; let enc = encrypt_stream_data("s", 42, plaintext, None); assert_ne!(enc.ciphertext, plaintext); let out = decrypt_stream_data("s", 42, &enc.ciphertext, None).unwrap(); assert_eq!(out, plaintext); } #[test] fn decrypt_fails_with_wrong_index() { let plaintext = b"hello world"; let enc = encrypt_stream_data("s", 42, plaintext, None); assert!(decrypt_stream_data("s", 43, &enc.ciphertext, None).is_none()); } #[test] fn manifest_sign_verify_roundtrip() { let secret = [1u8; 32]; let signing_key = SigningKey::from_bytes(&secret); let verifying_key = signing_key.verifying_key(); let keypair = ManifestKeypair { signing_key, verifying_key, }; let sig = sign_manifest_id("m", &keypair); assert!(verify_manifest_signature("m", &sig)); assert!(!verify_manifest_signature("evil", &sig)); } #[test] fn ethereum_manifest_sign_verify_roundtrip() { let body = ManifestBody { stream_id: ec_core::StreamId("stream".to_string()), epoch_id: "epoch-1".to_string(), chunk_duration_ms: 2000, total_chunks: 1, chunk_start_index: 0, encoder_profile_id: "p".to_string(), merkle_root: "11".repeat(32), created_unix_ms: 1, metadata: Vec::new(), chunk_hashes: vec!["22".repeat(32)], variants: None, }; let secret = [7u8; 32]; let signing_key = SecpSigningKey::from_bytes((&secret).into()).unwrap(); let verifying_key = *signing_key.verifying_key(); let keypair = EthereumManifestKeypair { signing_key, verifying_key, }; let sig = sign_manifest_body_eip712(&body, &keypair).unwrap(); assert!(verify_manifest_body_eip712_signature(&body, &sig)); let mut tampered = body.clone(); tampered.created_unix_ms = 2; assert!(!verify_manifest_body_eip712_signature(&tampered, &sig)); } #[test] fn load_keypair_from_env_hex() { let prev = env::var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY").ok(); env::set_var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY", "00".repeat(32)); let loaded = load_manifest_keypair_from_env().unwrap().unwrap(); let id = signer_id_from_key(&loaded.verifying_key); assert!(id.starts_with("ed25519:")); match prev { Some(value) => env::set_var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY", value), None => env::remove_var("EVERY_CHANNEL_MANIFEST_SIGNING_KEY"), } } #[test] fn load_ethereum_keypair_from_env_hex() { let prev = env::var(ETH_MANIFEST_SIGNING_KEY_ENV).ok(); env::set_var(ETH_MANIFEST_SIGNING_KEY_ENV, "01".repeat(32)); let loaded = load_ethereum_manifest_keypair_from_env().unwrap().unwrap(); let id = ethereum_signer_id_from_key(&loaded.verifying_key); assert!(id.starts_with("eth:0x")); match prev { Some(value) => env::set_var(ETH_MANIFEST_SIGNING_KEY_ENV, value), None => env::remove_var(ETH_MANIFEST_SIGNING_KEY_ENV), } } }