Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swisstronik encryption support #11

Open
wants to merge 13 commits into
base: hyperlane
Choose a base branch
from
237 changes: 164 additions & 73 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ members = [
"ethers-etherscan",
"ethers-solc",
"examples/ethers-wasm",
"ethers-encryption",
]

default-members = [
Expand All @@ -33,6 +34,7 @@ default-members = [
"ethers-middleware",
"ethers-etherscan",
"ethers-solc",
"ethers-encryption",
]

[package.metadata.docs.rs]
Expand Down
27 changes: 27 additions & 0 deletions ethers-encryption/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "ethers-encryption"
version = "1.0.0"
edition = "2021"
rust-version = "1.62"
authors = ["Mike Antonuk <[email protected]>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
documentation = "https://docs.rs/ethers"
repository = "https://github.com/SigmaGmbH/ethers-rs"
homepage = "https://docs.rs/ethers"
description = "Complete Ethereum library and wallet implementation in Rust."

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
deoxys = "0.1.0"
rand = "0.8.5"
sha2 = "0.10.7"
hmac = "0.12.1"
x25519-dalek = { version = "2.0.0", features = ["static_secrets"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] }
reqwest = { version = "0.11.13", default-features = false, features = ["json"] }
serde = { version = "1.0.124", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.64", default-features = false, features = ["raw_value"] }
3 changes: 3 additions & 0 deletions ethers-encryption/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# <h1 align="center"> ethers.rs </h1>

**Toolkit used for encryption and decryption in Swisstronik network**
32 changes: 32 additions & 0 deletions ethers-encryption/src/derivation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use deoxys::aead::KeyInit;
use sha2::Sha256;
use hmac::{Hmac, Mac};

use crate::KEY_SIZE;

type HmacSha256 = Hmac<Sha256>;

/// Converts provided x25519 private key to public key
pub fn x25519_private_to_public(private_key: [u8; KEY_SIZE]) -> [u8; KEY_SIZE] {
let secret = x25519_dalek::StaticSecret::from(private_key);
let public_key = x25519_dalek::PublicKey::from(&secret);
public_key.to_bytes()
}

/// Performs Diffie-Hellman derivation of encryption key for transaction encryption
/// * public_key – User public key
/// Returns shared secret which can be used for derivation of encryption key
pub fn derive_shared_secret(private_key: [u8; KEY_SIZE], public_key: [u8; KEY_SIZE]) -> x25519_dalek::SharedSecret {
let secret = x25519_dalek::StaticSecret::from(private_key);
secret.diffie_hellman(&x25519_dalek::PublicKey::from(public_key))
}

/// Derives encryption key using KDF
pub fn derive_encryption_key(private_key: &[u8], salt: &[u8]) -> [u8; KEY_SIZE] {
let mut kdf = <HmacSha256 as KeyInit>::new_from_slice(salt).unwrap();
kdf.update(private_key);
let mut derived_key = [0u8; KEY_SIZE];
let digest = kdf.finalize();
derived_key.copy_from_slice(&digest.into_bytes()[..KEY_SIZE]);
derived_key
}
80 changes: 80 additions & 0 deletions ethers-encryption/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use hmac::{Mac};
use rand::{rngs::OsRng, RngCore};
use deoxys::aead::generic_array::GenericArray;
use deoxys::aead::{Aead, KeyInit, Payload};
use deoxys::DeoxysII256;

use crate::{KEY_SIZE, TX_KEY_PREFIX, NONCE_SIZE, TAG_SIZE};

use crate::derivation::{
derive_shared_secret,
derive_encryption_key,
x25519_private_to_public,
};

pub fn encrypt_ecdh(
private_key: [u8; KEY_SIZE],
node_public_key: [u8; KEY_SIZE],
data: &[u8],
) -> Result<Vec<u8>, deoxys::Error> {
let shared_secret = derive_shared_secret(private_key, node_public_key);
let salt = TX_KEY_PREFIX.as_bytes();
let encryption_key = derive_encryption_key(shared_secret.as_bytes(), salt);

// Append encryption public key
let encrypted_data = deoxys_encrypt(&encryption_key, data)?;
let public_key = x25519_private_to_public(private_key);
let mut result = Vec::<u8>::new();
result.extend_from_slice(&public_key);
result.extend(encrypted_data);

Ok(result)
}

pub fn decrypt_ecdh(
private_key: [u8; KEY_SIZE],
node_public_key: [u8; KEY_SIZE],
encrypted_data: &[u8],
) -> Result<Vec<u8>, deoxys::Error> {
let shared_secret = derive_shared_secret(private_key, node_public_key);
let salt = TX_KEY_PREFIX.as_bytes();
let encryption_key = derive_encryption_key(shared_secret.as_bytes(), salt);
deoxys_decrypt(&encryption_key, encrypted_data)
}

pub fn deoxys_encrypt(
private_key: &[u8; KEY_SIZE],
data: &[u8],
) -> Result<Vec<u8>, deoxys::Error> {
let mut rng = OsRng;
let mut aad = [0u8; TAG_SIZE];
rng.fill_bytes(&mut aad);
let mut nonce = [0u8; NONCE_SIZE];
rng.fill_bytes(&mut nonce);
let nonce = GenericArray::from_slice(&nonce);
let payload = Payload {
msg: data,
aad: &aad,
};
let key = GenericArray::from_slice(private_key);
let encrypted = DeoxysII256::new(key).encrypt(nonce, payload);
match encrypted {
Ok(ciphertext) => {
let encrypted_data = [&nonce, aad.as_slice(), ciphertext.as_slice()].concat();
Ok(encrypted_data)
}
Err(e) => Err(e)
}
}

pub fn deoxys_decrypt(
private_key: &[u8; KEY_SIZE],
encrypted_data: &[u8],
) -> Result<Vec<u8>, deoxys::Error> {
let nonce = &encrypted_data[0..NONCE_SIZE];
let aad = &encrypted_data[NONCE_SIZE..NONCE_SIZE + TAG_SIZE];
let ciphertext = &encrypted_data[NONCE_SIZE + TAG_SIZE..];
let payload = Payload { msg: ciphertext, aad };
let key = GenericArray::from_slice(private_key);
DeoxysII256::new(key).decrypt(GenericArray::from_slice(nonce), payload)
}
86 changes: 86 additions & 0 deletions ethers-encryption/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use serde::{Deserialize, Serialize};
use rand::{rngs::OsRng, RngCore};
use std::convert::TryInto;

pub mod derivation;
pub mod encryption;

/// Salt for derivation of transaction encryption key
pub const TX_KEY_PREFIX: &str = "IOEncryptionKeyV1";
/// Salt for derivation of user key material
pub const USER_KEY_PREFIX: &str = "UserEncryptionKeyV1";
/// Size of `tag` for DEOXYS-II encryption
pub const TAG_SIZE: usize = 16;
/// Size of `nonce` for DEOXYS-II encryption
pub const NONCE_SIZE: usize = 15;
/// Default size of private / public key
pub const KEY_SIZE: usize = 32;

/// Encrypts provided transaction or call data field
///
/// * node_url - URL of JSON-RPC to obtain node public key
/// * data – raw data to encrypt
///
/// Returns Some(encrypted_data, used_key) if encryption was successful, returns None in case
/// of error
pub async fn encrypt_data(
node_public_key: [u8; 32],
data: &[u8],
) -> Option<(Vec<u8>, [u8; KEY_SIZE])> {
// Generate random encryption key
let mut rng = OsRng;
let mut key_material = [0u8; KEY_SIZE];
rng.fill_bytes(&mut key_material);

// Derive encryption key
let encryption_key = derivation::derive_encryption_key(
&key_material,
USER_KEY_PREFIX.as_bytes(),
);

// Encrypt data
let encrypted = encryption::encrypt_ecdh(
encryption_key.clone(),
node_public_key,
data,
);

match encrypted {
Ok(res) => Some((res.to_vec(), encryption_key)),
Err(err) => {
println!("Cannot encrypt transaction data. Reason: {:?}", err);
None
}
}
}

/// Decrypts provided ciphertext, received as a node response
///
/// * node_url – URL of JSON-RPC to obtain node public key
/// * encryption_key – key, used during encryption of raw data
/// * data – ciphertext, received from node
///
/// Returns Some(decrypted_data) in case of success, otherwise returns None
pub async fn decrypt_data(node_public_key: [u8; 32], encryption_key: [u8; KEY_SIZE], data: &[u8]) -> Option<Vec<u8>> {
// Decrypt data
let decrypted = encryption::decrypt_ecdh(
encryption_key,
node_public_key,
data,
);

match decrypted {
Ok(res) => Some(res.to_vec()),
Err(err) => {
println!("Cannot decrypt node response. Reason: {:?}", err);
None
}
}
}

pub fn convert_to_fixed_size_array(data: Vec<u8>) -> [u8; KEY_SIZE] {
let mut fixed_array = [0u8; KEY_SIZE];
fixed_array.copy_from_slice(&data[..KEY_SIZE]);
fixed_array
}

2 changes: 2 additions & 0 deletions ethers-middleware/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features =
ethers-etherscan = { version = "^1.0.0", path = "../ethers-etherscan", default-features = false }
ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-features = false }
ethers-signers = { version = "^1.0.0", path = "../ethers-signers", default-features = false }
ethers-encryption = { version = "^1.0.0", path = "../ethers-encryption", default-features = false , optional = true}

async-trait = { version = "0.1.50", default-features = false }
auto_impl = { version = "0.5.0", default-features = false }
Expand Down Expand Up @@ -56,3 +57,4 @@ tokio = { version = "1.18", default-features = false, features = ["rt", "macros"

[features]
celo = ["ethers-core/celo", "ethers-providers/celo", "ethers-signers/celo", "ethers-contract/celo"]
swisstronik = ["ethers-providers/swisstronik", "ethers-encryption"]
48 changes: 48 additions & 0 deletions ethers-middleware/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ use std::convert::TryFrom;
use async_trait::async_trait;
use thiserror::Error;

#[cfg(feature = "swisstronik")]
use ethers_providers::SwisstronikMiddleware;


#[derive(Clone, Debug)]
/// Middleware used for locally signing transactions, compatible with any implementer
/// of the [`Signer`] trait.
Expand Down Expand Up @@ -99,6 +103,12 @@ pub enum SignerMiddlewareError<M: Middleware, S: Signer> {
/// Thrown if the signer's chain_id is different than the chain_id of the transaction
#[error("specified chain_id is different than the signer's chain_id")]
DifferentChainID,
/// Thrown if during encryption we couldn't obtain node public key
#[error("failed to get node public key")]
FailedNodePublicKey,
/// Thrown if during encryption there was an error
#[error("failed to encrypt")]
FailedToEncrypt,
}

// Helper functions for locally signing transactions
Expand All @@ -125,6 +135,7 @@ where
/// If the transaction does not have a chain id set, it sets it to the signer's chain id.
/// Returns an error if the transaction's existing chain id does not match the signer's chain
/// id.
#[cfg(not(feature = "swisstronik"))]
async fn sign_transaction(
&self,
mut tx: TypedTransaction,
Expand All @@ -149,6 +160,43 @@ where
Ok(tx.rlp_signed(&signature))
}

#[cfg(feature = "swisstronik")]
async fn sign_transaction(
&self,
mut tx: TypedTransaction,
) -> Result<Bytes, SignerMiddlewareError<M, S>> {
// compare chain_id and use signer's chain_id if the tranasaction's chain_id is None,
// return an error if they are not consistent
let chain_id = self.signer.chain_id();
match tx.chain_id() {
Some(id) if id.as_u64() != chain_id => {
return Err(SignerMiddlewareError::DifferentChainID)
}
None => {
tx.set_chain_id(chain_id);
}
_ => {}
}

if tx.to().is_some() && tx.data().is_some() {
// Encryption transaction data in case of swisstronik network
let node_enc_key = self.provider().get_node_public_key().await.map_err(|_| SignerMiddlewareError::FailedNodePublicKey)?;
// Encrypt call data in case of Swisstronik network
let (encrypted_data, _) = ethers_encryption::encrypt_data(node_enc_key, tx.data().unwrap())
.await.ok_or(SignerMiddlewareError::FailedToEncrypt)?;

// Update call data
tx.set_data(encrypted_data.into());
}

let signature =
self.signer.sign_transaction(&tx).await.map_err(SignerMiddlewareError::SignerError)?;

// Return the raw rlp-encoded signed transaction
Ok(tx.rlp_signed(&signature))
}


/// Returns the client's address
pub fn address(&self) -> Address {
self.address
Expand Down
2 changes: 2 additions & 0 deletions ethers-providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features = false }
ethers-encryption = { version = "^1.0.0", path = "../ethers-encryption", default-features = false , optional = true}

async-trait = { version = "0.1.50", default-features = false }
hex = { version = "0.4.3", default-features = false, features = ["std"] }
Expand Down Expand Up @@ -68,6 +69,7 @@ tempfile = "3.3.0"
[features]
default = ["ws", "rustls"]
celo = ["ethers-core/celo"]
swisstronik = [ "ethers-encryption"]
ws = ["tokio-tungstenite", "futures-channel"]
ipc = ["tokio/io-util", "bytes", "futures-channel"]

Expand Down
11 changes: 11 additions & 0 deletions ethers-providers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,17 @@ pub trait CeloMiddleware: Middleware {
}
}

#[cfg(feature = "swisstronik")]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SwisstronikMiddleware: Middleware {
async fn get_node_public_key(
&self
) -> Result<[u8;32], ProviderError> {
self.provider().get_node_public_key().await.map_err(FromErr::from)
}
}

pub use test_provider::{GOERLI, MAINNET, ROPSTEN};

/// Pre-instantiated Infura HTTP clients which rotate through multiple API keys
Expand Down
Loading