Skip to content

Commit e28eac0

Browse files
authored
Add x25519 support (#126)
1 parent 982f236 commit e28eac0

File tree

19 files changed

+142
-85
lines changed

19 files changed

+142
-85
lines changed

.cargo/config.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
runner = 'wasm-bindgen-test-runner'
33

44
[env]
5-
RUST_TEST_THREADS = "1" # restirct concurrency
5+
RUST_TEST_THREADS = "1" # restrict concurrency

.cspell.jsonc

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"language": "en",
66
// words - list of words to be always considered correct
77
"words": [
8+
"armv",
89
"bindgen",
910
"chacha",
1011
"Codacy",
@@ -17,6 +18,7 @@
1718
"ecies",
1819
"eciespy",
1920
"eciesrs",
21+
"getrandom",
2022
"helloworld",
2123
"HKDF",
2224
"keypair",
@@ -45,6 +47,6 @@
4547
".cspell.jsonc",
4648
"LICENSE",
4749
"package.json",
48-
"yarn.lock"
50+
"Cargo.lock"
4951
]
5052
}

.github/workflows/ci.yml

+12-6
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,19 @@ jobs:
5555
run: cargo doc --no-deps
5656

5757
- run: sh ./scripts/test.sh
58+
env:
59+
CURVE: secp256k1
60+
- run: sh ./scripts/test.sh
61+
env:
62+
CURVE: x25519
5863

59-
# Pure Rust AES and XChaCha20 on WASM target
6064
- run: cargo install wasm-bindgen-cli || true
61-
- run: cargo test --no-default-features --features pure --target=wasm32-unknown-unknown
62-
- run: cargo test --no-default-features --features pure,std --target=wasm32-unknown-unknown
63-
- run: cargo test --no-default-features --features xchacha20 --target=wasm32-unknown-unknown
64-
- run: cargo test --no-default-features --features xchacha20,std --target=wasm32-unknown-unknown
65+
- run: sh ./scripts/test-wasm.sh
66+
env:
67+
CURVE: secp256k1
68+
- run: sh ./scripts/test-wasm.sh
69+
env:
70+
CURVE: x25519
6571

6672
# Coverage
6773
- run: cargo llvm-cov --no-default-features --features pure --lcov --output-path .lcov.info
@@ -81,7 +87,7 @@ jobs:
8187
- uses: actions/checkout@v4
8288
- uses: dtolnay/rust-toolchain@master
8389
with:
84-
toolchain: 1.73.0
90+
toolchain: 1.73.0 # msrv
8591
- run: cargo generate-lockfile
8692
- uses: Swatinem/rust-cache@v2
8793
- run: cargo build

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"rust-analyzer.cargo.features": [
3-
"openssl"
3+
"pure"
44
],
55
"rust-analyzer.cargo.noDefaultFeatures": true,
66
"rust-analyzer.procMacro.enable": true,

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 0.2.8
44

55
- Bump dependencies
6+
- Add x25519 support
67

78
## 0.2.1 ~ 0.2.7
89

Cargo.toml

+25-19
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ version = "0.2.8"
55
authors = ["Weiliang Li <[email protected]>"]
66
description = "Elliptic Curve Integrated Encryption Scheme for secp256k1"
77
edition = "2021"
8-
keywords = ["secp256k1", "crypto", "ecc", "ecies", "cryptocurrency"]
8+
keywords = ["secp256k1", "x25519", "crypto", "ecc", "ecies", "cryptocurrency"]
99
license = "MIT"
1010
readme = "README.md"
1111
# links
@@ -14,25 +14,27 @@ homepage = "https://ecies.org/rs/"
1414
repository = "https://github.com/ecies/rs"
1515

1616
[dependencies]
17-
hkdf = { version = "0.12.4", default-features = false }
18-
sha2 = { version = "0.10.8", default-features = false }
19-
2017
# elliptic curves
2118
libsecp256k1 = { version = "0.7.2", default-features = false, features = [
2219
"static-context",
2320
] }
24-
# x25519-dalek = {version = "2.0.0", default-features = false, features = ["static_secrets"], optional = true}
21+
x25519-dalek = { version = "2.0.1", default-features = false, features = [
22+
"static_secrets",
23+
], optional = true }
2524

26-
# openssl aes
25+
# symmetric ciphers
26+
# aes (openssl)
2727
openssl = { version = "0.10.71", default-features = false, optional = true }
28-
29-
# pure rust aes
28+
# aes (pure Rust)
3029
aes-gcm = { version = "0.10.3", default-features = false, optional = true }
3130
typenum = { version = "1.18.0", default-features = false, optional = true }
32-
33-
# chacha20 cipher
31+
# xchacha20
3432
chacha20poly1305 = { version = "0.10.1", default-features = false, optional = true }
3533

34+
# hash
35+
hkdf = { version = "0.12.4", default-features = false }
36+
sha2 = { version = "0.10.8", default-features = false }
37+
3638
# random number generator
3739
getrandom = { version = "=0.2.15", default-features = false }
3840
rand_core = { version = "0.6.4", default-features = false, features = [
@@ -52,22 +54,26 @@ once_cell = { version = "1.21.1", default-features = false, features = ["std"] }
5254
wasm-bindgen = { version = "0.2.100", default-features = false }
5355

5456
[target.'cfg(all(target_arch = "wasm32", not(target_os="unknown")))'.dependencies]
55-
# allows wasm32-wasi to build
57+
# for wasm32-wasi
5658
once_cell = { version = "1.21.1", default-features = false, features = ["std"] }
5759

5860
[features]
5961
default = ["openssl"]
60-
std = ["hkdf/std", "sha2/std", "once_cell/std", "libsecp256k1/std"]
62+
std = ["hkdf/std", "sha2/std", "once_cell/std"]
6163

62-
# curve
63-
# secp256k1 = ["libsecp256k1"]
64-
# x25519 = ["x25519-dalek"]
64+
# curves
65+
# no usage, TODO: make optional after 0.3.0
66+
secp256k1 = [] # ["libsecp256k1"]
67+
x25519 = ["x25519-dalek"]
6568

66-
# cipher
67-
aes-12bytes-nonce = [] # with feature "openssl" or "pure". default: 16 bytes
69+
# aes
6870
openssl = ["dep:openssl"]
6971
pure = ["aes-gcm/aes", "typenum"]
70-
xchacha20 = ["chacha20poly1305"]
72+
# with feature "openssl" or "pure" (aes-256-gcm)
73+
aes-12bytes-nonce = [] # default: 16 bytes without this
74+
75+
# xchacha20
76+
xchacha20 = ["chacha20poly1305/alloc"]
7177

7278
[dev-dependencies]
7379
criterion = { version = "0.5.1", default-features = false }
@@ -78,7 +84,7 @@ wasm-bindgen-test = "0.3.50"
7884

7985
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
8086
futures-util = "0.3.31"
81-
reqwest = "0.12.14"
87+
reqwest = "0.12.15"
8288
tokio = { version = "1.44.1", default-features = false, features = [
8389
"rt-multi-thread",
8490
] }

README.md

+20-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
[![Crates](https://img.shields.io/crates/v/ecies)](https://crates.io/crates/ecies)
88
[![Doc](https://docs.rs/ecies/badge.svg)](https://docs.rs/ecies/latest/ecies/)
99

10-
Elliptic Curve Integrated Encryption Scheme for secp256k1 in Rust, based on [pure Rust implementation](https://github.com/paritytech/libsecp256k1) of secp256k1.
10+
Elliptic Curve Integrated Encryption Scheme for secp256k1/x25519 in Rust, based on pure-Rust secp256k1/x25519 implementation.
1111

12-
ECIES functionalities are built upon AES-256-GCM and HKDF-SHA256.
12+
ECIES functionalities are built upon AES-256-GCM/XChaCha20-Poly1305 and HKDF-SHA256.
1313

1414
This is the Rust version of [eciespy](https://github.com/ecies/py).
1515

@@ -40,6 +40,14 @@ assert_eq!(
4040
);
4141
```
4242

43+
## Optional x25519 Support
44+
45+
You can choose to use x25519 (key exchange function on curve25519) instead of secp256k1:
46+
47+
```toml
48+
ecies = {version = "0.2", default-features = false, features = ["x25519"]}
49+
```
50+
4351
## Optional pure Rust AES backend
4452

4553
You can choose to use OpenSSL implementation or [pure Rust implementation](https://github.com/RustCrypto/AEADs) of AES-256-GCM:
@@ -62,11 +70,17 @@ If you select the pure Rust backend on modern x86 CPUs, consider building with
6270
RUSTFLAGS="-Ctarget-cpu=sandybridge -Ctarget-feature=+aes,+sse2,+sse4.1,+ssse3"
6371
```
6472

65-
to speed up AES encryption/decryption. This would be no longer necessary when [`aes-gcm` supports automatic CPU detection](https://github.com/RustCrypto/AEADs/issues/243#issuecomment-738821935).
73+
It can speed up AES encryption/decryption. This would be no longer necessary when [`aes-gcm` supports automatic CPU detection](https://github.com/RustCrypto/AEADs/issues/243#issuecomment-738821935).
74+
75+
On ARM CPUs, consider building with
76+
77+
```bash
78+
RUSTFLAGS="--cfg aes_armv8" # Rust 1.61+
79+
```
6680

6781
## WASM compatibility
6882

69-
It's also possible to build to the `wasm32-unknown-unknown` target with the pure Rust backend. Check out [this repo](https://github.com/ecies/rs-wasm) for more details.
83+
It's also possible to build to the `wasm32-unknown-unknown` target (or `wasm32-wasip2`) with the pure Rust backend. Check out [this repo](https://github.com/ecies/rs-wasm) for more details.
7084

7185
## Configuration
7286

@@ -82,6 +96,8 @@ You can also enable a pure Rust [XChaCha20-Poly1305](https://github.com/RustCryp
8296
ecies = {version = "0.2", default-features = false, features = ["xchacha20"]}
8397
```
8498

99+
### Secp256k1-specific configuration
100+
85101
Other behaviors can be configured by global static variable:
86102

87103
```rust

bench/simple.rs

+4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ fn criterion_benchmark(c: &mut Criterion) {
1111
use ecies::{decrypt, encrypt, utils::generate_keypair};
1212

1313
let (sk, pk) = generate_keypair();
14+
15+
#[cfg(not(feature = "x25519"))]
1416
let (sk, pk) = (&sk.serialize(), &pk.serialize());
17+
#[cfg(feature = "x25519")]
18+
let (sk, pk) = (sk.as_bytes(), pk.as_bytes());
1519

1620
let big = &BIG_MSG;
1721
let big_encrypted = &encrypt(pk, big).unwrap();

scripts/test-wasm.sh

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
3+
4+
# Pure Rust AES and XChaCha20 on WASM target
5+
cargo test --no-default-features --features $CURVE,pure --target=wasm32-unknown-unknown
6+
cargo test --no-default-features --features $CURVE,pure,std --target=wasm32-unknown-unknown
7+
cargo test --no-default-features --features $CURVE,xchacha20 --target=wasm32-unknown-unknown
8+
cargo test --no-default-features --features $CURVE,xchacha20,std --target=wasm32-unknown-unknown

scripts/test.sh

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
#!/bin/sh
22

33
# OpenSSL AES
4-
cargo test --no-default-features --features openssl
5-
cargo test --no-default-features --features openssl,std
6-
cargo test --no-default-features --features openssl,aes-12bytes-nonce
7-
cargo test --no-default-features --features openssl,std,aes-12bytes-nonce
4+
cargo test --no-default-features --features $CURVE,openssl
5+
cargo test --no-default-features --features $CURVE,openssl,std
6+
cargo test --no-default-features --features $CURVE,openssl,aes-12bytes-nonce
7+
cargo test --no-default-features --features $CURVE,openssl,std,aes-12bytes-nonce
88

99
# Pure Rust AES
10-
cargo test --no-default-features --features pure
11-
cargo test --no-default-features --features pure,std
12-
cargo test --no-default-features --features pure,aes-12bytes-nonce
13-
cargo test --no-default-features --features pure,std,aes-12bytes-nonce
10+
cargo test --no-default-features --features $CURVE,pure
11+
cargo test --no-default-features --features $CURVE,pure,std
12+
cargo test --no-default-features --features $CURVE,pure,aes-12bytes-nonce
13+
cargo test --no-default-features --features $CURVE,pure,std,aes-12bytes-nonce
1414

1515
# XChaCha20
16-
cargo test --no-default-features --features xchacha20
17-
cargo test --no-default-features --features xchacha20,std
16+
cargo test --no-default-features --features $CURVE,xchacha20
17+
cargo test --no-default-features --features $CURVE,xchacha20,std

src/config.rs

+11-14
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ pub fn is_ephemeral_key_compressed() -> bool {
2929
ECIES_CONFIG.read().is_ephemeral_key_compressed
3030
}
3131

32-
/// Get ephemeral key size: compressed(33) or uncompressed(65)
33-
// #[cfg(feature = "secp256k1")]
32+
/// Get hkdf key derived from compressed shared point or not
33+
pub fn is_hkdf_key_compressed() -> bool {
34+
ECIES_CONFIG.read().is_hkdf_key_compressed
35+
}
36+
37+
/// Get ephemeral key size: compressed(33) or uncompressed(65) on secp256k1 or 32 on x25519
38+
#[cfg(not(feature = "x25519"))]
3439
pub fn get_ephemeral_key_size() -> usize {
3540
use crate::consts::{COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE};
3641

@@ -41,17 +46,9 @@ pub fn get_ephemeral_key_size() -> usize {
4146
}
4247
}
4348

44-
// #[cfg(feature = "x25519")]
45-
// pub fn get_ephemeral_key_size() -> usize {
46-
// 32
47-
// }
48-
49-
// #[cfg(all(not(feature = "x25519"), not(feature = "secp256k1")))]
50-
// pub fn get_ephemeral_key_size() -> usize {
51-
// panic!("Not implemented")
52-
// }
49+
#[cfg(feature = "x25519")]
50+
pub fn get_ephemeral_key_size() -> usize {
51+
use crate::consts::PUBLIC_KEY_SIZE;
5352

54-
/// Get hkdf key derived from compressed shared point or not
55-
pub fn is_hkdf_key_compressed() -> bool {
56-
ECIES_CONFIG.read().is_hkdf_key_compressed
53+
PUBLIC_KEY_SIZE
5754
}

src/consts.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
/// Compressed public key size
2-
// #[cfg(feature = "secp256k1")]
2+
#[cfg(not(feature = "x25519"))]
33
pub const COMPRESSED_PUBLIC_KEY_SIZE: usize = 33;
44

55
/// Uncompressed public key size
6-
// #[cfg(feature = "secp256k1")]
6+
#[cfg(not(feature = "x25519"))]
77
pub const UNCOMPRESSED_PUBLIC_KEY_SIZE: usize = 65;
88

9+
#[cfg(feature = "x25519")]
10+
pub const PUBLIC_KEY_SIZE: usize = 32;
11+
912
/// Nonce length. AES (12/16 bytes) or XChaCha20 (24 bytes)
1013
#[cfg(all(not(feature = "aes-12bytes-nonce"), not(feature = "xchacha20")))]
1114
pub const NONCE_LENGTH: usize = 16;

src/elliptic/mod.rs

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
// #[cfg(feature = "secp256k1")]
1+
#[cfg(not(feature = "x25519"))]
22
mod secp256k1;
3-
// #[cfg(feature = "secp256k1")]
3+
#[cfg(not(feature = "x25519"))]
44
pub use secp256k1::{decapsulate, encapsulate, generate_keypair, PublicKey, SecretKey};
5-
// #[cfg(feature = "secp256k1")]
5+
#[cfg(not(feature = "x25519"))]
66
pub(crate) use secp256k1::{parse_pk, parse_sk, pk_to_vec, Error};
77

8-
// #[cfg(feature = "x25519")]
9-
// mod x25519;
10-
// #[cfg(feature = "x25519")]
11-
// pub use x25519::{decapsulate, encapsulate, generate_keypair, PublicKey, SecretKey};
12-
// #[cfg(feature = "x25519")]
13-
// pub(crate) use x25519::{parse_pk, parse_sk, pk_to_vec, Error};
8+
#[cfg(feature = "x25519")]
9+
mod x25519;
10+
#[cfg(feature = "x25519")]
11+
pub use x25519::{decapsulate, encapsulate, generate_keypair, PublicKey, SecretKey};
12+
#[cfg(feature = "x25519")]
13+
pub(crate) use x25519::{parse_pk, parse_sk, pk_to_vec, Error};
1414

1515
#[cfg(test)]
1616
mod tests {

src/elliptic/secp256k1.rs

+10
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ mod known_tests {
109109
decode_hex("6f982d63e8590c9d9b5b4c1959ff80315d772edd8f60287c9361d548d5200f82")
110110
);
111111
}
112+
113+
#[cfg(feature = "xchacha20")]
114+
#[test]
115+
pub fn test_known_encrypted_xchacha20() {
116+
use crate::decrypt;
117+
118+
let sk2 = decode_hex("0000000000000000000000000000000000000000000000000000000000000002");
119+
let encrypted = decode_hex("0x04e314abc14398e07974cd50221b682ed5f0629e977345fc03e2047208ee6e279ffb2a6942878d3798c968d89e59c999e082b0598d1b641968c48c8d47c570210d0ab1ade95eeca1080c45366562f9983faa423ee3fd3260757053d5843c5f453e1ee6bb955c8e5d4aee8572139357a091909357a8931b");
120+
assert_eq!(decrypt(&sk2, &encrypted).unwrap(), "helloworld🌍".as_bytes());
121+
}
112122
}
113123

114124
#[cfg(test)]

0 commit comments

Comments
 (0)