diff --git a/Cargo.lock b/Cargo.lock index 7f62cebf..bd891801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4186,6 +4186,7 @@ dependencies = [ "capnp", "capnpc", "cfg-if 1.0.0", + "chacha20", "chacha20poly1305", "chrono", "config 0.12.0", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index a260be3a..69d00223 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -25,6 +25,7 @@ hex = "^0" generic-array = "^0" secrecy = "^0" chacha20poly1305 = "^0" +chacha20 = "^0" hashlink = { path = "../external/hashlink", features = ["serde_impl"] } serde-big-array = "^0" futures-util = { version = "^0", default_features = false, features = ["alloc"] } diff --git a/veilid-core/src/dht/crypto.rs b/veilid-core/src/dht/crypto.rs index 4b444326..8d0aa987 100644 --- a/veilid-core/src/dht/crypto.rs +++ b/veilid-core/src/dht/crypto.rs @@ -2,6 +2,8 @@ use super::key::*; use crate::intf::*; use crate::xx::*; use crate::*; +use chacha20::cipher::{NewCipher, StreamCipher}; +use chacha20::XChaCha20; use chacha20poly1305 as ch; use chacha20poly1305::aead::{AeadInPlace, NewAead}; use core::convert::TryInto; @@ -16,7 +18,7 @@ pub type SharedSecret = [u8; 32]; pub type Nonce = [u8; 24]; const DH_CACHE_SIZE: usize = 1024; -pub const ENCRYPTION_OVERHEAD: usize = 16; +pub const AEAD_OVERHEAD: usize = 16; #[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] struct DHCacheKey { @@ -222,7 +224,7 @@ impl Crypto { s } - pub fn decrypt_in_place( + pub fn decrypt_in_place_aead( body: &mut Vec, nonce: &Nonce, shared_secret: &SharedSecret, @@ -236,20 +238,20 @@ impl Crypto { .map_err(logthru_crypto!()) } - pub fn decrypt( + pub fn decrypt_aead( body: &[u8], nonce: &Nonce, shared_secret: &SharedSecret, associated_data: Option<&[u8]>, ) -> Result, String> { let mut out = body.to_vec(); - Self::decrypt_in_place(&mut out, nonce, shared_secret, associated_data) + Self::decrypt_in_place_aead(&mut out, nonce, shared_secret, associated_data) .map_err(map_to_string) .map_err(logthru_crypto!())?; Ok(out) } - pub fn encrypt_in_place( + pub fn encrypt_in_place_aead( body: &mut Vec, nonce: &Nonce, shared_secret: &SharedSecret, @@ -264,16 +266,27 @@ impl Crypto { .map_err(logthru_crypto!()) } - pub fn encrypt( + pub fn encrypt_aead( body: &[u8], nonce: &Nonce, shared_secret: &SharedSecret, associated_data: Option<&[u8]>, ) -> Result, String> { let mut out = body.to_vec(); - Self::encrypt_in_place(&mut out, nonce, shared_secret, associated_data) + Self::encrypt_in_place_aead(&mut out, nonce, shared_secret, associated_data) .map_err(map_to_string) .map_err(logthru_crypto!())?; Ok(out) } + + pub fn crypt_in_place_no_auth(body: &mut Vec, nonce: &Nonce, shared_secret: &SharedSecret) { + let mut cipher = XChaCha20::new(shared_secret.into(), nonce.into()); + cipher.apply_keystream(body); + } + + pub fn crypt_no_auth(body: &[u8], nonce: &Nonce, shared_secret: &SharedSecret) -> Vec { + let mut out = body.to_vec(); + Self::crypt_in_place_no_auth(&mut out, nonce, shared_secret); + out + } } diff --git a/veilid-core/src/dht/envelope.rs b/veilid-core/src/dht/envelope.rs index 6a838cff..3b6ae51d 100644 --- a/veilid-core/src/dht/envelope.rs +++ b/veilid-core/src/dht/envelope.rs @@ -29,14 +29,13 @@ use core::convert::TryInto; // sender_id: [u8; 32], // 0x2A: Node ID of the message source, which is the Ed25519 public key of the sender (must be verified with find_node if this is a new node_id/address combination) // recipient_id: [u8; 32], // 0x4A: Node ID of the intended recipient, which is the Ed25519 public key of the recipient (must be the receiving node, or a relay lease holder) // // 0x6A: message is appended (operations) -// // encrypted by XChaCha20Poly1305(nonce,x25519(recipient_id, sender_secret_key)) -// // decryptable by XChaCha20Poly1305(nonce,x25519(sender_id, recipient_secret_key)) -// // entire header needs to be included in message digest, relays are not allowed to modify the envelope without invalidating the signature. +// // encrypted by XChaCha20Poly1305(nonce,x25519(recipient_id, sender_secret_key)) +// signature: [u8; 64], // 0x?? (end-0x40): Ed25519 signature of the entire envelope including header is appended to the packet +// // entire header needs to be included in message digest, relays are not allowed to modify the envelope without invalidating the signature. // } pub const MAX_ENVELOPE_SIZE: usize = 65507; -pub const MIN_ENVELOPE_SIZE: usize = 106; -pub const AEAD_ADDITIONAL_SIZE: usize = 16; +pub const MIN_ENVELOPE_SIZE: usize = 0x6A + 0x40; // Header + Signature pub const ENVELOPE_MAGIC: &[u8; 4] = b"VLID"; pub const MIN_VERSION: u8 = 0u8; pub const MAX_VERSION: u8 = 0u8; @@ -77,7 +76,7 @@ impl Envelope { } } - pub fn from_data(data: &[u8]) -> Result { + pub fn from_signed_data(data: &[u8]) -> Result { // Ensure we are at least the length of the envelope if data.len() < MIN_ENVELOPE_SIZE { trace!("envelope too small: len={}", data.len()); @@ -155,6 +154,12 @@ impl Envelope { return Err(()); } + // Get signature + let signature = DHTSignature::new(data[(data.len() - 64)..].try_into().map_err(drop)?); + + // Validate signature + verify(&sender_id, &data[0..(data.len() - 64)], &signature).map_err(drop)?; + // Return envelope Ok(Self { version, @@ -176,13 +181,8 @@ impl Envelope { // Get DH secret let dh_secret = crypto.cached_dh(&self.sender_id, node_id_secret)?; - // Decrypt message and authenticate, including the envelope header as associated data to authenticate - let body = Crypto::decrypt( - &data[0x6A..], - &self.nonce, - &dh_secret, - Some(&data[0..MIN_ENVELOPE_SIZE]), - )?; + // Decrypt message without authentication + let body = Crypto::crypt_no_auth(&data[0x6A..data.len() - 64], &self.nonce, &dh_secret); Ok(body) } @@ -203,7 +203,7 @@ impl Envelope { } // Ensure body isn't too long - let envelope_size: usize = body.len() + MIN_ENVELOPE_SIZE + AEAD_ADDITIONAL_SIZE; + let envelope_size: usize = body.len() + MIN_ENVELOPE_SIZE; if envelope_size > MAX_ENVELOPE_SIZE { return Err(()); } @@ -234,11 +234,23 @@ impl Envelope { .map_err(drop)?; // Encrypt and authenticate message - let encrypted_body = - Crypto::encrypt(body, &self.nonce, &dh_secret, Some(&data[0..0x6A])).map_err(drop)?; + let encrypted_body = Crypto::crypt_no_auth(body, &self.nonce, &dh_secret); // Write body - data[0x6A..].copy_from_slice(encrypted_body.as_slice()); + if !encrypted_body.is_empty() { + data[0x6A..envelope_size - 64].copy_from_slice(encrypted_body.as_slice()); + } + + // Sign the envelope + let signature = sign( + &self.sender_id, + node_id_secret, + &data[0..(envelope_size - 64)], + ) + .map_err(drop)?; + + // Append the signature + data[(envelope_size - 64)..].copy_from_slice(&signature.bytes); Ok(data) } diff --git a/veilid-core/src/network_manager.rs b/veilid-core/src/network_manager.rs index f0c9fde7..b134b288 100644 --- a/veilid-core/src/network_manager.rs +++ b/veilid-core/src/network_manager.rs @@ -541,7 +541,7 @@ impl NetworkManager { // Decode envelope header (may fail signature validation) let envelope = - Envelope::from_data(data).map_err(|_| "envelope failed to decode".to_owned())?; + Envelope::from_signed_data(data).map_err(|_| "envelope failed to decode".to_owned())?; // Get routing table and rpc processor let (routing_table, rpc) = { diff --git a/veilid-core/src/rpc_processor/private_route.rs b/veilid-core/src/rpc_processor/private_route.rs index cbc9a7ab..0e2c7042 100644 --- a/veilid-core/src/rpc_processor/private_route.rs +++ b/veilid-core/src/rpc_processor/private_route.rs @@ -93,7 +93,7 @@ impl RPCProcessor { ) .map_err(map_error_internal!("dh failed"))?; let enc_msg_data = - Crypto::encrypt(blob_data.as_slice(), &nonce, &dh_secret, None) + Crypto::encrypt_aead(blob_data.as_slice(), &nonce, &dh_secret, None) .map_err(map_error_internal!("encryption failed"))?; rhd_builder.set_blob(enc_msg_data.as_slice()); @@ -118,7 +118,7 @@ impl RPCProcessor { &safety_route.secret_key, ) .map_err(map_error_internal!("dh failed"))?; - let enc_msg_data = Crypto::encrypt(blob_data.as_slice(), &nonce, &dh_secret, None) + let enc_msg_data = Crypto::encrypt_aead(blob_data.as_slice(), &nonce, &dh_secret, None) .map_err(map_error_internal!("encryption failed"))?; first_rhd_builder.set_blob(enc_msg_data.as_slice()); @@ -148,7 +148,7 @@ impl RPCProcessor { .crypto .cached_dh(&pr_pk, &sr.secret_key) .map_err(map_error_internal!("dh failed"))?; - let enc_msg_data = Crypto::encrypt(&message_data, &nonce, &dh_secret, None) + let enc_msg_data = Crypto::encrypt_aead(&message_data, &nonce, &dh_secret, None) .map_err(map_error_internal!("encryption failed"))?; // Prepare route operation diff --git a/veilid-core/src/tests/common/test_crypto.rs b/veilid-core/src/tests/common/test_crypto.rs index 8e8079b7..c6f1544f 100644 --- a/veilid-core/src/tests/common/test_crypto.rs +++ b/veilid-core/src/tests/common/test_crypto.rs @@ -21,8 +21,8 @@ async fn shutdown(api: VeilidAPI) { trace!("test_table_store: finished"); } -pub async fn test_enc_dec() { - trace!("test_enc_dec"); +pub async fn test_aead() { + trace!("test_aead"); let n1 = Crypto::get_random_nonce(); let n2 = loop { @@ -44,52 +44,101 @@ pub async fn test_enc_dec() { let body2 = body.clone(); let size_before_encrypt = body.len(); assert!( - Crypto::encrypt_in_place(&mut body, &n1, &ss1, None).is_ok(), + Crypto::encrypt_in_place_aead(&mut body, &n1, &ss1, None).is_ok(), "encrypt should succeed" ); let size_after_encrypt = body.len(); assert!( - size_after_encrypt - size_before_encrypt == ENCRYPTION_OVERHEAD, + size_after_encrypt - size_before_encrypt == AEAD_OVERHEAD, "overhead should match" ); let mut body3 = body.clone(); let mut body4 = body.clone(); let mut body5 = body.clone(); assert!( - Crypto::decrypt_in_place(&mut body, &n1, &ss1, None).is_ok(), + Crypto::decrypt_in_place_aead(&mut body, &n1, &ss1, None).is_ok(), "decrypt should succeed" ); assert_eq!(body, body2, "results should be the same"); assert!( - Crypto::decrypt_in_place(&mut body3, &n2, &ss1, None).is_err(), + Crypto::decrypt_in_place_aead(&mut body3, &n2, &ss1, None).is_err(), "decrypt with wrong nonce should fail" ); assert_ne!(body3, body, "failure changes data"); assert!( - Crypto::decrypt_in_place(&mut body4, &n1, &ss2, None).is_err(), + Crypto::decrypt_in_place_aead(&mut body4, &n1, &ss2, None).is_err(), "decrypt with wrong secret should fail" ); assert_ne!(body4, body, "failure changes data"); assert!( - Crypto::decrypt_in_place(&mut body5, &n1, &ss2, Some(b"foobar")).is_err(), + Crypto::decrypt_in_place_aead(&mut body5, &n1, &ss2, Some(b"foobar")).is_err(), "decrypt with wrong associated data should fail" ); assert_ne!(body5, body, "failure changes data"); assert!( - Crypto::decrypt(LOREM_IPSUM, &n1, &ss1, None).is_err(), + Crypto::decrypt_aead(LOREM_IPSUM, &n1, &ss1, None).is_err(), "should fail authentication" ); - let body5 = Crypto::encrypt(LOREM_IPSUM, &n1, &ss1, None).unwrap(); - let body6 = Crypto::decrypt(&body5, &n1, &ss1, None).unwrap(); - let body7 = Crypto::encrypt(LOREM_IPSUM, &n1, &ss1, None).unwrap(); + let body5 = Crypto::encrypt_aead(LOREM_IPSUM, &n1, &ss1, None).unwrap(); + let body6 = Crypto::decrypt_aead(&body5, &n1, &ss1, None).unwrap(); + let body7 = Crypto::encrypt_aead(LOREM_IPSUM, &n1, &ss1, None).unwrap(); assert_eq!(body6, LOREM_IPSUM); assert_eq!(body5, body7); } + +pub async fn test_no_auth() { + trace!("test_no_auth"); + + let n1 = Crypto::get_random_nonce(); + let n2 = loop { + let n = Crypto::get_random_nonce(); + if n != n1 { + break n; + } + }; + + let ss1 = Crypto::get_random_secret(); + let ss2 = loop { + let ss = Crypto::get_random_secret(); + if ss != ss1 { + break ss; + } + }; + + let mut body = LOREM_IPSUM.to_vec(); + let body2 = body.clone(); + let size_before_encrypt = body.len(); + Crypto::crypt_in_place_no_auth(&mut body, &n1, &ss1); + + let size_after_encrypt = body.len(); + assert_eq!( + size_after_encrypt, size_before_encrypt, + "overhead should match" + ); + let mut body3 = body.clone(); + let mut body4 = body.clone(); + + Crypto::crypt_in_place_no_auth(&mut body, &n1, &ss1); + assert_eq!(body, body2, "result after decrypt should be the same"); + + Crypto::crypt_in_place_no_auth(&mut body3, &n2, &ss1); + assert_ne!(body3, body, "decrypt should not be equal with wrong nonce"); + + Crypto::crypt_in_place_no_auth(&mut body4, &n1, &ss2); + assert_ne!(body4, body, "decrypt should not be equal with wrong secret"); + + let body5 = Crypto::crypt_no_auth(LOREM_IPSUM, &n1, &ss1); + let body6 = Crypto::crypt_no_auth(&body5, &n1, &ss1); + let body7 = Crypto::crypt_no_auth(LOREM_IPSUM, &n1, &ss1); + assert_eq!(body6, LOREM_IPSUM); + assert_eq!(body5, body7); +} + pub async fn test_dh(crypto: Crypto) { trace!("test_dh"); let (dht_key, dht_key_secret) = key::generate_secret(); @@ -119,7 +168,8 @@ pub async fn test_dh(crypto: Crypto) { pub async fn test_all() { let api = startup().await; let crypto = api.crypto().unwrap(); - test_enc_dec().await; + test_aead().await; + test_no_auth().await; test_dh(crypto).await; shutdown(api.clone()).await; assert!(api.is_shutdown()); diff --git a/veilid-core/src/tests/common/test_envelope_receipt.rs b/veilid-core/src/tests/common/test_envelope_receipt.rs index 2223943a..4a20b7ac 100644 --- a/veilid-core/src/tests/common/test_envelope_receipt.rs +++ b/veilid-core/src/tests/common/test_envelope_receipt.rs @@ -33,19 +33,31 @@ pub async fn test_envelope_round_trip() { // Deserialize from bytes let envelope2 = - Envelope::from_data(&enc_data).expect("failed to deserialize envelope from data"); + Envelope::from_signed_data(&enc_data).expect("failed to deserialize envelope from data"); let body2 = envelope2 .decrypt_body(crypto.clone(), &enc_data, &recipient_secret) .expect("failed to decrypt envelope body"); - envelope2 - .decrypt_body(crypto.clone(), &enc_data, &sender_secret) - .expect_err("should have failed to decrypt using wrong secret"); - // Compare envelope and body assert_eq!(envelope, envelope2); assert_eq!(body.to_vec(), body2); + + // Deserialize from modified bytes + let enc_data_len = enc_data.len(); + let mut mod_enc_data = enc_data.clone(); + mod_enc_data[enc_data_len - 1] ^= 0x80u8; + assert!( + Envelope::from_signed_data(&mod_enc_data).is_err(), + "should have failed to decode envelope with modified signature" + ); + let mut mod_enc_data2 = enc_data.clone(); + mod_enc_data2[enc_data_len - 65] ^= 0x80u8; + assert!( + Envelope::from_signed_data(&mod_enc_data2).is_err(), + "should have failed to decode envelope with modified data" + ); + api.shutdown().await; }