every.channel: sanitized baseline
This commit is contained in:
commit
897e556bea
258 changed files with 74298 additions and 0 deletions
12
crates/ec-crypto/Cargo.toml
Normal file
12
crates/ec-crypto/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "ec-crypto"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
blake3 = "1"
|
||||
chacha20poly1305 = "0.10"
|
||||
ed25519-dalek = { version = "2", features = ["pkcs8"] }
|
||||
hex = "0.4"
|
||||
ec-core = { path = "../ec-core" }
|
||||
227
crates/ec-crypto/src/lib.rs
Normal file
227
crates/ec-crypto/src/lib.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
//! Cryptographic helpers for every.channel.
|
||||
|
||||
use chacha20poly1305::{aead::Aead, KeyInit, XChaCha20Poly1305, XNonce};
|
||||
use ec_core::ManifestSignature;
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
pub const MANIFEST_SIG_ALG: &str = "ed25519";
|
||||
|
||||
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<u8>,
|
||||
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<Vec<u8>> {
|
||||
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,
|
||||
}
|
||||
|
||||
pub fn load_manifest_keypair_from_env() -> Result<Option<ManifestKeypair>, 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 trimmed = value.trim();
|
||||
let key_bytes = if std::path::Path::new(trimmed).exists() {
|
||||
let text = fs::read_to_string(trimmed).map_err(|err| err.to_string())?;
|
||||
hex::decode(text.trim()).map_err(|err| err.to_string())?
|
||||
} else {
|
||||
hex::decode(trimmed).map_err(|err| err.to_string())?
|
||||
};
|
||||
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 signer_id_from_key(key: &VerifyingKey) -> String {
|
||||
format!("ed25519:{}", hex::encode(key.to_bytes()))
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
|
||||
#[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 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue