From 212852e35fed2ccd6be4c2a63fdcb9576eff9b05 Mon Sep 17 00:00:00 2001 From: Weiliang Li Date: Mon, 11 Sep 2023 19:29:56 +0900 Subject: [PATCH] Refactor elliptic --- .cspell.jsonc | 7 +- .github/workflows/cd.yml | 2 +- Cargo.toml | 27 ++-- README.md | 3 + src/config.rs | 76 ++--------- src/consts.rs | 3 + src/elliptic/mod.rs | 50 ++++++++ src/elliptic/secp256k1.rs | 260 ++++++++++++++++++++++++++++++++++++++ src/elliptic/x25519.rs | 54 ++++++++ src/lib.rs | 108 +++------------- src/utils.rs | 133 +------------------ tests/integration.rs | 1 + 12 files changed, 423 insertions(+), 301 deletions(-) create mode 100644 src/elliptic/mod.rs create mode 100644 src/elliptic/secp256k1.rs create mode 100644 src/elliptic/x25519.rs diff --git a/.cspell.jsonc b/.cspell.jsonc index d9a68d2..9221244 100644 --- a/.cspell.jsonc +++ b/.cspell.jsonc @@ -8,8 +8,11 @@ "bindgen", "chacha", "Codacy", + "Codecov", "consts", "Ctarget", + "dalek", + "diffie", "docsrs", "ecies", "eciespy", @@ -32,7 +35,9 @@ // 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/cd.yml b/.github/workflows/cd.yml index 936aeb9..6e46492 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -19,7 +19,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: ${{ matrix.toolchain }} + toolchain: stable target: wasm32-unknown-unknown - run: cargo generate-lockfile diff --git a/Cargo.toml b/Cargo.toml index a279603..1d7182b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "ecies" -version = "0.2.6" +version = "0.2.7" # docs authors = ["Weiliang Li "] -description = "Elliptic Curve Integrated Encryption Scheme for secp256k1 in Rust" +description = "Elliptic Curve Integrated Encryption Scheme for secp256k1/curve25519" edition = "2021" keywords = [ "secp256k1", + "curve25519", "crypto", "ecc", "ecies", @@ -21,11 +22,14 @@ repository = "https://github.com/ecies/rs" [dependencies] hkdf = {version = "0.12.3", default-features = false} -libsecp256k1 = {version = "0.7.1", default-features = false, features = ["static-context"]} sha2 = {version = "0.10.7", default-features = false} +# elliptic curves +libsecp256k1 = {version = "0.7.1", default-features = false, features = ["static-context"], optional = true} +x25519-dalek = {version = "2.0.0", default-features = false, features = ["static_secrets"], optional = true} + # openssl aes -openssl = {version = "0.10.56", default-features = false, optional = true} +openssl = {version = "0.10.57", default-features = false, optional = true} # pure rust aes aes-gcm = {version = "0.10.2", default-features = false, optional = true} @@ -52,16 +56,21 @@ wasm-bindgen = {version = "0.2.87", default-features = false} once_cell = {version = "1.18.0", default-features = false, features = ["std"]} [features] +default = ["openssl"] std = ["hkdf/std", "sha2/std", "once_cell/std"] -default = ["openssl"] +# curve +secp256k1 = ["libsecp256k1"] +x25519 = ["x25519-dalek"] -aes-12bytes-nonce = [] # with feature "openssl" or "pure". default: 16 bytes -pure = ["aes-gcm/aes", "typenum"] -xchacha20 = ["chacha20poly1305"] +# cipher +aes-12bytes-nonce = ["secp256k1"] # with feature "openssl" or "pure". default: 16 bytes +openssl = ["dep:openssl", "secp256k1"] +pure = ["aes-gcm/aes", "typenum", "secp256k1"] +xchacha20 = ["chacha20poly1305", "secp256k1"] [dev-dependencies] -criterion = {version = "0.5.1", default-features = false} +criterion = {version = "0.4.0", default-features = false} hex = {version = "0.4.3", default-features = false, features = ["alloc"]} [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/README.md b/README.md index 6818038..fd2c56f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,10 @@ use ecies::{decrypt, encrypt, utils::generate_keypair}; const MSG: &str = "helloworld🌍"; let (sk, pk) = generate_keypair(); +#[cfg(not(feature = "x25519"))] let (sk, pk) = (&sk.serialize(), &pk.serialize()); +#[cfg(feature = "x25519")] +let (sk, pk) = (sk.as_bytes(), pk.as_bytes()); let msg = MSG.as_bytes(); assert_eq!( diff --git a/src/config.rs b/src/config.rs index 9862ef6..0922ce0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,6 @@ use once_cell::sync::Lazy; use parking_lot::RwLock; -use crate::consts::{COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE}; - /// ECIES config. Make sure all parties use the same config #[derive(Default)] pub struct Config { @@ -32,7 +30,10 @@ pub fn is_ephemeral_key_compressed() -> bool { } /// Get ephemeral key size: compressed(33) or uncompressed(65) +#[cfg(not(feature = "x25519"))] pub fn get_ephemeral_key_size() -> usize { + use crate::consts::{COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE}; + if is_ephemeral_key_compressed() { COMPRESSED_PUBLIC_KEY_SIZE } else { @@ -40,73 +41,12 @@ pub fn get_ephemeral_key_size() -> usize { } } +#[cfg(feature = "x25519")] +pub fn get_ephemeral_key_size() -> usize { + 32 +} + /// Get hkdf key derived from compressed shared point or not pub fn is_hkdf_key_compressed() -> bool { ECIES_CONFIG.read().is_hkdf_key_compressed } - -#[cfg(test)] -mod tests { - use super::{reset_config, update_config, Config}; - use libsecp256k1::PublicKey; - - use crate::{ - decrypt, encrypt, - utils::{ - encapsulate, generate_keypair, - tests::{decode_hex, get_sk2_sk3}, - }, - }; - - const MSG: &str = "helloworld🌍"; - - #[test] - pub(super) fn test_known_hkdf_config() { - let (sk2, sk3) = get_sk2_sk3(); - let pk3 = PublicKey::from_secret_key(&sk3); - - update_config(Config { - is_hkdf_key_compressed: true, - ..Config::default() - }); - - let encapsulated = encapsulate(&sk2, &pk3).unwrap(); - - assert_eq!( - encapsulated.to_vec(), - decode_hex("b192b226edb3f02da11ef9c6ce4afe1c7e40be304e05ae3b988f4834b1cb6c69") - ); - - reset_config(); - } - - #[test] - pub(super) fn test_ephemeral_key_config() { - 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()); - - update_config(Config { - is_ephemeral_key_compressed: true, - ..Config::default() - }); - - 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()); - - reset_config(); - } -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod wasm_tests { - use wasm_bindgen_test::*; - - #[wasm_bindgen_test] - fn test_wasm() { - super::tests::test_ephemeral_key_config(); - super::tests::test_known_hkdf_config(); - } -} diff --git a/src/consts.rs b/src/consts.rs index 8ac8272..1226f22 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -19,3 +19,6 @@ pub const NONCE_TAG_LENGTH: usize = NONCE_LENGTH + AEAD_TAG_LENGTH; /// Empty bytes array pub const EMPTY_BYTES: [u8; 0] = []; + +/// Shared secret derived from key exchange by hkdf +pub type SharedSecret = [u8; 32]; diff --git a/src/elliptic/mod.rs b/src/elliptic/mod.rs new file mode 100644 index 0000000..8192959 --- /dev/null +++ b/src/elliptic/mod.rs @@ -0,0 +1,50 @@ +use hkdf::Hkdf; +use sha2::Sha256; + +use crate::compat::Vec; +use crate::consts::{SharedSecret, EMPTY_BYTES}; + +#[cfg(not(feature = "x25519"))] +mod secp256k1; +#[cfg(not(feature = "x25519"))] +pub use secp256k1::{decapsulate, encapsulate, generate_keypair}; +#[cfg(not(feature = "x25519"))] +pub(crate) use secp256k1::{parse_pk, parse_sk, pk_to_vec, Error}; + +#[cfg(feature = "x25519")] +mod x25519; +#[cfg(feature = "x25519")] +pub use x25519::{decapsulate, encapsulate, generate_keypair}; +#[cfg(feature = "x25519")] +pub(crate) use x25519::{parse_pk, parse_sk, pk_to_vec, Error}; + +fn hkdf_derive(sender_point: &[u8], shared_point: &[u8]) -> SharedSecret { + let size = sender_point.len() + shared_point.len(); + let mut master = Vec::with_capacity(size); + master.extend(sender_point); + master.extend(shared_point); + hkdf_sha256(&master) +} + +fn hkdf_sha256(master: &[u8]) -> SharedSecret { + let h = Hkdf::::new(None, master); + let mut out = [0u8; 32]; + // never fails because 32 < 255 * chunk_len, which is 32 on SHA256 + h.expand(&EMPTY_BYTES, &mut out).unwrap(); + out +} + +#[cfg(test)] +mod tests { + use super::hkdf_sha256; + + use crate::utils::tests::decode_hex; + + #[test] + fn test_known_hkdf_vector() { + assert_eq!( + hkdf_sha256(b"secret").to_vec(), + decode_hex("2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf") + ); + } +} diff --git a/src/elliptic/secp256k1.rs b/src/elliptic/secp256k1.rs new file mode 100644 index 0000000..433c624 --- /dev/null +++ b/src/elliptic/secp256k1.rs @@ -0,0 +1,260 @@ +use libsecp256k1::{PublicKey, SecretKey}; +use rand_core::OsRng; + +use super::hkdf_derive; +use crate::compat::Vec; +use crate::config::is_hkdf_key_compressed; +use crate::consts::SharedSecret; + +pub use libsecp256k1::Error; + +/// Generate a `(SecretKey, PublicKey)` pair +pub fn generate_keypair() -> (SecretKey, PublicKey) { + let sk = SecretKey::random(&mut OsRng); + let pk = PublicKey::from_secret_key(&sk); + (sk, pk) +} + +/// Calculate a shared symmetric key of our secret key and peer's public key by hkdf +pub fn encapsulate(sk: &SecretKey, peer_pk: &PublicKey) -> Result { + let mut shared_point = *peer_pk; + shared_point.tweak_mul_assign(sk)?; + let sender_point = &PublicKey::from_secret_key(sk); + Ok(get_shared_secret(sender_point, &shared_point)) +} + +/// Calculate a shared symmetric key of our public key and peer's secret key by hkdf +pub fn decapsulate(pk: &PublicKey, peer_sk: &SecretKey) -> Result { + let mut shared_point = *pk; + shared_point.tweak_mul_assign(peer_sk)?; + Ok(get_shared_secret(pk, &shared_point)) +} + +/// Parse secret key bytes +pub fn parse_sk(sk: &[u8]) -> Result { + SecretKey::parse_slice(sk) +} + +/// Parse public key bytes +pub fn parse_pk(pk: &[u8]) -> Result { + PublicKey::parse_slice(pk, None) +} + +/// Public key to bytes +pub fn pk_to_vec(pk: &PublicKey, compressed: bool) -> Vec { + if compressed { + pk.serialize_compressed().to_vec() + } else { + pk.serialize().to_vec() + } +} + +fn get_shared_secret(sender_point: &PublicKey, shared_point: &PublicKey) -> SharedSecret { + if is_hkdf_key_compressed() { + hkdf_derive( + &sender_point.serialize_compressed(), + &shared_point.serialize_compressed(), + ) + } else { + hkdf_derive(&sender_point.serialize(), &shared_point.serialize()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::utils::tests::decode_hex; + + #[test] + fn test_secret_validity() { + // 0 < private key < group order is valid + let mut zero = [0u8; 32]; + let group_order = decode_hex("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + let invalid_sks = [zero.to_vec(), group_order]; + + for sk in invalid_sks.iter() { + assert_eq!(parse_sk(sk).err().unwrap(), Error::InvalidSecretKey); + } + + zero[31] = 1; + + let one = zero; + let group_order_minus_1 = decode_hex("0Xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140"); + let valid_sks = [one.to_vec(), group_order_minus_1]; + for sk in valid_sks.iter() { + parse_sk(sk).unwrap(); + } + } + + #[test] + fn test_key_exchange() { + let (sk1, pk1) = generate_keypair(); + let (sk2, pk2) = generate_keypair(); + assert_ne!(sk1, sk2); + assert_ne!(pk1, pk2); + assert_eq!(encapsulate(&sk2, &pk1).unwrap(), decapsulate(&pk2, &sk1).unwrap()); + } + + /// Generate two secret keys with values 2 and 3 + pub fn get_sk2_sk3() -> (SecretKey, SecretKey) { + let mut two = [0u8; 32]; + let mut three = [0u8; 32]; + two[31] = 2u8; + three[31] = 3u8; + + let sk2 = SecretKey::parse_slice(&two).unwrap(); + let sk3 = SecretKey::parse_slice(&three).unwrap(); + (sk2, sk3) + } + + #[test] + pub fn test_known_shared_secret() { + let (sk2, sk3) = get_sk2_sk3(); + let pk3 = PublicKey::from_secret_key(&sk3); + + assert_eq!( + encapsulate(&sk2, &pk3).unwrap().to_vec(), + decode_hex("6f982d63e8590c9d9b5b4c1959ff80315d772edd8f60287c9361d548d5200f82") + ); + } +} + +#[cfg(test)] +mod lib_tests { + use super::{generate_keypair, Error}; + use crate::{decrypt, encrypt}; + + const MSG: &str = "helloworld🌍"; + const BIG_MSG_SIZE: usize = 2 * 1024 * 1024; // 2 MB + const BIG_MSG: [u8; BIG_MSG_SIZE] = [1u8; BIG_MSG_SIZE]; + + fn test_enc_dec(sk: &[u8], pk: &[u8]) { + let msg = MSG.as_bytes(); + assert_eq!(msg.to_vec(), decrypt(sk, &encrypt(pk, msg).unwrap()).unwrap()); + let msg = &BIG_MSG; + assert_eq!(msg.to_vec(), decrypt(sk, &encrypt(pk, msg).unwrap()).unwrap()); + } + + #[test] + pub fn attempts_to_encrypt_with_invalid_key() { + assert_eq!(encrypt(&[0u8; 33], MSG.as_bytes()), Err(Error::InvalidPublicKey)); + } + + #[test] + pub fn attempts_to_decrypt_with_invalid_key() { + assert_eq!(decrypt(&[0u8; 32], &[]), Err(Error::InvalidSecretKey)); + } + + #[test] + pub fn attempts_to_decrypt_incorrect_message() { + let (sk, _) = generate_keypair(); + + assert_eq!(decrypt(&sk.serialize(), &[]), Err(Error::InvalidMessage)); + assert_eq!(decrypt(&sk.serialize(), &[0u8; 65]), Err(Error::InvalidPublicKey)); + } + + #[test] + pub fn attempts_to_decrypt_with_another_key() { + let (_, pk1) = generate_keypair(); + let (sk2, _) = generate_keypair(); + + let encrypted = encrypt(&pk1.serialize_compressed(), MSG.as_bytes()).unwrap(); + assert_eq!(decrypt(&sk2.serialize(), &encrypted), Err(Error::InvalidMessage)); + } + + #[test] + pub fn test_compressed_public() { + let (sk, pk) = generate_keypair(); + let (sk, pk) = (&sk.serialize(), &pk.serialize_compressed()); + test_enc_dec(sk, pk); + } + + #[test] + pub fn test_uncompressed_public() { + let (sk, pk) = generate_keypair(); + let (sk, pk) = (&sk.serialize(), &pk.serialize()); + test_enc_dec(sk, pk); + } +} + +#[cfg(test)] +mod config_tests { + use super::*; + + use crate::config::{reset_config, update_config, Config}; + use crate::utils::tests::decode_hex; + use crate::{decrypt, encrypt}; + use tests::get_sk2_sk3; + + const MSG: &str = "helloworld🌍"; + + #[test] + pub fn test_known_hkdf_config() { + let (sk2, sk3) = get_sk2_sk3(); + let pk3 = PublicKey::from_secret_key(&sk3); + + update_config(Config { + is_hkdf_key_compressed: true, + ..Config::default() + }); + + let encapsulated = encapsulate(&sk2, &pk3).unwrap(); + + assert_eq!( + encapsulated.to_vec(), + decode_hex("b192b226edb3f02da11ef9c6ce4afe1c7e40be304e05ae3b988f4834b1cb6c69") + ); + + reset_config(); + } + + #[test] + pub fn test_ephemeral_key_config() { + 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()); + + update_config(Config { + is_ephemeral_key_compressed: true, + ..Config::default() + }); + + 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()); + + reset_config(); + } +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod wasm_tests { + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn test() { + super::tests::test_known_shared_secret(); + } + + #[wasm_bindgen_test] + fn test_config() { + super::config_tests::test_ephemeral_key_config(); + super::config_tests::test_known_hkdf_config(); + } + + #[wasm_bindgen_test] + fn test_lib() { + super::lib_tests::test_compressed_public(); + super::lib_tests::test_uncompressed_public(); + } + + #[wasm_bindgen_test] + fn test_error() { + super::lib_tests::attempts_to_encrypt_with_invalid_key(); + super::lib_tests::attempts_to_decrypt_with_invalid_key(); + super::lib_tests::attempts_to_decrypt_incorrect_message(); + super::lib_tests::attempts_to_decrypt_with_another_key(); + } +} diff --git a/src/elliptic/x25519.rs b/src/elliptic/x25519.rs new file mode 100644 index 0000000..42da67c --- /dev/null +++ b/src/elliptic/x25519.rs @@ -0,0 +1,54 @@ +use rand_core::OsRng; +use x25519_dalek::{PublicKey, StaticSecret as SecretKey}; + +use super::hkdf_derive; +use crate::compat::Vec; +use crate::consts::SharedSecret; + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Error { + InvalidMessage, +} + +/// Generate a `(SecretKey, PublicKey)` pair +pub fn generate_keypair() -> (SecretKey, PublicKey) { + let sk = SecretKey::random_from_rng(&mut OsRng); + let pk = PublicKey::from(&sk); + (sk, pk) +} + +/// Calculate a shared symmetric key of our secret key and peer's public key by hkdf +pub fn encapsulate(sk: &SecretKey, peer_pk: &PublicKey) -> Result { + let shared_point = sk.diffie_hellman(&peer_pk); + let sender_point = PublicKey::from(sk); + Ok(get_shared_secret(sender_point.as_bytes(), shared_point.as_bytes())) +} + +/// Calculate a shared symmetric key of our public key and peer's secret key by hkdf +pub fn decapsulate(pk: &PublicKey, peer_sk: &SecretKey) -> Result { + let shared_point = peer_sk.diffie_hellman(&pk); + Ok(get_shared_secret(pk.as_bytes(), shared_point.as_bytes())) +} + +/// Parse secret key bytes +pub fn parse_sk(sk: &[u8]) -> Result { + let mut data = [0u8; 32]; + data.copy_from_slice(sk); + Ok(SecretKey::from(data)) +} + +/// Parse public key bytes +pub fn parse_pk(pk: &[u8]) -> Result { + let mut data = [0u8; 32]; + data.copy_from_slice(pk); + Ok(PublicKey::from(data)) +} + +/// Public key to bytes +pub fn pk_to_vec(pk: &PublicKey, _compressed: bool) -> Vec { + pk.as_bytes().to_vec() +} + +fn get_shared_secret(sender_point: &[u8], shared_point: &[u8]) -> SharedSecret { + hkdf_derive(sender_point, shared_point) +} diff --git a/src/lib.rs b/src/lib.rs index c67f841..afb3f37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,10 @@ extern crate std; #[cfg(not(feature = "std"))] extern crate alloc; -pub use libsecp256k1::{Error as SecpError, PublicKey, SecretKey}; +#[cfg(not(feature = "x25519"))] +pub use libsecp256k1::{PublicKey, SecretKey}; +#[cfg(feature = "x25519")] +pub use x25519_dalek::{PublicKey, StaticSecret as SecretKey}; /// ECIES configuration pub mod config; @@ -19,10 +22,11 @@ pub mod symmetric; pub mod utils; mod compat; +mod elliptic; use config::{get_ephemeral_key_size, is_ephemeral_key_compressed}; +use elliptic::{decapsulate, encapsulate, generate_keypair, parse_pk, parse_sk, pk_to_vec, Error}; use symmetric::{sym_decrypt, sym_encrypt}; -use utils::{decapsulate, encapsulate, generate_keypair}; use crate::compat::Vec; @@ -32,24 +36,20 @@ use crate::compat::Vec; /// /// * `receiver_pub` - The u8 array reference of a receiver's public key /// * `msg` - The u8 array reference of the message to encrypt -pub fn encrypt(receiver_pub: &[u8], msg: &[u8]) -> Result, SecpError> { - let receiver_pk = PublicKey::parse_slice(receiver_pub, None)?; +pub fn encrypt(receiver_pub: &[u8], msg: &[u8]) -> Result, Error> { + let receiver_pk = parse_pk(receiver_pub)?; let (ephemeral_sk, ephemeral_pk) = generate_keypair(); let aes_key = encapsulate(&ephemeral_sk, &receiver_pk)?; - let encrypted = sym_encrypt(&aes_key, msg).ok_or(SecpError::InvalidMessage)?; + let encrypted = sym_encrypt(&aes_key, msg).ok_or(Error::InvalidMessage)?; let is_compressed = is_ephemeral_key_compressed(); let key_size = get_ephemeral_key_size(); let mut cipher_text = Vec::with_capacity(key_size + encrypted.len()); + let ephemeral_pk = pk_to_vec(&ephemeral_pk, is_compressed); - if is_compressed { - cipher_text.extend(&ephemeral_pk.serialize_compressed()); - } else { - cipher_text.extend(&ephemeral_pk.serialize()); - } - + cipher_text.extend(&ephemeral_pk); cipher_text.extend(encrypted); Ok(cipher_text) @@ -61,94 +61,18 @@ pub fn encrypt(receiver_pub: &[u8], msg: &[u8]) -> Result, SecpError> { /// /// * `receiver_sec` - The u8 array reference of a receiver's secret key /// * `msg` - The u8 array reference of the encrypted message -pub fn decrypt(receiver_sec: &[u8], msg: &[u8]) -> Result, SecpError> { - let receiver_sk = SecretKey::parse_slice(receiver_sec)?; +pub fn decrypt(receiver_sec: &[u8], msg: &[u8]) -> Result, Error> { + let receiver_sk = parse_sk(receiver_sec)?; let key_size = get_ephemeral_key_size(); if msg.len() < key_size { - return Err(SecpError::InvalidMessage); + return Err(Error::InvalidMessage); } - let ephemeral_pk = PublicKey::parse_slice(&msg[..key_size], None)?; + let ephemeral_pk = parse_pk(&msg[..key_size])?; let encrypted = &msg[key_size..]; let aes_key = decapsulate(&ephemeral_pk, &receiver_sk)?; - sym_decrypt(&aes_key, encrypted).ok_or(SecpError::InvalidMessage) -} - -#[cfg(test)] -mod tests { - use super::{decrypt, encrypt, generate_keypair, SecpError}; - - const MSG: &str = "helloworld🌍"; - const BIG_MSG_SIZE: usize = 2 * 1024 * 1024; // 2 MB - const BIG_MSG: [u8; BIG_MSG_SIZE] = [1u8; BIG_MSG_SIZE]; - - fn test_enc_dec(sk: &[u8], pk: &[u8]) { - let msg = MSG.as_bytes(); - assert_eq!(msg.to_vec(), decrypt(sk, &encrypt(pk, msg).unwrap()).unwrap()); - let msg = &BIG_MSG; - assert_eq!(msg.to_vec(), decrypt(sk, &encrypt(pk, msg).unwrap()).unwrap()); - } - - #[test] - pub(super) fn attempts_to_encrypt_with_invalid_key() { - assert_eq!(encrypt(&[0u8; 33], MSG.as_bytes()), Err(SecpError::InvalidPublicKey)); - } - - #[test] - pub(super) fn attempts_to_decrypt_with_invalid_key() { - assert_eq!(decrypt(&[0u8; 32], &[]), Err(SecpError::InvalidSecretKey)); - } - - #[test] - pub(super) fn attempts_to_decrypt_incorrect_message() { - let (sk, _) = generate_keypair(); - - assert_eq!(decrypt(&sk.serialize(), &[]), Err(SecpError::InvalidMessage)); - assert_eq!(decrypt(&sk.serialize(), &[0u8; 65]), Err(SecpError::InvalidPublicKey)); - } - - #[test] - pub(super) fn attempts_to_decrypt_with_another_key() { - let (_, pk1) = generate_keypair(); - let (sk2, _) = generate_keypair(); - - let encrypted = encrypt(&pk1.serialize_compressed(), MSG.as_bytes()).unwrap(); - assert_eq!(decrypt(&sk2.serialize(), &encrypted), Err(SecpError::InvalidMessage)); - } - - #[test] - pub(super) fn test_compressed_public() { - let (sk, pk) = generate_keypair(); - let (sk, pk) = (&sk.serialize(), &pk.serialize_compressed()); - test_enc_dec(sk, pk); - } - - #[test] - pub(super) fn test_uncompressed_public() { - let (sk, pk) = generate_keypair(); - let (sk, pk) = (&sk.serialize(), &pk.serialize()); - test_enc_dec(sk, pk); - } -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod wasm_tests { - use wasm_bindgen_test::*; - - #[wasm_bindgen_test] - fn test_wasm() { - super::tests::test_compressed_public(); - super::tests::test_uncompressed_public(); - } - - #[wasm_bindgen_test] - fn test_wasm_error() { - super::tests::attempts_to_encrypt_with_invalid_key(); - super::tests::attempts_to_decrypt_with_invalid_key(); - super::tests::attempts_to_decrypt_incorrect_message(); - super::tests::attempts_to_decrypt_with_another_key(); - } + sym_decrypt(&aes_key, encrypted).ok_or(Error::InvalidMessage) } diff --git a/src/utils.rs b/src/utils.rs index ebdf96a..0e22198 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,67 +1,11 @@ -use hkdf::Hkdf; -use libsecp256k1::{Error as SecpError, PublicKey, SecretKey}; -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; - -/// Shared secret derived from key exchange by hkdf -pub type SharedSecret = [u8; 32]; - -/// Generate a `(SecretKey, PublicKey)` pair -pub fn generate_keypair() -> (SecretKey, PublicKey) { - let sk = SecretKey::random(&mut OsRng); - (sk, PublicKey::from_secret_key(&sk)) -} - -/// Calculate a shared AES key of our secret key and peer's public key by hkdf -pub fn encapsulate(sk: &SecretKey, peer_pk: &PublicKey) -> Result { - let mut shared_point = *peer_pk; - shared_point.tweak_mul_assign(sk)?; - - let pk = PublicKey::from_secret_key(sk); - Ok(derive_key(&pk, &shared_point, is_hkdf_key_compressed())) -} - -/// Calculate a shared AES key of our public key and peer's secret key by hkdf -pub fn decapsulate(pk: &PublicKey, peer_sk: &SecretKey) -> Result { - let mut shared_point = *pk; - shared_point.tweak_mul_assign(peer_sk)?; - - Ok(derive_key(pk, &shared_point, is_hkdf_key_compressed())) -} - -// private below -fn derive_key(pk: &PublicKey, shared_point: &PublicKey, is_compressed: bool) -> SharedSecret { - let key_size = get_ephemeral_key_size(); - let mut master = Vec::with_capacity(key_size * 2); - - if is_compressed { - master.extend(&pk.serialize_compressed()); - master.extend(&shared_point.serialize_compressed()); - } else { - master.extend(&pk.serialize()); - master.extend(&shared_point.serialize()); - } - hkdf_sha256(&master) -} - -fn hkdf_sha256(master: &[u8]) -> SharedSecret { - let h = Hkdf::::new(None, master); - let mut out = [0u8; 32]; - // never fails because 32 < 255 * chunk_len, which is 32 on SHA256 - h.expand(&EMPTY_BYTES, &mut out).unwrap(); - out -} +pub use crate::consts::SharedSecret; +pub use crate::elliptic::{decapsulate, encapsulate, generate_keypair}; #[cfg(test)] pub mod tests { + use crate::compat::Vec; use hex::decode; - use super::*; - /// Convert hex string to u8 vector pub fn decode_hex(hex: &str) -> Vec { let hex = if hex.starts_with("0x") || hex.starts_with("0X") { @@ -71,75 +15,4 @@ pub mod tests { }; decode(hex).unwrap() } - - #[test] - fn test_key_exchange() { - let (sk1, pk1) = generate_keypair(); - let (sk2, pk2) = generate_keypair(); - assert_ne!(sk1, sk2); - assert_ne!(pk1, pk2); - assert_eq!(encapsulate(&sk2, &pk1).unwrap(), decapsulate(&pk2, &sk1).unwrap()); - } - - #[test] - fn test_secret_validity() { - // 0 < private key < group order is valid - let mut zero = [0u8; 32]; - let group_order = decode_hex("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); - let invalid_sks = [zero.to_vec(), group_order]; - - for sk in invalid_sks.iter() { - assert_eq!(SecretKey::parse_slice(sk).err().unwrap(), SecpError::InvalidSecretKey); - } - - zero[31] = 1; - - let one = zero; - let group_order_minus_1 = decode_hex("0Xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140"); - let valid_sks = [one.to_vec(), group_order_minus_1]; - for sk in valid_sks.iter() { - SecretKey::parse_slice(sk).unwrap(); - } - } - - #[test] - fn test_known_hkdf_vector() { - assert_eq!( - hkdf_sha256(b"secret").to_vec(), - decode_hex("2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf") - ); - } - - /// Generate two secret keys with values 2 and 3 - pub fn get_sk2_sk3() -> (SecretKey, SecretKey) { - let mut two = [0u8; 32]; - let mut three = [0u8; 32]; - two[31] = 2u8; - three[31] = 3u8; - - let sk2 = SecretKey::parse_slice(&two).unwrap(); - let sk3 = SecretKey::parse_slice(&three).unwrap(); - (sk2, sk3) - } - - #[test] - pub(super) fn test_known_shared_secret() { - let (sk2, sk3) = get_sk2_sk3(); - let pk3 = PublicKey::from_secret_key(&sk3); - - assert_eq!( - encapsulate(&sk2, &pk3).unwrap().to_vec(), - decode_hex("6f982d63e8590c9d9b5b4c1959ff80315d772edd8f60287c9361d548d5200f82") - ); - } -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod wasm_tests { - use wasm_bindgen_test::*; - - #[wasm_bindgen_test] - fn test_wasm() { - super::tests::test_known_shared_secret(); - } } diff --git a/tests/integration.rs b/tests/integration.rs index 1adfa23..68ea845 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,6 +1,7 @@ #[test] #[cfg(all( not(target_arch = "wasm32"), + not(feature = "x25519"), not(feature = "aes-12bytes-nonce"), not(feature = "xchacha20"), ))]