diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/.cspell.jsonc b/.cspell.jsonc index 9a6662c..d9a68d2 100644 --- a/.cspell.jsonc +++ b/.cspell.jsonc @@ -32,9 +32,7 @@ // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. // For example "hte" should be "the" - "flagWords": [ - "hte" - ], + "flagWords": ["hte"], "ignorePaths": [ ".git", ".github", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a64bef..d773885 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] toolchain: [stable, beta, nightly] - feature: [pure, xchacha20, "pure,aes-12bytes-nonce"] + feature: [openssl, pure, xchacha20] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -58,18 +58,24 @@ jobs: - name: Check and run lint run: cargo check && cargo fmt && cargo clippy - - name: Run openssl tests - run: cargo test - - name: Run openssl 12 bytes nonce tests - run: cargo test --features aes-12bytes-nonce - - - name: Run pure rust tests + - name: Run tests run: cargo test --no-default-features --features ${{ matrix.feature }} + - name: Run tests with std + run: cargo test --no-default-features --features ${{ matrix.feature }},std + + - name: Run tests with 12 bytes nonce + run: cargo test --no-default-features --features ${{ matrix.feature }},aes-12bytes-nonce + if: matrix.feature != 'xchacha20' + + - name: Run tests with std and 12 bytes nonce + run: cargo test --no-default-features --features ${{ matrix.feature }},std,aes-12bytes-nonce + if: matrix.feature != 'xchacha20' - name: Install wasm dep run: cargo install wasm-bindgen-cli || true - - name: Run wasm tests + - name: Run tests on wasm target run: cargo test --no-default-features --features ${{ matrix.feature }} --target=wasm32-unknown-unknown + if: matrix.feature != 'openssl' - name: Check cargo package run: cargo publish --dry-run diff --git a/CHANGELOG.md b/CHANGELOG.md index 78234bb..c7c531d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## 0.2.1 ~ 0.2.5 +## 0.2.1 ~ 0.2.6 +- Support `no_std` - Revamp documentation - Revamp configuration and add xchacha20-poly1305 backend - Add configuration for more compatibility diff --git a/Cargo.toml b/Cargo.toml index 448a79f..15a3755 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ecies" -version = "0.2.5" +version = "0.2.6" # docs authors = ["Weiliang Li "] description = "Elliptic Curve Integrated Encryption Scheme for secp256k1 in Rust" @@ -20,13 +20,12 @@ homepage = "https://ecies.org/rs/" repository = "https://github.com/ecies/rs" [dependencies] -hkdf = "0.12.3" +hkdf = {version = "0.12.3", default-features = false} libsecp256k1 = {version = "0.7.1", default-features = false, features = ["static-context"]} -once_cell = "1.18.0" -sha2 = "0.10.7" +sha2 = {version = "0.10.7", default-features = false} # openssl aes -openssl = {version = "0.10.55", optional = true} +openssl = {version = "0.10.55", default-features = false, optional = true} # pure rust aes aes-gcm = {version = "0.10.2", default-features = false, optional = true} @@ -35,23 +34,35 @@ typenum = {version = "1.16.0", default-features = false, optional = true} # chacha20 cipher chacha20poly1305 = {version = "0.10.1", default-features = false, optional = true} -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = {version = "0.2.10", features = ["js"]} -rand = {version = "0.8.5", features = ["getrandom"]} +# random number generator +getrandom = {version = "0.2.10", default-features = false} +rand_core = {version = "0.6.4", default-features = false, features = ["getrandom"]} -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -rand = {version = "0.8.5"} +# configuration +once_cell = {version = "1.18.0", default-features = false} +parking_lot = "0.12.1" + +[target.'cfg(all(target_arch = "wasm32", target_os="unknown"))'.dependencies] +# only for js (browser or node). if it's not js, like substrate, it won't build +getrandom = {version = "0.2.10", default-features = false, features = ["js"]} +wasm-bindgen = {version = "0.2.87", default-features = false} + +[target.'cfg(all(target_arch = "wasm32", not(target_os="unknown")))'.dependencies] +# allows wasm32-wasi to build +once_cell = {version = "1.18.0", default-features = false, features = ["std"]} [features] +std = ["hkdf/std", "sha2/std", "once_cell/std"] + default = ["openssl"] aes-12bytes-nonce = [] # with feature "openssl" or "pure". default: 16 bytes pure = ["aes-gcm/aes", "typenum"] -xchacha20 = ["chacha20poly1305/std"] +xchacha20 = ["chacha20poly1305"] [dev-dependencies] criterion = {version = "0.5.1", default-features = false} -hex = "0.4.3" +hex = {version = "0.4.3", default-features = false, features = ["alloc"]} [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.37" diff --git a/README.md b/README.md index f0f5412..f6956d6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ This library can be compiled to the WASM target at your option, see [WASM compat ## Quick Start +`no_std` is enabled by default. You can enable `std` with `std` feature. + +```toml +ecies = {version = "0.2", features = ["std"]} +``` + ```rust use ecies::{decrypt, encrypt, utils::generate_keypair}; diff --git a/src/compat.rs b/src/compat.rs new file mode 100644 index 0000000..3f32299 --- /dev/null +++ b/src/compat.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "std")] +pub(crate) use std::vec::Vec; + +#[cfg(not(feature = "std"))] +pub(crate) use alloc::vec::Vec; diff --git a/src/config.rs b/src/config.rs index f10f18b..99949bd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,5 @@ -use std::sync::Mutex; - use once_cell::sync::Lazy; +use parking_lot::RwLock; use crate::consts::{COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE}; @@ -12,14 +11,14 @@ pub struct Config { } /// Global config variable -pub static ECIES_CONFIG: Lazy> = Lazy::new(|| { +pub static ECIES_CONFIG: Lazy> = Lazy::new(|| { let config: Config = Config::default(); - Mutex::new(config) + RwLock::new(config) }); /// Update global config pub fn update_config(config: Config) { - *ECIES_CONFIG.lock().unwrap() = config; + *ECIES_CONFIG.write() = config; } /// Reset global config to default @@ -29,7 +28,7 @@ pub fn reset_config() { /// Get ephemeral key compressed or not pub fn is_ephemeral_key_compressed() -> bool { - ECIES_CONFIG.lock().unwrap().is_ephemeral_key_compressed + ECIES_CONFIG.read().is_ephemeral_key_compressed } /// Get ephemeral key size: compressed(33) or uncompressed(65) @@ -43,5 +42,5 @@ pub fn get_ephemeral_key_size() -> usize { /// Get hkdf key derived from compressed shared point or not pub fn is_hkdf_key_compressed() -> bool { - ECIES_CONFIG.lock().unwrap().is_hkdf_key_compressed + ECIES_CONFIG.read().is_hkdf_key_compressed } diff --git a/src/lib.rs b/src/lib.rs index 9333c26..85f4ec1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,11 @@ #![doc = include_str!("../README.md")] +#![no_std] + +#[cfg(feature = "std")] +extern crate std; + +#[cfg(not(feature = "std"))] +extern crate alloc; pub use libsecp256k1::{Error as SecpError, PublicKey, SecretKey}; @@ -11,10 +18,14 @@ pub mod symmetric; /// Utility functions pub mod utils; +mod compat; + use config::{get_ephemeral_key_size, is_ephemeral_key_compressed}; use symmetric::{sym_decrypt, sym_encrypt}; use utils::{decapsulate, encapsulate, generate_keypair}; +use crate::compat::Vec; + /// Encrypt a message by a public key /// /// # Arguments diff --git a/src/symmetric/mod.rs b/src/symmetric/mod.rs index a5e0258..fe7b44f 100644 --- a/src/symmetric/mod.rs +++ b/src/symmetric/mod.rs @@ -12,6 +12,8 @@ use pure_aes::{decrypt, encrypt}; #[cfg(feature = "xchacha20")] use xchacha20::{decrypt, encrypt}; +use crate::compat::Vec; + /// Symmetric encryption wrapper. Openssl AES-256-GCM, pure Rust AES-256-GCM, or XChaCha20-Poly1305 pub fn sym_encrypt(key: &[u8], msg: &[u8]) -> Option> { encrypt(key, msg) @@ -25,7 +27,7 @@ pub fn sym_decrypt(key: &[u8], encrypted_msg: &[u8]) -> Option> { #[cfg(test)] pub(crate) mod tests { use hex::decode; // dev dep - use rand::{thread_rng, Rng}; + use rand_core::{OsRng, RngCore}; use super::*; @@ -52,7 +54,7 @@ pub(crate) mod tests { fn test_random_key() { let text = b"this is a text"; let mut key = [0u8; 32]; - thread_rng().fill(&mut key); + OsRng.fill_bytes(&mut key); assert_eq!( text, diff --git a/src/symmetric/openssl_aes.rs b/src/symmetric/openssl_aes.rs index 6e8fa1f..9562038 100644 --- a/src/symmetric/openssl_aes.rs +++ b/src/symmetric/openssl_aes.rs @@ -1,7 +1,8 @@ use openssl::symm::{decrypt_aead, encrypt_aead, Cipher}; -use rand::{thread_rng, Rng}; +use rand_core::{OsRng, RngCore}; use crate::consts::{AEAD_TAG_LENGTH, AES_NONCE_LENGTH, EMPTY_BYTES}; +use crate::Vec; const NONCE_TAG_LENGTH: usize = AES_NONCE_LENGTH + AEAD_TAG_LENGTH; @@ -10,7 +11,7 @@ pub fn encrypt(key: &[u8], msg: &[u8]) -> Option> { let cipher = Cipher::aes_256_gcm(); let mut iv = [0u8; AES_NONCE_LENGTH]; - thread_rng().fill(&mut iv); + OsRng.fill_bytes(&mut iv); let mut tag = [0u8; AEAD_TAG_LENGTH]; diff --git a/src/symmetric/pure_aes.rs b/src/symmetric/pure_aes.rs index 9457dce..0ccbfd7 100644 --- a/src/symmetric/pure_aes.rs +++ b/src/symmetric/pure_aes.rs @@ -3,10 +3,11 @@ use aes_gcm::{ aes::Aes256, AesGcm, KeyInit, }; -use rand::{thread_rng, Rng}; +use rand_core::{OsRng, RngCore}; #[allow(unused_imports)] use typenum::consts::{U12, U16}; +use crate::compat::Vec; use crate::consts::{AEAD_TAG_LENGTH, AES_NONCE_LENGTH, EMPTY_BYTES}; #[cfg(not(feature = "aes-12bytes-nonce"))] @@ -23,7 +24,7 @@ pub fn encrypt(key: &[u8], msg: &[u8]) -> Option> { let aead = Aes256Gcm::new(key); let mut iv = [0u8; AES_NONCE_LENGTH]; - thread_rng().fill(&mut iv); + OsRng.fill_bytes(&mut iv); let nonce = GenericArray::from_slice(&iv); let mut out = Vec::with_capacity(msg.len()); diff --git a/src/symmetric/xchacha20.rs b/src/symmetric/xchacha20.rs index 850ebcf..0c1a5ee 100644 --- a/src/symmetric/xchacha20.rs +++ b/src/symmetric/xchacha20.rs @@ -2,8 +2,9 @@ use chacha20poly1305::{ aead::{generic_array::GenericArray, AeadInPlace}, KeyInit, XChaCha20Poly1305, }; -use rand::{thread_rng, Rng}; +use rand_core::{OsRng, RngCore}; +use crate::compat::Vec; use crate::consts::{AEAD_TAG_LENGTH, EMPTY_BYTES, XCHACHA20_NONCE_LENGTH}; const NONCE_TAG_LENGTH: usize = XCHACHA20_NONCE_LENGTH + AEAD_TAG_LENGTH; @@ -14,7 +15,7 @@ pub fn encrypt(key: &[u8], msg: &[u8]) -> Option> { let aead = XChaCha20Poly1305::new(key); let mut iv = [0u8; XCHACHA20_NONCE_LENGTH]; - thread_rng().fill(&mut iv); + OsRng.fill_bytes(&mut iv); let nonce = GenericArray::from_slice(&iv); let mut out = Vec::with_capacity(msg.len()); diff --git a/src/utils.rs b/src/utils.rs index 8ff186b..b3e456f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,9 @@ use hkdf::Hkdf; use libsecp256k1::{Error as SecpError, PublicKey, SecretKey}; -use rand::thread_rng; +use rand_core::OsRng; use sha2::Sha256; +use crate::compat::Vec; use crate::config::{get_ephemeral_key_size, is_hkdf_key_compressed}; use crate::consts::EMPTY_BYTES; @@ -11,7 +12,7 @@ pub type SharedSecret = [u8; 32]; /// Generate a `(SecretKey, PublicKey)` pair pub fn generate_keypair() -> (SecretKey, PublicKey) { - let sk = SecretKey::random(&mut thread_rng()); + let sk = SecretKey::random(&mut OsRng); (sk, PublicKey::from_secret_key(&sk)) } diff --git a/tests/integration.rs b/tests/integration.rs index c051323..e8f69df 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -8,8 +8,7 @@ use hex::decode; const MSG: &str = "helloworld🌍"; -#[test] -fn can_change_behavior_with_config() { +fn check_config_hkdf_key() { let mut two = [0u8; 32]; let mut three = [0u8; 32]; two[31] = 2u8; @@ -25,34 +24,56 @@ fn can_change_behavior_with_config() { ..Config::default() }); - assert_eq!(encapsulate(&sk2, &pk3), decapsulate(&pk2, &sk3)); + let encapsulated = encapsulate(&sk2, &pk3).unwrap(); + assert_eq!(encapsulated, decapsulate(&pk2, &sk3).unwrap()); assert_eq!( - encapsulate(&sk2, &pk3).unwrap().to_vec(), + encapsulated.to_vec(), decode("b192b226edb3f02da11ef9c6ce4afe1c7e40be304e05ae3b988f4834b1cb6c69").unwrap() ); +} + +fn check_config_ephemeral_key() { + let (sk, pk) = generate_keypair(); + let (sk, pk) = (&sk.serialize(), &pk.serialize_compressed()); + let encrypted_1 = encrypt(pk, MSG.as_bytes()).unwrap(); + assert_eq!(MSG.as_bytes(), decrypt(sk, &encrypted_1).unwrap().as_slice()); update_config(Config { is_ephemeral_key_compressed: true, - is_hkdf_key_compressed: true, + ..Config::default() }); - let (sk, pk) = generate_keypair(); - let (sk, pk) = (&sk.serialize(), &pk.serialize_compressed()); - - assert_eq!( - MSG.as_bytes(), - decrypt(sk, &encrypt(pk, MSG.as_bytes()).unwrap()).unwrap().as_slice() - ); + let encrypted_2 = encrypt(pk, MSG.as_bytes()).unwrap(); + assert_eq!(encrypted_1.len() - encrypted_2.len(), 32); + assert_eq!(MSG.as_bytes(), decrypt(sk, &encrypted_2).unwrap().as_slice()); reset_config(); } +#[test] +fn can_change_behavior_with_config() { + check_config_hkdf_key(); + check_config_ephemeral_key(); +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod wasm_tests { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn can_change_behavior_with_config() { + check_config_hkdf_key(); + check_config_ephemeral_key(); + } +} + #[test] #[cfg(all( not(target_arch = "wasm32"), not(feature = "aes-12bytes-nonce"), - not(feature = "xchacha20") + not(feature = "xchacha20"), ))] fn is_compatible_with_python() { use futures_util::FutureExt;