diff --git a/.cspell.jsonc b/.cspell.jsonc index c912085..07671ff 100644 --- a/.cspell.jsonc +++ b/.cspell.jsonc @@ -22,6 +22,7 @@ "secp", "secp256k1", "ssse", + "struct", "symm", "typenum" ], diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5cde165..910241a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,5 +3,5 @@ updates: - package-ecosystem: cargo directory: "/" schedule: - interval: daily + interval: monthly open-pull-requests-limit: 10 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..92cfd4f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +## 0.2.1 ~ 0.2.4 + +- Add configuration for more compatibility +- Revamp error handling +- Migrate to edition 2021 +- Bump dependencies + +## 0.2.0 + +- Revamp documentation +- Optional pure Rust AES backend +- WASM compatibility + +## 0.1.1 ~ 0.1.5 + +- Bump dependencies +- Update documentation +- Fix error handling + +## 0.1.0 + +- First beta version release diff --git a/Cargo.toml b/Cargo.toml index d183a02..a84e98a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ rand = {version = "0.8.5"} [features] default = ["openssl"] pure = ["aes-gcm", "typenum"] +# default: 16 bytes +aes_12bytes_nonce = [] [dev-dependencies] criterion = {version = "0.5.1", default-features = false} diff --git a/README.md b/README.md index 858554b..359e0b3 100644 --- a/README.md +++ b/README.md @@ -133,26 +133,38 @@ Found 1 outliers among 10 measurements (10.00%) 1 (10.00%) high mild ``` -## Release Notes +## Configuration -### 0.2.1 ~ 0.2.4 +You can enable 12 bytes nonce by specify `aes_12bytes_nonce` feature. -- Revamp error handling -- Migrate to edition 2021 -- Bump dependencies +```toml +ecies = {version = "0.2", default-features = false, features = ["aes_12bytes_nonce"]} +``` + +Other behaviors can be configured by global static variable: -### 0.2.0 +```rs +pub struct Config { + pub is_ephemeral_key_compressed: bool, + pub is_hkdf_key_compressed: bool, + pub symmetric_algorithm: SymmetricAlgorithm, +} +``` + +If you set `is_ephemeral_key_compressed: true`, the payload would be like: `33 Bytes + AES` instead of `65 Bytes + AES`. -- Revamp documentation -- Optional pure Rust AES backend -- WASM compatibility +If you set `is_hkdf_key_compressed: true`, the hkdf key would be derived from `ephemeral public key (compressed) + shared public key (compressed)` instead of `ephemeral public key (uncompressed) + shared public key (uncompressed)`. -### 0.1.1 ~ 0.1.5 +```rs +update_config(Config { + is_ephemeral_key_compressed: true, + is_hkdf_key_compressed: true, + ..Config::default() +}); +``` -- Bump dependencies -- Update documentation -- Fix error handling +For compatibility, make sure different applications share the same configuration. -### 0.1.0 +## Changelog -- First beta version release +See [CHANGELOG.md](./CHANGELOG.md). diff --git a/src/config.rs b/src/config.rs index 27a2d9f..d705978 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,27 +4,20 @@ use once_cell::sync::Lazy; use crate::consts::{COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE}; +#[derive(Default)] pub enum SymmetricAlgorithm { + #[default] Aes256Gcm, } +#[derive(Default)] pub struct Config { pub is_ephemeral_key_compressed: bool, pub is_hkdf_key_compressed: bool, pub symmetric_algorithm: SymmetricAlgorithm, } -impl Config { - pub fn default() -> Self { - Config { - is_ephemeral_key_compressed: false, - is_hkdf_key_compressed: false, - symmetric_algorithm: SymmetricAlgorithm::Aes256Gcm, - } - } -} - -/// Global config +/// Global config variable pub static ECIES_CONFIG: Lazy> = Lazy::new(|| { let config: Config = Config::default(); Mutex::new(config) diff --git a/src/consts.rs b/src/consts.rs index 365bc53..936fe3e 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,10 +1,16 @@ pub use libsecp256k1::util::{COMPRESSED_PUBLIC_KEY_SIZE, FULL_PUBLIC_KEY_SIZE as UNCOMPRESSED_PUBLIC_KEY_SIZE}; -/// AES IV/nonce length -pub const AES_IV_LENGTH: usize = 16; -/// AES tag length -pub const AES_TAG_LENGTH: usize = 16; -/// AES IV + tag length -pub const AES_IV_PLUS_TAG_LENGTH: usize = AES_IV_LENGTH + AES_TAG_LENGTH; +/// AES nonce length +#[cfg(not(feature = "aes_12bytes_nonce"))] +pub const AES_NONCE_LENGTH: usize = 16; +#[cfg(feature = "aes_12bytes_nonce")] +pub const AES_NONCE_LENGTH: usize = 12; + +/// AEAD tag length +pub const AEAD_TAG_LENGTH: usize = 16; + +/// Nonce and tag length +pub const NONCE_TAG_LENGTH: usize = AES_NONCE_LENGTH + AEAD_TAG_LENGTH; + /// Empty bytes array pub const EMPTY_BYTES: [u8; 0] = []; diff --git a/src/lib.rs b/src/lib.rs index b1d6123..85f58da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,57 +190,6 @@ mod tests { let (sk, pk) = (&sk.serialize(), &pk.serialize_compressed()); test_enc_dec_big(sk, pk); } - - #[test] - #[cfg(not(target_arch = "wasm32"))] - fn test_against_python() { - use futures_util::FutureExt; - use hex::encode; - use tokio::runtime::Runtime; - - use utils::tests::decode_hex; - - const PYTHON_BACKEND: &str = "https://eciespydemo-1-d5397785.deta.app/"; - - let (sk, pk) = generate_keypair(); - - let sk_hex = encode(&sk.serialize().to_vec()); - let uncompressed_pk = &pk.serialize(); - let pk_hex = encode(uncompressed_pk.to_vec()); - - let client = reqwest::Client::new(); - let params = [("data", MSG), ("pub", pk_hex.as_str())]; - - let rt = Runtime::new().unwrap(); - let res = rt - .block_on( - client - .post(PYTHON_BACKEND) - .form(¶ms) - .send() - .then(|r| r.unwrap().text()), - ) - .unwrap(); - - let server_encrypted = decode_hex(&res); - let local_decrypted = decrypt(&sk.serialize(), server_encrypted.as_slice()).unwrap(); - assert_eq!(local_decrypted, MSG.as_bytes()); - - let local_encrypted = encrypt(uncompressed_pk, MSG.as_bytes()).unwrap(); - let params = [("data", encode(local_encrypted)), ("prv", sk_hex)]; - - let res = rt - .block_on( - client - .post(PYTHON_BACKEND) - .form(¶ms) - .send() - .then(|r| r.unwrap().text()), - ) - .unwrap(); - - assert_eq!(res, MSG); - } } #[cfg(all(test, target_arch = "wasm32"))] diff --git a/src/openssl_aes.rs b/src/openssl_aes.rs index e40b0e1..fe474ca 100644 --- a/src/openssl_aes.rs +++ b/src/openssl_aes.rs @@ -1,19 +1,19 @@ use openssl::symm::{decrypt_aead, encrypt_aead, Cipher}; use rand::{thread_rng, Rng}; -use crate::consts::{AES_IV_LENGTH, AES_IV_PLUS_TAG_LENGTH, AES_TAG_LENGTH, EMPTY_BYTES}; +use crate::consts::{AEAD_TAG_LENGTH, AES_NONCE_LENGTH, EMPTY_BYTES, NONCE_TAG_LENGTH}; /// AES-256-GCM encryption wrapper pub fn aes_encrypt(key: &[u8], msg: &[u8]) -> Option> { let cipher = Cipher::aes_256_gcm(); - let mut iv = [0u8; AES_IV_LENGTH]; + let mut iv = [0u8; AES_NONCE_LENGTH]; thread_rng().fill(&mut iv); - let mut tag = [0u8; AES_TAG_LENGTH]; + let mut tag = [0u8; AEAD_TAG_LENGTH]; if let Ok(encrypted) = encrypt_aead(cipher, key, Some(&iv), &EMPTY_BYTES, msg, &mut tag) { - let mut output = Vec::with_capacity(AES_IV_PLUS_TAG_LENGTH + encrypted.len()); + let mut output = Vec::with_capacity(NONCE_TAG_LENGTH + encrypted.len()); output.extend(&iv); output.extend(&tag); output.extend(encrypted); @@ -26,15 +26,15 @@ pub fn aes_encrypt(key: &[u8], msg: &[u8]) -> Option> { /// AES-256-GCM decryption wrapper pub fn aes_decrypt(key: &[u8], encrypted_msg: &[u8]) -> Option> { - if encrypted_msg.len() < AES_IV_PLUS_TAG_LENGTH { + if encrypted_msg.len() < NONCE_TAG_LENGTH { return None; } let cipher = Cipher::aes_256_gcm(); - let iv = &encrypted_msg[..AES_IV_LENGTH]; - let tag = &encrypted_msg[AES_IV_LENGTH..AES_IV_PLUS_TAG_LENGTH]; - let encrypted = &encrypted_msg[AES_IV_PLUS_TAG_LENGTH..]; + let iv = &encrypted_msg[..AES_NONCE_LENGTH]; + let tag = &encrypted_msg[AES_NONCE_LENGTH..NONCE_TAG_LENGTH]; + let encrypted = &encrypted_msg[NONCE_TAG_LENGTH..]; decrypt_aead(cipher, key, Some(iv), &EMPTY_BYTES, encrypted, tag).ok() } diff --git a/src/pure_aes.rs b/src/pure_aes.rs index 0041339..8c76394 100644 --- a/src/pure_aes.rs +++ b/src/pure_aes.rs @@ -1,19 +1,25 @@ use aes_gcm::aead::{generic_array::GenericArray, AeadInPlace}; use aes_gcm::{aes::Aes256, AesGcm, KeyInit}; use rand::{thread_rng, Rng}; -use typenum::consts::U16; +#[allow(unused_imports)] +use typenum::consts::{U12, U16}; -use crate::consts::{AES_IV_LENGTH, AES_IV_PLUS_TAG_LENGTH, EMPTY_BYTES}; +use crate::consts::{AES_NONCE_LENGTH, EMPTY_BYTES, NONCE_TAG_LENGTH}; /// AES-256-GCM with 16 bytes Nonce/IV +#[cfg(not(feature = "aes_12bytes_nonce"))] pub type Aes256Gcm = AesGcm; +/// AES-256-GCM with 12 bytes Nonce/IV +#[cfg(feature = "aes_12bytes_nonce")] +pub type Aes256Gcm = AesGcm; + /// AES-256-GCM encryption wrapper pub fn aes_encrypt(key: &[u8], msg: &[u8]) -> Option> { - let key = GenericArray::from_slice(key); + let key: &GenericArray = GenericArray::from_slice(key); let aead = Aes256Gcm::new(key); - let mut iv = [0u8; AES_IV_LENGTH]; + let mut iv = [0u8; AES_NONCE_LENGTH]; thread_rng().fill(&mut iv); let nonce = GenericArray::from_slice(&iv); @@ -22,8 +28,8 @@ pub fn aes_encrypt(key: &[u8], msg: &[u8]) -> Option> { out.extend(msg); if let Ok(tag) = aead.encrypt_in_place_detached(nonce, &EMPTY_BYTES, &mut out) { - let mut output = Vec::with_capacity(AES_IV_PLUS_TAG_LENGTH + msg.len()); - output.extend(&iv); + let mut output = Vec::with_capacity(NONCE_TAG_LENGTH + msg.len()); + output.extend(nonce); output.extend(tag); output.extend(out); Some(output) @@ -34,18 +40,18 @@ pub fn aes_encrypt(key: &[u8], msg: &[u8]) -> Option> { /// AES-256-GCM decryption wrapper pub fn aes_decrypt(key: &[u8], encrypted_msg: &[u8]) -> Option> { - if encrypted_msg.len() < AES_IV_PLUS_TAG_LENGTH { + if encrypted_msg.len() < NONCE_TAG_LENGTH { return None; } let key = GenericArray::from_slice(key); let aead = Aes256Gcm::new(key); - let iv = GenericArray::from_slice(&encrypted_msg[..AES_IV_LENGTH]); - let tag = GenericArray::from_slice(&encrypted_msg[AES_IV_LENGTH..AES_IV_PLUS_TAG_LENGTH]); + let iv = GenericArray::from_slice(&encrypted_msg[..AES_NONCE_LENGTH]); + let tag = GenericArray::from_slice(&encrypted_msg[AES_NONCE_LENGTH..NONCE_TAG_LENGTH]); - let mut out = Vec::with_capacity(encrypted_msg.len() - AES_IV_PLUS_TAG_LENGTH); - out.extend(&encrypted_msg[AES_IV_PLUS_TAG_LENGTH..]); + let mut out = Vec::with_capacity(encrypted_msg.len() - NONCE_TAG_LENGTH); + out.extend(&encrypted_msg[NONCE_TAG_LENGTH..]); if let Ok(_) = aead.decrypt_in_place_detached(iv, &EMPTY_BYTES, &mut out, tag) { Some(out) diff --git a/src/utils.rs b/src/utils.rs index 5964ef9..981bdae 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -68,7 +68,7 @@ pub(crate) mod tests { use hex::decode; use super::*; - use crate::consts::{AES_IV_LENGTH, EMPTY_BYTES}; + use crate::consts::EMPTY_BYTES; /// Remove 0x prefix of a hex string pub fn remove0x(hex: &str) -> &str { @@ -102,8 +102,7 @@ pub(crate) mod tests { #[test] fn test_attempt_to_decrypt_invalid_message() { assert!(aes_decrypt(&[], &[]).is_none()); - - assert!(aes_decrypt(&[], &[0; AES_IV_LENGTH]).is_none()); + assert!(aes_decrypt(&[], &[0; 16]).is_none()); } #[test] @@ -129,6 +128,7 @@ pub(crate) mod tests { } #[test] + #[cfg(not(feature = "aes_12bytes_nonce"))] fn test_aes_known_key() { let text = b"helloworld"; let key = decode_hex("0000000000000000000000000000000000000000000000000000000000000000"); diff --git a/tests/integration.rs b/tests/integration.rs index d80377c..dc51925 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,13 +1,12 @@ use ecies::{ - config::{reset_config, update_config, Config, SymmetricAlgorithm}, + config::{reset_config, update_config, Config}, decrypt, encrypt, utils::{decapsulate, encapsulate, generate_keypair}, PublicKey, SecretKey, }; - use hex::decode; -const MSG: &[u8] = "helloworld".as_bytes(); +const MSG: &str = "helloworld🌍"; #[test] fn can_change_behavior_with_config() { @@ -22,9 +21,8 @@ fn can_change_behavior_with_config() { let pk3 = PublicKey::from_secret_key(&sk3); update_config(Config { - is_ephemeral_key_compressed: false, is_hkdf_key_compressed: true, - symmetric_algorithm: SymmetricAlgorithm::Aes256Gcm, + ..Config::default() }); assert_eq!(encapsulate(&sk2, &pk3), decapsulate(&pk2, &sk3)); @@ -37,13 +35,65 @@ fn can_change_behavior_with_config() { update_config(Config { is_ephemeral_key_compressed: true, is_hkdf_key_compressed: true, - symmetric_algorithm: SymmetricAlgorithm::Aes256Gcm, + ..Config::default() }); let (sk, pk) = generate_keypair(); let (sk, pk) = (&sk.serialize(), &pk.serialize_compressed()); - assert_eq!(MSG, decrypt(sk, &encrypt(pk, MSG).unwrap()).unwrap().as_slice()); + assert_eq!( + MSG.as_bytes(), + decrypt(sk, &encrypt(pk, MSG.as_bytes()).unwrap()).unwrap().as_slice() + ); reset_config(); } + +#[test] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "aes_12bytes_nonce")))] +fn is_compatible_with_python() { + use futures_util::FutureExt; + use hex::encode; + use tokio::runtime::Runtime; + + const PYTHON_BACKEND: &str = "https://eciespydemo-1-d5397785.deta.app/"; + + let (sk, pk) = generate_keypair(); + + let sk_hex = encode(sk.serialize()); + let uncompressed_pk = &pk.serialize(); + let pk_hex = encode(uncompressed_pk); + + let client = reqwest::Client::new(); + let params = [("data", MSG), ("pub", pk_hex.as_str())]; + + let rt = Runtime::new().unwrap(); + let res = rt + .block_on( + client + .post(PYTHON_BACKEND) + .form(¶ms) + .send() + .then(|r| r.unwrap().text()), + ) + .unwrap(); + + let server_encrypted = decode(res).unwrap(); + let local_decrypted = decrypt(&sk.serialize(), server_encrypted.as_slice()).unwrap(); + assert_eq!(local_decrypted, MSG.as_bytes()); + + let local_encrypted = encrypt(uncompressed_pk, MSG.as_bytes()).unwrap(); + let params = [("data", encode(local_encrypted)), ("prv", sk_hex)]; + + let res = rt + .block_on( + client + .post(PYTHON_BACKEND) + .form(¶ms) + .send() + .then(|r| r.unwrap().text()), + ) + .unwrap(); + + assert_eq!(res.as_bytes(), MSG.as_bytes()); +}