From ff55758b8949561fa62873e2018397bcb3c325fb Mon Sep 17 00:00:00 2001
From: Weiliang Li <to.be.impressive@gmail.com>
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, 424 insertions(+), 300 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..a5c816b 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 <to.be.impressive@gmail.com>"]
-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}
@@ -45,6 +49,7 @@ 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"]}
+once_cell = {version = "1.18.0", default-features = false, features = ["std"]}
 wasm-bindgen = {version = "0.2.87", default-features = false}
 
 [target.'cfg(all(target_arch = "wasm32", not(target_os="unknown")))'.dependencies]
@@ -52,15 +57,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]
+clap = "=4.3.24"
 criterion = {version = "0.5.1", default-features = false}
 hex = {version = "0.4.3", default-features = false, features = ["alloc"]}
 
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::<Sha256>::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<SharedSecret, Error> {
+    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<SharedSecret, Error> {
+    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, Error> {
+    SecretKey::parse_slice(sk)
+}
+
+/// Parse public key bytes
+pub fn parse_pk(pk: &[u8]) -> Result<PublicKey, Error> {
+    PublicKey::parse_slice(pk, None)
+}
+
+/// Public key to bytes
+pub fn pk_to_vec(pk: &PublicKey, compressed: bool) -> Vec<u8> {
+    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<SharedSecret, Error> {
+    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<SharedSecret, Error> {
+    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<SecretKey, Error> {
+    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<PublicKey, Error> {
+    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<u8> {
+    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<Vec<u8>, SecpError> {
-    let receiver_pk = PublicKey::parse_slice(receiver_pub, None)?;
+pub fn encrypt(receiver_pub: &[u8], msg: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, SecpError> {
-    let receiver_sk = SecretKey::parse_slice(receiver_sec)?;
+pub fn decrypt(receiver_sec: &[u8], msg: &[u8]) -> Result<Vec<u8>, 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<SharedSecret, SecpError> {
-    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<SharedSecret, SecpError> {
-    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::<Sha256>::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<u8> {
         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"),
 ))]