diff --git a/Cargo.lock b/Cargo.lock index e3340651125d..c8f5aeaac064 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.6" @@ -154,6 +165,21 @@ dependencies = [ "shlex", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -190,6 +216,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +dependencies = [ + "generic-array", +] + [[package]] name = "bs58" version = "0.4.0" @@ -302,6 +337,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.4.0" @@ -524,6 +569,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "der" version = "0.6.0" @@ -579,6 +633,18 @@ dependencies = [ "signature", ] +[[package]] +name = "educe" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07b7cc9cd8c08d10db74fca3b20949b9b6199725c04a0cce6d543496098fcac" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.8.0" @@ -624,6 +690,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum-ordinalize" +version = "3.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2170fc0efee383079a8bdd05d6ea2a184d2a0f07a1c1dcabdb2fd5e9f24bc36c" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "ethabi" version = "17.2.0" @@ -671,7 +751,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "0.17.0" -source = "git+https://github.com/rjected/ethers-rs?branch=add-h128#e600580c0348e959d74fdfb0db9d3cdfb67222d7" +source = "git+https://github.com/gakonst/ethers-rs#a07581489a12b1007c3a261dc8df2bbdc4e27918" dependencies = [ "arrayvec", "bytes", @@ -1177,6 +1257,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1893,6 +1983,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.1", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.21" @@ -1938,6 +2060,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.5.3" @@ -2029,6 +2160,38 @@ dependencies = [ "thiserror", ] +[[package]] +name = "reth-ecies" +version = "0.1.0" +dependencies = [ + "aes", + "block-padding", + "byteorder", + "bytes", + "cipher", + "ctr", + "digest 0.10.5", + "educe", + "futures", + "generic-array", + "hex", + "hex-literal", + "hmac", + "proptest", + "rand", + "reth-primitives", + "reth-rlp", + "secp256k1", + "sha2", + "sha3", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "typenum", +] + [[package]] name = "reth-eth-wire" version = "0.1.0" @@ -2353,6 +2516,18 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.11" @@ -2533,6 +2708,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "1.6.3" @@ -2749,7 +2933,9 @@ dependencies = [ "memchr", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "winapi", @@ -2779,9 +2965,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" dependencies = [ "futures-core", "pin-project-lite", @@ -2917,6 +3103,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index 84ba53c2a14c..0bb846476446 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/executor", "crates/interfaces", "crates/net/p2p", + "crates/net/ecies", "crates/net/eth-wire", "crates/net/rpc", "crates/net/rpc-api", diff --git a/crates/net/ecies/Cargo.toml b/crates/net/ecies/Cargo.toml new file mode 100644 index 000000000000..bbf14c2f1d14 --- /dev/null +++ b/crates/net/ecies/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "reth-ecies" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/foundry-rs/reth" +readme = "README.md" + +[dependencies] +reth-rlp = { path = "../../common/rlp", features = ["derive", "ethereum-types", "std"] } +reth-primitives = { path = "../../primitives" } + +futures = "0.3.24" +thiserror = "1.0.37" +tokio = { version = "1.21.2", features = ["full"] } +tokio-stream = "0.1.11" +tokio-util = { version = "0.7.4", features = ["codec"] } + +educe = "0.4.19" +hex = "0.4.3" +tracing = "0.1.37" + +# HeaderBytes +generic-array = "0.14.6" +typenum = "1.15.0" +byteorder = "1.4.3" +bytes = "1.2.1" + +# crypto +rand = "0.8.5" +ctr = "0.9.2" +digest = "0.10.5" +secp256k1 = { version = "0.24.0", features = ["global-context", "rand-std", "recovery"] } +sha2 = "0.10.6" +sha3 = "0.10.5" +aes = "0.8.1" +hmac = "0.12.1" +block-padding = "0.3.2" +cipher = { version = "0.4.3", features = ["block-padding"] } + +[dev-dependencies] +hex-literal = "0.3.4" +proptest = "1.0.0" diff --git a/crates/net/ecies/src/algorithm.rs b/crates/net/ecies/src/algorithm.rs new file mode 100644 index 000000000000..e1b15915d78e --- /dev/null +++ b/crates/net/ecies/src/algorithm.rs @@ -0,0 +1,766 @@ +#![allow(missing_docs)] +use crate::{ + mac::{HeaderBytes, MAC}, + util::{hmac_sha256, id2pk, pk2id, sha256}, + ECIESError, +}; +use aes::{cipher::StreamCipher, Aes128, Aes256}; +use byteorder::{BigEndian, ByteOrder, ReadBytesExt}; +use bytes::{BufMut, Bytes, BytesMut}; +use ctr::Ctr64BE; +use digest::{crypto_common::KeyIvInit, Digest}; +use educe::Educe; +use rand::{thread_rng, Rng}; +use reth_primitives::{H128, H256, H512 as PeerId}; +use reth_rlp::{Encodable, Rlp, RlpEncodable, RlpMaxEncodedLen}; +use secp256k1::{ + ecdsa::{RecoverableSignature, RecoveryId}, + PublicKey, SecretKey, SECP256K1, +}; +use sha2::Sha256; +use sha3::Keccak256; +use std::{convert::TryFrom, io}; + +const PROTOCOL_VERSION: usize = 4; + +pub(crate) const MAX_BODY_SIZE: usize = 19_573_451; + +fn ecdh_x(public_key: &PublicKey, secret_key: &SecretKey) -> H256 { + H256::from_slice(&secp256k1::ecdh::shared_secret_point(public_key, secret_key)[..32]) +} + +fn kdf(secret: H256, s1: &[u8], dest: &mut [u8]) { + // SEC/ISO/Shoup specify counter size SHOULD be equivalent + // to size of hash output, however, it also notes that + // the 4 bytes is okay. NIST specifies 4 bytes. + let mut ctr = 1_u32; + let mut written = 0_usize; + while written < dest.len() { + let mut hasher = Sha256::default(); + let ctrs = [(ctr >> 24) as u8, (ctr >> 16) as u8, (ctr >> 8) as u8, ctr as u8]; + hasher.update(ctrs); + hasher.update(secret.as_bytes()); + hasher.update(s1); + let d = hasher.finalize(); + dest[written..(written + 32)].copy_from_slice(&d); + written += 32; + ctr += 1; + } +} + +#[derive(Educe)] +#[educe(Debug)] +pub struct ECIES { + #[educe(Debug(ignore))] + secret_key: SecretKey, + public_key: PublicKey, + remote_public_key: Option, + + pub(crate) remote_id: Option, + + #[educe(Debug(ignore))] + ephemeral_secret_key: SecretKey, + ephemeral_public_key: PublicKey, + ephemeral_shared_secret: Option, + remote_ephemeral_public_key: Option, + + nonce: H256, + remote_nonce: Option, + + #[educe(Debug(ignore))] + ingress_aes: Option>, + #[educe(Debug(ignore))] + egress_aes: Option>, + ingress_mac: Option, + egress_mac: Option, + + init_msg: Option, + remote_init_msg: Option, + + body_size: Option, +} + +fn split_at_mut(arr: &mut [T], idx: usize) -> Result<(&mut [T], &mut [T]), ECIESError> { + if idx > arr.len() { + return Err(ECIESError::OutOfBounds { idx, len: arr.len() }) + } + Ok(arr.split_at_mut(idx)) +} + +impl ECIES { + /// Create a new client with the given static secret key, remote peer id, nonce, and ephemeral + /// secret key. + fn new_static_client( + secret_key: SecretKey, + remote_id: PeerId, + nonce: H256, + ephemeral_secret_key: SecretKey, + ) -> Result { + let public_key = PublicKey::from_secret_key(SECP256K1, &secret_key); + let remote_public_key = id2pk(remote_id)?; + let ephemeral_public_key = PublicKey::from_secret_key(SECP256K1, &ephemeral_secret_key); + + Ok(Self { + secret_key, + public_key, + ephemeral_secret_key, + ephemeral_public_key, + nonce, + + remote_public_key: Some(remote_public_key), + remote_ephemeral_public_key: None, + remote_nonce: None, + ephemeral_shared_secret: None, + init_msg: None, + remote_init_msg: None, + + remote_id: Some(remote_id), + + body_size: None, + egress_aes: None, + ingress_aes: None, + egress_mac: None, + ingress_mac: None, + }) + } + + /// Create a new ECIES client with the given static secret key and remote peer ID. + pub fn new_client(secret_key: SecretKey, remote_id: PeerId) -> Result { + let nonce = H256::random(); + let ephemeral_secret_key = SecretKey::new(&mut secp256k1::rand::thread_rng()); + + Self::new_static_client(secret_key, remote_id, nonce, ephemeral_secret_key) + } + + /// Create a new server with the given static secret key, remote peer id, and ephemeral secret + /// key. + pub fn new_static_server( + secret_key: SecretKey, + nonce: H256, + ephemeral_secret_key: SecretKey, + ) -> Result { + let public_key = PublicKey::from_secret_key(SECP256K1, &secret_key); + let ephemeral_public_key = PublicKey::from_secret_key(SECP256K1, &ephemeral_secret_key); + + Ok(Self { + secret_key, + public_key, + ephemeral_secret_key, + ephemeral_public_key, + nonce, + + remote_public_key: None, + remote_ephemeral_public_key: None, + remote_nonce: None, + ephemeral_shared_secret: None, + init_msg: None, + remote_init_msg: None, + + remote_id: None, + + body_size: None, + egress_aes: None, + ingress_aes: None, + egress_mac: None, + ingress_mac: None, + }) + } + + /// Create a new ECIES server with the given static secret key. + pub fn new_server(secret_key: SecretKey) -> Result { + let nonce = H256::random(); + let ephemeral_secret_key = SecretKey::new(&mut secp256k1::rand::thread_rng()); + + Self::new_static_server(secret_key, nonce, ephemeral_secret_key) + } + + /// Return the contained remote peer ID. + pub fn remote_id(&self) -> PeerId { + self.remote_id.unwrap() + } + + fn encrypt_message(&self, data: &[u8], out: &mut BytesMut) { + out.reserve(secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE + 16 + data.len() + 32); + + let secret_key = SecretKey::new(&mut secp256k1::rand::thread_rng()); + out.extend_from_slice( + &PublicKey::from_secret_key(SECP256K1, &secret_key).serialize_uncompressed(), + ); + + let x = ecdh_x(&self.remote_public_key.unwrap(), &secret_key); + let mut key = [0_u8; 32]; + kdf(x, &[], &mut key); + + let enc_key = H128::from_slice(&key[0..16]); + let mac_key = sha256(&key[16..32]); + + let iv = H128::random(); + let mut encryptor = Ctr64BE::::new(enc_key.as_ref().into(), iv.as_ref().into()); + + let mut encrypted = data.to_vec(); + encryptor.apply_keystream(&mut encrypted); + + let total_size: u16 = u16::try_from(65 + 16 + data.len() + 32).unwrap(); + + let tag = + hmac_sha256(mac_key.as_ref(), &[iv.as_bytes(), &encrypted], &total_size.to_be_bytes()); + + out.extend_from_slice(iv.as_bytes()); + out.extend_from_slice(&encrypted); + out.extend_from_slice(tag.as_ref()); + } + + fn decrypt_message<'a>(&self, data: &'a mut [u8]) -> Result<&'a mut [u8], ECIESError> { + let (auth_data, encrypted) = split_at_mut(data, 2)?; + let (pubkey_bytes, encrypted) = split_at_mut(encrypted, 65)?; + let public_key = PublicKey::from_slice(pubkey_bytes)?; + let (data_iv, tag_bytes) = split_at_mut(encrypted, encrypted.len() - 32)?; + let (iv, encrypted_data) = split_at_mut(data_iv, 16)?; + let tag = H256::from_slice(tag_bytes); + + let x = ecdh_x(&public_key, &self.secret_key); + let mut key = [0_u8; 32]; + kdf(x, &[], &mut key); + let enc_key = H128::from_slice(&key[0..16]); + let mac_key = sha256(&key[16..32]); + + let check_tag = hmac_sha256(mac_key.as_ref(), &[iv, encrypted_data], auth_data); + if check_tag != tag { + return Err(ECIESError::TagCheckFailed) + } + + let decrypted_data = encrypted_data; + + let mut decryptor = Ctr64BE::::new(enc_key.as_ref().into(), (*iv).into()); + decryptor.apply_keystream(decrypted_data); + + Ok(decrypted_data) + } + + fn create_auth_unencrypted(&self) -> BytesMut { + let x = ecdh_x(&self.remote_public_key.unwrap(), &self.secret_key); + let msg = x ^ self.nonce; + let (rec_id, sig) = SECP256K1 + .sign_ecdsa_recoverable( + &secp256k1::Message::from_slice(msg.as_bytes()).unwrap(), + &self.ephemeral_secret_key, + ) + .serialize_compact(); + + let mut sig_bytes = [0_u8; 65]; + sig_bytes[..64].copy_from_slice(&sig); + sig_bytes[64] = rec_id.to_i32() as u8; + + let id = pk2id(&self.public_key); + + #[derive(RlpEncodable)] + struct S<'a> { + sig_bytes: &'a [u8; 65], + id: &'a PeerId, + nonce: &'a H256, + protocol_version: u8, + } + + let mut out = BytesMut::new(); + S { + sig_bytes: &sig_bytes, + id: &id, + nonce: &self.nonce, + protocol_version: PROTOCOL_VERSION as u8, + } + .encode(&mut out); + + out.resize(out.len() + thread_rng().gen_range(100..=300), 0); + out + } + + #[cfg(test)] + fn create_auth(&mut self) -> BytesMut { + let mut buf = BytesMut::new(); + self.write_auth(&mut buf); + buf + } + + /// Write an auth message to the given buffer. + pub fn write_auth(&mut self, buf: &mut BytesMut) { + let unencrypted = self.create_auth_unencrypted(); + + let mut out = buf.split_off(buf.len()); + out.put_u16(0); + + let mut encrypted = out.split_off(out.len()); + self.encrypt_message(&unencrypted, &mut encrypted); + + let len_bytes = u16::try_from(encrypted.len()).unwrap().to_be_bytes(); + out[..len_bytes.len()].copy_from_slice(&len_bytes); + + out.unsplit(encrypted); + + self.init_msg = Some(Bytes::copy_from_slice(&out)); + + buf.unsplit(out); + } + + fn parse_auth_unencrypted(&mut self, data: &[u8]) -> Result<(), ECIESError> { + let mut data = Rlp::new(data)?; + + let sigdata = data.get_next::<[u8; 65]>()?.ok_or(ECIESError::InvalidAuthData)?; + let signature = RecoverableSignature::from_compact( + &sigdata[0..64], + RecoveryId::from_i32(sigdata[64] as i32)?, + )?; + let remote_id = data.get_next()?.ok_or(ECIESError::InvalidAuthData)?; + self.remote_id = Some(remote_id); + self.remote_public_key = Some(id2pk(remote_id)?); + self.remote_nonce = Some(data.get_next()?.ok_or(ECIESError::InvalidAuthData)?); + + let x = ecdh_x(&self.remote_public_key.unwrap(), &self.secret_key); + self.remote_ephemeral_public_key = Some(SECP256K1.recover_ecdsa( + &secp256k1::Message::from_slice((x ^ self.remote_nonce.unwrap()).as_ref()).unwrap(), + &signature, + )?); + self.ephemeral_shared_secret = + Some(ecdh_x(&self.remote_ephemeral_public_key.unwrap(), &self.ephemeral_secret_key)); + + Ok(()) + } + + /// Read and verify an auth message from the input data. + pub fn read_auth(&mut self, data: &mut [u8]) -> Result<(), ECIESError> { + self.remote_init_msg = Some(Bytes::copy_from_slice(data)); + let unencrypted = self.decrypt_message(data)?; + self.parse_auth_unencrypted(unencrypted) + } + + fn create_ack_unencrypted(&self) -> impl AsRef<[u8]> { + #[derive(RlpEncodable, RlpMaxEncodedLen)] + struct S { + id: PeerId, + nonce: H256, + protocol_version: u8, + } + + reth_rlp::encode_fixed_size(&S { + id: pk2id(&self.ephemeral_public_key), + nonce: self.nonce, + protocol_version: PROTOCOL_VERSION as u8, + }) + } + + #[cfg(test)] + pub fn create_ack(&mut self) -> BytesMut { + let mut buf = BytesMut::new(); + self.write_ack(&mut buf); + buf + } + + /// Write an ack message to the given buffer. + pub fn write_ack(&mut self, out: &mut BytesMut) { + let unencrypted = self.create_ack_unencrypted(); + + let mut buf = out.split_off(out.len()); + + // reserve space for length + buf.put_u16(0); + + // encrypt and append + let mut encrypted = buf.split_off(buf.len()); + self.encrypt_message(unencrypted.as_ref(), &mut encrypted); + let len_bytes = u16::try_from(encrypted.len()).unwrap().to_be_bytes(); + buf.unsplit(encrypted); + + // write length + buf[..len_bytes.len()].copy_from_slice(&len_bytes[..]); + + self.init_msg = Some(buf.clone().freeze()); + out.unsplit(buf); + + self.setup_frame(true); + } + + fn parse_ack_unencrypted(&mut self, data: &[u8]) -> Result<(), ECIESError> { + let mut data = Rlp::new(data)?; + self.remote_ephemeral_public_key = + Some(id2pk(data.get_next()?.ok_or(ECIESError::InvalidAckData)?)?); + self.remote_nonce = Some(data.get_next()?.ok_or(ECIESError::InvalidAckData)?); + + self.ephemeral_shared_secret = + Some(ecdh_x(&self.remote_ephemeral_public_key.unwrap(), &self.ephemeral_secret_key)); + Ok(()) + } + + /// Read and verify an ack message from the input data. + pub fn read_ack(&mut self, data: &mut [u8]) -> Result<(), ECIESError> { + self.remote_init_msg = Some(Bytes::copy_from_slice(data)); + let unencrypted = self.decrypt_message(data)?; + self.parse_ack_unencrypted(unencrypted)?; + self.setup_frame(false); + Ok(()) + } + + fn setup_frame(&mut self, incoming: bool) { + let mut hasher = Keccak256::new(); + for el in &if incoming { + [self.nonce, self.remote_nonce.unwrap()] + } else { + [self.remote_nonce.unwrap(), self.nonce] + } { + hasher.update(el); + } + let h_nonce = H256::from(hasher.finalize().as_ref()); + + let iv = H128::default(); + let shared_secret: H256 = { + let mut hasher = Keccak256::new(); + hasher.update(self.ephemeral_shared_secret.unwrap().as_ref()); + hasher.update(h_nonce.as_ref()); + H256::from(hasher.finalize().as_ref()) + }; + + let aes_secret: H256 = { + let mut hasher = Keccak256::new(); + hasher.update(self.ephemeral_shared_secret.unwrap().as_ref()); + hasher.update(shared_secret.as_ref()); + H256::from(hasher.finalize().as_ref()) + }; + self.ingress_aes = + Some(Ctr64BE::::new(aes_secret.as_ref().into(), iv.as_ref().into())); + self.egress_aes = + Some(Ctr64BE::::new(aes_secret.as_ref().into(), iv.as_ref().into())); + + let mac_secret: H256 = { + let mut hasher = Keccak256::new(); + hasher.update(self.ephemeral_shared_secret.unwrap().as_ref()); + hasher.update(aes_secret.as_ref()); + H256::from(hasher.finalize().as_ref()) + }; + self.ingress_mac = Some(MAC::new(mac_secret)); + self.ingress_mac.as_mut().unwrap().update((mac_secret ^ self.nonce).as_ref()); + self.ingress_mac.as_mut().unwrap().update(self.remote_init_msg.as_ref().unwrap()); + self.egress_mac = Some(MAC::new(mac_secret)); + self.egress_mac + .as_mut() + .unwrap() + .update((mac_secret ^ self.remote_nonce.unwrap()).as_ref()); + self.egress_mac.as_mut().unwrap().update(self.init_msg.as_ref().unwrap()); + } + + #[cfg(test)] + fn create_header(&mut self, size: usize) -> BytesMut { + let mut out = BytesMut::new(); + self.write_header(&mut out, size); + out + } + + pub fn write_header(&mut self, out: &mut BytesMut, size: usize) { + let mut buf = [0; 8]; + BigEndian::write_uint(&mut buf, size as u64, 3); + let mut header = [0_u8; 16]; + header[0..3].copy_from_slice(&buf[0..3]); + header[3..6].copy_from_slice(&[194, 128, 128]); + + let mut header = HeaderBytes::from(header); + self.egress_aes.as_mut().unwrap().apply_keystream(&mut header); + self.egress_mac.as_mut().unwrap().update_header(&header); + let tag = self.egress_mac.as_mut().unwrap().digest(); + + out.reserve(ECIES::header_len()); + out.extend_from_slice(&header); + out.extend_from_slice(tag.as_bytes()); + } + + pub fn read_header(&mut self, data: &mut [u8]) -> Result { + let (header_bytes, mac_bytes) = split_at_mut(data, 16)?; + let header = HeaderBytes::from_mut_slice(header_bytes); + let mac = H128::from_slice(&mac_bytes[..16]); + + self.ingress_mac.as_mut().unwrap().update_header(header); + let check_mac = self.ingress_mac.as_mut().unwrap().digest(); + if check_mac != mac { + return Err(ECIESError::TagCheckFailed) + } + + self.ingress_aes.as_mut().unwrap().apply_keystream(header); + if header.as_slice().len() < 3 { + return Err(ECIESError::InvalidHeader) + } + let body_size = usize::try_from(header.as_slice().read_uint::(3)?)?; + + if body_size > MAX_BODY_SIZE { + return Err(ECIESError::IO(io::Error::new( + io::ErrorKind::InvalidInput, + format!("body size ({}) exceeds limit ({} bytes)", body_size, MAX_BODY_SIZE), + ))) + } + + self.body_size = Some(body_size); + + Ok(self.body_size.unwrap()) + } + + pub const fn header_len() -> usize { + 32 + } + + pub fn body_len(&self) -> usize { + let len = self.body_size.unwrap(); + (if len % 16 == 0 { len } else { (len / 16 + 1) * 16 }) + 16 + } + + #[cfg(test)] + fn create_body(&mut self, data: &[u8]) -> BytesMut { + let mut out = BytesMut::new(); + self.write_body(&mut out, data); + out + } + + pub fn write_body(&mut self, out: &mut BytesMut, data: &[u8]) { + let len = if data.len() % 16 == 0 { data.len() } else { (data.len() / 16 + 1) * 16 }; + let old_len = out.len(); + out.resize(old_len + len, 0); + + let encrypted = &mut out[old_len..old_len + len]; + encrypted[..data.len()].copy_from_slice(data); + + self.egress_aes.as_mut().unwrap().apply_keystream(encrypted); + self.egress_mac.as_mut().unwrap().update_body(encrypted); + let tag = self.egress_mac.as_mut().unwrap().digest(); + + out.extend_from_slice(tag.as_bytes()); + } + + pub fn read_body<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a mut [u8], ECIESError> { + let (body, mac_bytes) = split_at_mut(data, data.len() - 16)?; + let mac = H128::from_slice(mac_bytes); + self.ingress_mac.as_mut().unwrap().update_body(body); + let check_mac = self.ingress_mac.as_mut().unwrap().digest(); + if check_mac != mac { + return Err(ECIESError::TagCheckFailed) + } + + let size = self.body_size.unwrap(); + self.body_size = None; + let ret = body; + self.ingress_aes.as_mut().unwrap().apply_keystream(ret); + Ok(split_at_mut(ret, size)?.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn ecdh() { + let our_secret_key = SecretKey::from_slice(&hex!( + "202a36e24c3eb39513335ec99a7619bad0e7dc68d69401b016253c7d26dc92f8" + )) + .unwrap(); + let remote_public_key = id2pk(hex!("d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666").into()).unwrap(); + + assert_eq!( + ecdh_x(&remote_public_key, &our_secret_key), + hex!("821ce7e01ea11b111a52b2dafae8a3031a372d83bdf1a78109fa0783c2b9d5d3").into() + ) + } + + #[test] + fn communicate() { + let server_secret_key = SecretKey::new(&mut secp256k1::rand::thread_rng()); + let server_public_key = PublicKey::from_secret_key(SECP256K1, &server_secret_key); + let client_secret_key = SecretKey::new(&mut secp256k1::rand::thread_rng()); + + let mut server_ecies = ECIES::new_server(server_secret_key).unwrap(); + let mut client_ecies = + ECIES::new_client(client_secret_key, pk2id(&server_public_key)).unwrap(); + + // Handshake + let mut auth = client_ecies.create_auth(); + server_ecies.read_auth(&mut auth).unwrap(); + let mut ack = server_ecies.create_ack(); + client_ecies.read_ack(&mut ack).unwrap(); + + let server_to_client_data = [0_u8, 1_u8, 2_u8, 3_u8, 4_u8]; + let client_to_server_data = [5_u8, 6_u8, 7_u8]; + + // Test server to client 1 + let mut header = server_ecies.create_header(server_to_client_data.len()); + assert_eq!(header.len(), ECIES::header_len()); + client_ecies.read_header(&mut *header).unwrap(); + let mut body = server_ecies.create_body(&server_to_client_data); + assert_eq!(body.len(), client_ecies.body_len()); + let ret = client_ecies.read_body(&mut *body).unwrap(); + assert_eq!(ret, server_to_client_data); + + // Test client to server 1 + server_ecies + .read_header(&mut *client_ecies.create_header(client_to_server_data.len())) + .unwrap(); + let mut b = client_ecies.create_body(&client_to_server_data); + let ret = server_ecies.read_body(&mut b).unwrap(); + assert_eq!(ret, client_to_server_data); + + // Test server to client 2 + client_ecies + .read_header(&mut *server_ecies.create_header(server_to_client_data.len())) + .unwrap(); + let mut b = server_ecies.create_body(&server_to_client_data); + let ret = client_ecies.read_body(&mut b).unwrap(); + assert_eq!(ret, server_to_client_data); + + // Test server to client 3 + client_ecies + .read_header(&mut *server_ecies.create_header(server_to_client_data.len())) + .unwrap(); + let mut b = server_ecies.create_body(&server_to_client_data); + let ret = client_ecies.read_body(&mut b).unwrap(); + assert_eq!(ret, server_to_client_data); + + // Test client to server 2 + server_ecies + .read_header(&mut *client_ecies.create_header(client_to_server_data.len())) + .unwrap(); + let mut b = client_ecies.create_body(&client_to_server_data); + let ret = server_ecies.read_body(&mut b).unwrap(); + assert_eq!(ret, client_to_server_data); + + // Test client to server 3 + server_ecies + .read_header(&mut *client_ecies.create_header(client_to_server_data.len())) + .unwrap(); + let mut b = client_ecies.create_body(&client_to_server_data); + let ret = server_ecies.read_body(&mut b).unwrap(); + assert_eq!(ret, client_to_server_data); + } + + fn eip8_test_server_key() -> SecretKey { + SecretKey::from_slice(&hex!( + "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" + )) + .unwrap() + } + + fn eip8_test_client() -> ECIES { + let client_static_key = SecretKey::from_slice(&hex!( + "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee" + )) + .unwrap(); + + let client_ephemeral_key = SecretKey::from_slice(&hex!( + "869d6ecf5211f1cc60418a13b9d870b22959d0c16f02bec714c960dd2298a32d" + )) + .unwrap(); + + let client_nonce = + H256(hex!("7e968bba13b6c50e2c4cd7f241cc0d64d1ac25c7f5952df231ac6a2bda8ee5d6")); + + let server_id = pk2id(&PublicKey::from_secret_key(SECP256K1, &eip8_test_server_key())); + + ECIES::new_static_client(client_static_key, server_id, client_nonce, client_ephemeral_key) + .unwrap() + } + + fn eip8_test_server() -> ECIES { + let server_ephemeral_key = SecretKey::from_slice(&hex!( + "e238eb8e04fee6511ab04c6dd3c89ce097b11f25d584863ac2b6d5b35b1847e4" + )) + .unwrap(); + + let server_nonce = + H256(hex!("559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd")); + + ECIES::new_static_server(eip8_test_server_key(), server_nonce, server_ephemeral_key) + .unwrap() + } + + #[test] + /// Test vectors from https://eips.ethereum.org/EIPS/eip-8 + fn eip8_test() { + // EIP-8 format with version 4 and no additional list elements + let auth2 = hex!( + " + 01b304ab7578555167be8154d5cc456f567d5ba302662433674222360f08d5f1534499d3678b513b + 0fca474f3a514b18e75683032eb63fccb16c156dc6eb2c0b1593f0d84ac74f6e475f1b8d56116b84 + 9634a8c458705bf83a626ea0384d4d7341aae591fae42ce6bd5c850bfe0b999a694a49bbbaf3ef6c + da61110601d3b4c02ab6c30437257a6e0117792631a4b47c1d52fc0f8f89caadeb7d02770bf999cc + 147d2df3b62e1ffb2c9d8c125a3984865356266bca11ce7d3a688663a51d82defaa8aad69da39ab6 + d5470e81ec5f2a7a47fb865ff7cca21516f9299a07b1bc63ba56c7a1a892112841ca44b6e0034dee + 70c9adabc15d76a54f443593fafdc3b27af8059703f88928e199cb122362a4b35f62386da7caad09 + c001edaeb5f8a06d2b26fb6cb93c52a9fca51853b68193916982358fe1e5369e249875bb8d0d0ec3 + 6f917bc5e1eafd5896d46bd61ff23f1a863a8a8dcd54c7b109b771c8e61ec9c8908c733c0263440e + 2aa067241aaa433f0bb053c7b31a838504b148f570c0ad62837129e547678c5190341e4f1693956c + 3bf7678318e2d5b5340c9e488eefea198576344afbdf66db5f51204a6961a63ce072c8926c + " + ); + + // EIP-8 format with version 56 and 3 additional list elements (sent from A to B) + let auth3 = hex!( + " + 01b8044c6c312173685d1edd268aa95e1d495474c6959bcdd10067ba4c9013df9e40ff45f5bfd6f7 + 2471f93a91b493f8e00abc4b80f682973de715d77ba3a005a242eb859f9a211d93a347fa64b597bf + 280a6b88e26299cf263b01b8dfdb712278464fd1c25840b995e84d367d743f66c0e54a586725b7bb + f12acca27170ae3283c1073adda4b6d79f27656993aefccf16e0d0409fe07db2dc398a1b7e8ee93b + cd181485fd332f381d6a050fba4c7641a5112ac1b0b61168d20f01b479e19adf7fdbfa0905f63352 + bfc7e23cf3357657455119d879c78d3cf8c8c06375f3f7d4861aa02a122467e069acaf513025ff19 + 6641f6d2810ce493f51bee9c966b15c5043505350392b57645385a18c78f14669cc4d960446c1757 + 1b7c5d725021babbcd786957f3d17089c084907bda22c2b2675b4378b114c601d858802a55345a15 + 116bc61da4193996187ed70d16730e9ae6b3bb8787ebcaea1871d850997ddc08b4f4ea668fbf3740 + 7ac044b55be0908ecb94d4ed172ece66fd31bfdadf2b97a8bc690163ee11f5b575a4b44e36e2bfb2 + f0fce91676fd64c7773bac6a003f481fddd0bae0a1f31aa27504e2a533af4cef3b623f4791b2cca6 + d490 + " + ); + + // EIP-8 format with version 4 and no additional list elements (sent from B to A) + let ack2 = hex!( + " + 01ea0451958701280a56482929d3b0757da8f7fbe5286784beead59d95089c217c9b917788989470 + b0e330cc6e4fb383c0340ed85fab836ec9fb8a49672712aeabbdfd1e837c1ff4cace34311cd7f4de + 05d59279e3524ab26ef753a0095637ac88f2b499b9914b5f64e143eae548a1066e14cd2f4bd7f814 + c4652f11b254f8a2d0191e2f5546fae6055694aed14d906df79ad3b407d94692694e259191cde171 + ad542fc588fa2b7333313d82a9f887332f1dfc36cea03f831cb9a23fea05b33deb999e85489e645f + 6aab1872475d488d7bd6c7c120caf28dbfc5d6833888155ed69d34dbdc39c1f299be1057810f34fb + e754d021bfca14dc989753d61c413d261934e1a9c67ee060a25eefb54e81a4d14baff922180c395d + 3f998d70f46f6b58306f969627ae364497e73fc27f6d17ae45a413d322cb8814276be6ddd13b885b + 201b943213656cde498fa0e9ddc8e0b8f8a53824fbd82254f3e2c17e8eaea009c38b4aa0a3f306e8 + 797db43c25d68e86f262e564086f59a2fc60511c42abfb3057c247a8a8fe4fb3ccbadde17514b7ac + 8000cdb6a912778426260c47f38919a91f25f4b5ffb455d6aaaf150f7e5529c100ce62d6d92826a7 + 1778d809bdf60232ae21ce8a437eca8223f45ac37f6487452ce626f549b3b5fdee26afd2072e4bc7 + 5833c2464c805246155289f4 + " + ); + + // EIP-8 format with version 57 and 3 additional list elements (sent from B to A) + let ack3 = hex!( + " + 01f004076e58aae772bb101ab1a8e64e01ee96e64857ce82b1113817c6cdd52c09d26f7b90981cd7 + ae835aeac72e1573b8a0225dd56d157a010846d888dac7464baf53f2ad4e3d584531fa203658fab0 + 3a06c9fd5e35737e417bc28c1cbf5e5dfc666de7090f69c3b29754725f84f75382891c561040ea1d + dc0d8f381ed1b9d0d4ad2a0ec021421d847820d6fa0ba66eaf58175f1b235e851c7e2124069fbc20 + 2888ddb3ac4d56bcbd1b9b7eab59e78f2e2d400905050f4a92dec1c4bdf797b3fc9b2f8e84a482f3 + d800386186712dae00d5c386ec9387a5e9c9a1aca5a573ca91082c7d68421f388e79127a5177d4f8 + 590237364fd348c9611fa39f78dcdceee3f390f07991b7b47e1daa3ebcb6ccc9607811cb17ce51f1 + c8c2c5098dbdd28fca547b3f58c01a424ac05f869f49c6a34672ea2cbbc558428aa1fe48bbfd6115 + 8b1b735a65d99f21e70dbc020bfdface9f724a0d1fb5895db971cc81aa7608baa0920abb0a565c9c + 436e2fd13323428296c86385f2384e408a31e104670df0791d93e743a3a5194ee6b076fb6323ca59 + 3011b7348c16cf58f66b9633906ba54a2ee803187344b394f75dd2e663a57b956cb830dd7a908d4f + 39a2336a61ef9fda549180d4ccde21514d117b6c6fd07a9102b5efe710a32af4eeacae2cb3b1dec0 + 35b9593b48b9d3ca4c13d245d5f04169b0b1 + " + ); + + eip8_test_server().read_auth(&mut auth2.to_vec()).unwrap(); + eip8_test_server().read_auth(&mut auth3.to_vec()).unwrap(); + + let mut test_client = eip8_test_client(); + let mut test_server = eip8_test_server(); + + test_server.read_auth(&mut test_client.create_auth()).unwrap(); + + test_client.read_ack(&mut test_server.create_ack()).unwrap(); + + test_client.read_ack(&mut ack2.to_vec()).unwrap(); + test_client.read_ack(&mut ack3.to_vec()).unwrap(); + } +} diff --git a/crates/net/ecies/src/codec.rs b/crates/net/ecies/src/codec.rs new file mode 100644 index 000000000000..68c23e0514dc --- /dev/null +++ b/crates/net/ecies/src/codec.rs @@ -0,0 +1,152 @@ +use crate::{ + algorithm::{ECIES, MAX_BODY_SIZE}, + ECIESError, EgressECIESValue, IngressECIESValue, +}; +use bytes::{Bytes, BytesMut}; +use reth_primitives::H512 as PeerId; +use secp256k1::SecretKey; +use std::{fmt::Debug, io}; +use tokio_util::codec::{Decoder, Encoder}; +use tracing::{instrument, trace}; + +/// Tokio codec for ECIES +#[derive(Debug)] +pub(crate) struct ECIESCodec { + ecies: ECIES, + state: ECIESState, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Current ECIES state of a connection +enum ECIESState { + /// The first stage of the ECIES handshake, where each side of the connection sends an auth + /// message containing the ephemeral public key, signature of the public key, nonce, and other + /// metadata. + Auth, + + /// The second stage of the ECIES handshake, where each side of the connection sends an ack + /// message containing the nonce and other metadata. + Ack, + Header, + Body, +} + +impl ECIESCodec { + /// Create a new server codec using the given secret key + pub(crate) fn new_server(secret_key: SecretKey) -> Result { + Ok(Self { ecies: ECIES::new_server(secret_key)?, state: ECIESState::Auth }) + } + + /// Create a new client codec using the given secret key and the server's public id + pub(crate) fn new_client(secret_key: SecretKey, remote_id: PeerId) -> Result { + Ok(Self { ecies: ECIES::new_client(secret_key, remote_id)?, state: ECIESState::Auth }) + } +} + +impl Decoder for ECIESCodec { + type Item = IngressECIESValue; + type Error = ECIESError; + + #[instrument(level = "trace", skip_all, fields(peer=&*format!("{:?}", self.ecies.remote_id.map(|s| s.to_string())), state=&*format!("{:?}", self.state)))] + fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { + loop { + match self.state { + ECIESState::Auth => { + trace!("parsing auth"); + if buf.len() < 2 { + return Ok(None) + } + + let payload_size = u16::from_be_bytes([buf[0], buf[1]]) as usize; + let total_size = payload_size + 2; + + if buf.len() < total_size { + trace!("current len {}, need {}", buf.len(), total_size); + return Ok(None) + } + + self.ecies.read_auth(&mut buf.split_to(total_size))?; + + self.state = ECIESState::Header; + return Ok(Some(IngressECIESValue::AuthReceive(self.ecies.remote_id()))) + } + ECIESState::Ack => { + trace!("parsing ack with len {}", buf.len()); + if buf.len() < 2 { + return Ok(None) + } + + let payload_size = u16::from_be_bytes([buf[0], buf[1]]) as usize; + let total_size = payload_size + 2; + + if buf.len() < total_size { + trace!("current len {}, need {}", buf.len(), total_size); + return Ok(None) + } + + self.ecies.read_ack(&mut buf.split_to(total_size))?; + + self.state = ECIESState::Header; + return Ok(Some(IngressECIESValue::Ack)) + } + ECIESState::Header => { + if buf.len() < ECIES::header_len() { + trace!("current len {}, need {}", buf.len(), ECIES::header_len()); + return Ok(None) + } + + self.ecies.read_header(&mut buf.split_to(ECIES::header_len()))?; + + self.state = ECIESState::Body; + } + ECIESState::Body => { + if buf.len() < self.ecies.body_len() { + return Ok(None) + } + + let mut data = buf.split_to(self.ecies.body_len()); + let ret = Bytes::copy_from_slice(self.ecies.read_body(&mut data)?); + + self.state = ECIESState::Header; + return Ok(Some(IngressECIESValue::Message(ret))) + } + } + } + } +} + +impl Encoder for ECIESCodec { + type Error = io::Error; + + #[instrument(level = "trace", skip(self, buf), fields(peer=&*format!("{:?}", self.ecies.remote_id.map(|s| s.to_string())), state=&*format!("{:?}", self.state)))] + fn encode(&mut self, item: EgressECIESValue, buf: &mut BytesMut) -> Result<(), Self::Error> { + match item { + EgressECIESValue::Auth => { + self.state = ECIESState::Ack; + self.ecies.write_auth(buf); + Ok(()) + } + EgressECIESValue::Ack => { + self.state = ECIESState::Header; + self.ecies.write_ack(buf); + Ok(()) + } + EgressECIESValue::Message(data) => { + if data.len() > MAX_BODY_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "body size ({}) exceeds limit ({} bytes)", + data.len(), + MAX_BODY_SIZE + ), + )) + } + + self.ecies.write_header(buf, data.len()); + self.ecies.write_body(buf, &data); + Ok(()) + } + } + } +} diff --git a/crates/net/ecies/src/error.rs b/crates/net/ecies/src/error.rs new file mode 100644 index 000000000000..ef25040d2793 --- /dev/null +++ b/crates/net/ecies/src/error.rs @@ -0,0 +1,48 @@ +use thiserror::Error; + +use crate::IngressECIESValue; + +/// An error that occurs while reading or writing to an ECIES stream. +#[derive(Debug, Error)] +pub enum ECIESError { + /// Error during IO + #[error("IO error")] + IO(#[from] std::io::Error), + /// Error when checking the HMAC tag against the tag on the data + #[error("tag check failure")] + TagCheckFailed, + /// Error when parsing AUTH data + #[error("invalid auth data")] + InvalidAuthData, + /// Error when parsing ACK data + #[error("invalid ack data")] + InvalidAckData, + /// Error when reading the header if its length is <3 + #[error("invalid body data")] + InvalidHeader, + /// Error when interacting with secp256k1 + #[error(transparent)] + Secp256k1(#[from] secp256k1::Error), + /// Error when decoding RLP data + #[error(transparent)] + RLPDecoding(#[from] reth_rlp::DecodeError), + /// Error when convering to integer + #[error(transparent)] + FromInt(#[from] std::num::TryFromIntError), + /// Error when trying to split an array beyond its length + #[error("requested {idx} but array len is {len}")] + OutOfBounds { + /// The index you are trying to split at + idx: usize, + /// The length of the array + len: usize, + }, + /// Error when handshaking with a peer (ack / auth) + #[error("invalid handshake: expected {expected:?}, got {msg:?} instead")] + InvalidHandshake { + /// The expected return value from the peer + expected: IngressECIESValue, + /// The actual value returned from the peer + msg: Option, + }, +} diff --git a/crates/net/ecies/src/lib.rs b/crates/net/ecies/src/lib.rs new file mode 100644 index 000000000000..268ece82d5d2 --- /dev/null +++ b/crates/net/ecies/src/lib.rs @@ -0,0 +1,42 @@ +#![warn(missing_docs, unreachable_pub)] +#![deny(unused_must_use, rust_2018_idioms)] +#![doc(test( + no_crate_inject, + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) +))] + +//! RLPx ECIES framed transport protocol. + +pub mod algorithm; +pub mod mac; +pub mod stream; +mod util; + +mod error; +pub use error::ECIESError; + +mod codec; + +use reth_primitives::H512 as PeerId; + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Raw egress values for an ECIES protocol +pub enum EgressECIESValue { + /// The AUTH message being sent out + Auth, + /// The ACK message being sent out + Ack, + /// The message being sent out (wrapped bytes) + Message(bytes::Bytes), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Raw ingress values for an ECIES protocol +pub enum IngressECIESValue { + /// Receiving a message from a [`peerId`] + AuthReceive(PeerId), + /// Receiving an ACK message + Ack, + /// Receiving a message + Message(bytes::Bytes), +} diff --git a/crates/net/ecies/src/mac.rs b/crates/net/ecies/src/mac.rs new file mode 100644 index 000000000000..2a834839ffdb --- /dev/null +++ b/crates/net/ecies/src/mac.rs @@ -0,0 +1,53 @@ +#![allow(missing_docs)] +use aes::Aes256Enc; +use block_padding::NoPadding; +use cipher::BlockEncrypt; +use digest::KeyInit; +use generic_array::GenericArray; +use reth_primitives::{H128, H256}; +use sha3::{Digest, Keccak256}; +use typenum::U16; + +pub type HeaderBytes = GenericArray; + +#[derive(Debug)] +pub struct MAC { + secret: H256, + hasher: Keccak256, +} + +impl MAC { + pub fn new(secret: H256) -> Self { + Self { secret, hasher: Keccak256::new() } + } + + pub fn update(&mut self, data: &[u8]) { + self.hasher.update(data) + } + + pub fn update_header(&mut self, data: &HeaderBytes) { + let aes = Aes256Enc::new_from_slice(self.secret.as_ref()).unwrap(); + let mut encrypted = self.digest().to_fixed_bytes(); + aes.encrypt_padded::(&mut encrypted, H128::len_bytes()).unwrap(); + for i in 0..data.len() { + encrypted[i] ^= data[i]; + } + self.hasher.update(encrypted); + } + + pub fn update_body(&mut self, data: &[u8]) { + self.hasher.update(data); + let prev = self.digest(); + let aes = Aes256Enc::new_from_slice(self.secret.as_ref()).unwrap(); + let mut encrypted = self.digest().to_fixed_bytes(); + aes.encrypt_padded::(&mut encrypted, H128::len_bytes()).unwrap(); + for i in 0..16 { + encrypted[i] ^= prev[i]; + } + self.hasher.update(encrypted); + } + + pub fn digest(&self) -> H128 { + H128::from_slice(&self.hasher.clone().finalize()[0..16]) + } +} diff --git a/crates/net/ecies/src/stream.rs b/crates/net/ecies/src/stream.rs new file mode 100644 index 000000000000..41897f066f99 --- /dev/null +++ b/crates/net/ecies/src/stream.rs @@ -0,0 +1,187 @@ +//! The ECIES Stream implementation which wraps over [`AsyncRead`] and [`AsyncWrite`]. +use crate::{ECIESError, EgressECIESValue, IngressECIESValue}; +use bytes::Bytes; +use futures::{ready, Sink, SinkExt}; +use reth_primitives::H512 as PeerId; +use secp256k1::SecretKey; +use std::{ + fmt::Debug, + io, + net::SocketAddr, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; +use tokio_stream::{Stream, StreamExt}; +use tokio_util::codec::{Decoder, Framed}; +use tracing::{debug, instrument, trace}; + +use crate::codec::ECIESCodec; + +/// `ECIES` stream over TCP exchanging raw bytes +#[derive(Debug)] +pub struct ECIESStream { + stream: Framed, + remote_id: PeerId, +} + +/// This trait is just for instrumenting the stream with a socket addr +pub trait HasRemoteAddr { + /// Maybe returns a [`SocketAddr`] + fn remote_addr(&self) -> Option; +} + +impl HasRemoteAddr for TcpStream { + fn remote_addr(&self) -> Option { + self.peer_addr().ok() + } +} + +impl ECIESStream +where + Io: AsyncRead + AsyncWrite + Unpin + HasRemoteAddr, +{ + /// Connect to an `ECIES` server + #[instrument(skip(transport, secret_key), fields(peer=&*format!("{:?}", transport.remote_addr())))] + pub async fn connect( + transport: Io, + secret_key: SecretKey, + remote_id: PeerId, + ) -> Result { + let ecies = ECIESCodec::new_client(secret_key, remote_id) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid handshake"))?; + + let mut transport = ecies.framed(transport); + + trace!("sending ecies auth ..."); + transport.send(EgressECIESValue::Auth).await?; + + trace!("waiting for ecies ack ..."); + let msg = transport.try_next().await?; + + trace!("parsing ecies ack ..."); + if matches!(msg, Some(IngressECIESValue::Ack)) { + Ok(Self { stream: transport, remote_id }) + } else { + Err(ECIESError::InvalidHandshake { expected: IngressECIESValue::Ack, msg }) + } + } + + /// Listen on a just connected ECIES client + #[instrument(skip_all, fields(peer=&*format!("{:?}", transport.remote_addr())))] + pub async fn incoming(transport: Io, secret_key: SecretKey) -> Result { + let ecies = ECIESCodec::new_server(secret_key)?; + + debug!("incoming ecies stream ..."); + let mut transport = ecies.framed(transport); + let msg = transport.try_next().await?; + + debug!("receiving ecies auth"); + let remote_id = match &msg { + Some(IngressECIESValue::AuthReceive(remote_id)) => *remote_id, + _ => { + return Err(ECIESError::InvalidHandshake { + expected: IngressECIESValue::AuthReceive(Default::default()), + msg, + }) + } + }; + + debug!("sending ecies ack ..."); + transport.send(EgressECIESValue::Ack).await?; + + Ok(Self { stream: transport, remote_id }) + } + + /// Get the remote id + pub fn remote_id(&self) -> PeerId { + self.remote_id + } +} + +impl Stream for ECIESStream +where + Io: AsyncRead + Unpin, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match ready!(Pin::new(&mut self.get_mut().stream).poll_next(cx)) { + Some(Ok(IngressECIESValue::Message(body))) => Poll::Ready(Some(Ok(body))), + Some(other) => Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::Other, + format!("ECIES stream protocol error: expected message, received {:?}", other), + )))), + None => Poll::Ready(None), + } + } +} + +impl Sink for ECIESStream +where + Io: AsyncWrite + Unpin, +{ + type Error = io::Error; + + fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().stream).poll_ready(cx) + } + + fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> { + let this = self.get_mut(); + Pin::new(&mut this.stream).start_send(EgressECIESValue::Message(item))?; + + Ok(()) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().stream).poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().stream).poll_close(cx) + } +} + +#[cfg(test)] +mod tests { + use secp256k1::{rand, SECP256K1}; + use tokio::net::TcpListener; + + use crate::util::pk2id; + + use super::*; + + #[tokio::test] + // TODO: implement test for the proposed + // API: https://github.com/foundry-rs/reth/issues/64#issue-1408708420 + async fn can_write_and_read() { + let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); + let server_key = SecretKey::new(&mut rand::thread_rng()); + + let handle = tokio::spawn(async move { + // roughly based off of the design of tokio::net::TcpListener + let (incoming, _) = listener.accept().await.unwrap(); + let mut stream = ECIESStream::incoming(incoming, server_key).await.unwrap(); + + // use the stream to get the next messagse + let message = stream.next().await.unwrap().unwrap(); + assert_eq!(message, Bytes::from("hello")); + }); + + // create the server pubkey + let server_id = pk2id(&server_key.public_key(SECP256K1)); + + let client_key = SecretKey::new(&mut rand::thread_rng()); + let outgoing = TcpStream::connect("127.0.0.1:8080").await.unwrap(); + let mut client_stream = + ECIESStream::connect(outgoing, client_key, server_id).await.unwrap(); + client_stream.send(Bytes::from("hello")).await.unwrap(); + + // make sure the server receives the message and asserts before ending the test + handle.await.unwrap(); + } +} diff --git a/crates/net/ecies/src/util.rs b/crates/net/ecies/src/util.rs new file mode 100644 index 000000000000..bd5ee5d97905 --- /dev/null +++ b/crates/net/ecies/src/util.rs @@ -0,0 +1,51 @@ +use hmac::{Hmac, Mac}; +use reth_primitives::{H256, H512 as PeerId}; +use secp256k1::PublicKey; +use sha2::{Digest, Sha256}; + +/// Hashes the input data with SHA256. +pub(crate) fn sha256(data: &[u8]) -> H256 { + H256::from(Sha256::digest(data).as_ref()) +} + +/// Produces a HMAC_SHA256 digest of the `input_data` and `auth_data` with the given `key`. +/// This is done by accumulating each slice in `input_data` into the HMAC state, then accumulating +/// the `auth_data` and returning the resulting digest. +pub(crate) fn hmac_sha256(key: &[u8], input: &[&[u8]], auth_data: &[u8]) -> H256 { + let mut hmac = Hmac::::new_from_slice(key).unwrap(); + for input in input { + hmac.update(input); + } + hmac.update(auth_data); + H256::from_slice(&hmac.finalize().into_bytes()) +} + +/// Converts a [secp256k1::PublicKey] to a [PeerId] by stripping the +/// SECP256K1_TAG_PUBKEY_UNCOMPRESSED tag and storing the rest of the slice in the [PeerId]. +pub(crate) fn pk2id(pk: &PublicKey) -> PeerId { + PeerId::from_slice(&pk.serialize_uncompressed()[1..]) +} + +/// Converts a [PeerId] to a [secp256k1::PublicKey] by prepending the [PeerId] bytes with the +/// SECP256K1_TAG_PUBKEY_UNCOMPRESSED tag. +pub(crate) fn id2pk(id: PeerId) -> Result { + let mut s = [0_u8; 65]; + // SECP256K1_TAG_PUBKEY_UNCOMPRESSED = 0x04 + // see: https://github.com/bitcoin-core/secp256k1/blob/master/include/secp256k1.h#L211 + s[0] = 4; + s[1..].copy_from_slice(id.as_bytes()); + PublicKey::from_slice(&s) +} + +#[cfg(test)] +mod tests { + use super::*; + use secp256k1::{SecretKey, SECP256K1}; + + #[test] + fn pk2id2pk() { + let prikey = SecretKey::new(&mut secp256k1::rand::thread_rng()); + let pubkey = PublicKey::from_secret_key(SECP256K1, &prikey); + assert_eq!(pubkey, id2pk(pk2id(&pubkey)).unwrap()); + } +}