mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(net): add Framed ECIES implementation (#80)
* feat(net): add ecies crate * cargo fmt * add hex-literal and proptest to dev-dependencies * adds std feature to reth-rlp * document util * document more * chore: allow missing docs in ecies/algorith.rs * feat(ecies): improve error handling remove anyhow and strictly type all errors * refactor(ecies): movem ingress/egress to lib.rs * chore(ecies): allow missing docs in mac * chore(ecies): cleanup utils * refactor(ecies): move ECIES Codec to separate file * refactor(ecies): rename proto to stream * add test scaffold * implement server/client read/write test * chore: clippy / fmt Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
201
Cargo.lock
generated
201
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
43
crates/net/ecies/Cargo.toml
Normal file
43
crates/net/ecies/Cargo.toml
Normal file
@ -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"
|
||||
766
crates/net/ecies/src/algorithm.rs
Normal file
766
crates/net/ecies/src/algorithm.rs
Normal file
@ -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<PublicKey>,
|
||||
|
||||
pub(crate) remote_id: Option<PeerId>,
|
||||
|
||||
#[educe(Debug(ignore))]
|
||||
ephemeral_secret_key: SecretKey,
|
||||
ephemeral_public_key: PublicKey,
|
||||
ephemeral_shared_secret: Option<H256>,
|
||||
remote_ephemeral_public_key: Option<PublicKey>,
|
||||
|
||||
nonce: H256,
|
||||
remote_nonce: Option<H256>,
|
||||
|
||||
#[educe(Debug(ignore))]
|
||||
ingress_aes: Option<Ctr64BE<Aes256>>,
|
||||
#[educe(Debug(ignore))]
|
||||
egress_aes: Option<Ctr64BE<Aes256>>,
|
||||
ingress_mac: Option<MAC>,
|
||||
egress_mac: Option<MAC>,
|
||||
|
||||
init_msg: Option<Bytes>,
|
||||
remote_init_msg: Option<Bytes>,
|
||||
|
||||
body_size: Option<usize>,
|
||||
}
|
||||
|
||||
fn split_at_mut<T>(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<Self, ECIESError> {
|
||||
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<Self, ECIESError> {
|
||||
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<Self, ECIESError> {
|
||||
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<Self, ECIESError> {
|
||||
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::<Aes128>::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::<Aes128>::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::<Aes256>::new(aes_secret.as_ref().into(), iv.as_ref().into()));
|
||||
self.egress_aes =
|
||||
Some(Ctr64BE::<Aes256>::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<usize, ECIESError> {
|
||||
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::<BigEndian>(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();
|
||||
}
|
||||
}
|
||||
152
crates/net/ecies/src/codec.rs
Normal file
152
crates/net/ecies/src/codec.rs
Normal file
@ -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<Self, ECIESError> {
|
||||
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<Self, ECIESError> {
|
||||
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<Option<Self::Item>, 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<EgressECIESValue> 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
crates/net/ecies/src/error.rs
Normal file
48
crates/net/ecies/src/error.rs
Normal file
@ -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<IngressECIESValue>,
|
||||
},
|
||||
}
|
||||
42
crates/net/ecies/src/lib.rs
Normal file
42
crates/net/ecies/src/lib.rs
Normal file
@ -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),
|
||||
}
|
||||
53
crates/net/ecies/src/mac.rs
Normal file
53
crates/net/ecies/src/mac.rs
Normal file
@ -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<u8, U16>;
|
||||
|
||||
#[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::<NoPadding>(&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::<NoPadding>(&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])
|
||||
}
|
||||
}
|
||||
187
crates/net/ecies/src/stream.rs
Normal file
187
crates/net/ecies/src/stream.rs
Normal file
@ -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<Io> {
|
||||
stream: Framed<Io, ECIESCodec>,
|
||||
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<SocketAddr>;
|
||||
}
|
||||
|
||||
impl HasRemoteAddr for TcpStream {
|
||||
fn remote_addr(&self) -> Option<SocketAddr> {
|
||||
self.peer_addr().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io> ECIESStream<Io>
|
||||
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<Self, ECIESError> {
|
||||
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<Self, ECIESError> {
|
||||
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<Io> Stream for ECIESStream<Io>
|
||||
where
|
||||
Io: AsyncRead + Unpin,
|
||||
{
|
||||
type Item = Result<Bytes, io::Error>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
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<Io> Sink<Bytes> for ECIESStream<Io>
|
||||
where
|
||||
Io: AsyncWrite + Unpin,
|
||||
{
|
||||
type Error = io::Error;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
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<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.get_mut().stream).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
51
crates/net/ecies/src/util.rs
Normal file
51
crates/net/ecies/src/util.rs
Normal file
@ -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::<Sha256>::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<PublicKey, secp256k1::Error> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user