Skip to content

Commit 3374cad

Browse files
committed
Add ECIES encryption support to subxt-signer
Adds encrypt/decrypt methods to sr25519::Keypair using schnorrkel's new ECIES module. Gated behind the `ecies` feature flag. Depends on: paritytech/schnorrkel#116
1 parent 3f2a662 commit 3374cad

5 files changed

Lines changed: 121 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 38 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ bip39 = { version = "2.1.0", default-features = false }
173173
bip32 = { version = "0.5.2", default-features = false }
174174
hmac = { version = "0.12.1", default-features = false }
175175
pbkdf2 = { version = "0.12.2", default-features = false }
176-
schnorrkel = { version = "0.11.4", default-features = false }
176+
schnorrkel = { version = "0.11.5", default-features = false, git = "https://github.com/hitchhooker/schnorrkel", branch = "ecies" }
177177
secp256k1 = { version = "0.30.0", default-features = false }
178178
keccak-hash = { version = "0.11.0", default-features = false }
179179
secrecy = "0.10.3"

signer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ std = [
3636
# ecdsa compiling to WASM on my mac; following this comment helped:
3737
# https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699
3838
sr25519 = ["schnorrkel"]
39+
ecies = ["sr25519", "schnorrkel/ecies"]
3940
ecdsa = ["secp256k1"]
4041
unstable-eth = ["keccak-hash", "ecdsa", "secp256k1", "bip32"]
4142

signer/examples/ecies.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2019-2026 Parity Technologies (UK) Ltd.
2+
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3+
// see LICENSE for license details.
4+
5+
//! ECIES encryption/decryption example using sr25519 dev accounts.
6+
7+
use subxt_signer::sr25519;
8+
9+
fn main() {
10+
let alice = sr25519::dev::alice();
11+
let bob = sr25519::dev::bob();
12+
13+
let plaintext = b"The quick brown fox jumps over the lazy dog";
14+
let ctx = b"example-ecies-v1";
15+
16+
// Alice encrypts a message for Bob
17+
let encrypted = alice
18+
.encrypt(plaintext, &bob.public_key(), ctx)
19+
.expect("encryption failed");
20+
21+
println!("Plaintext: {} bytes", plaintext.len());
22+
println!("Ciphertext: {} bytes (overhead: {} bytes)", encrypted.len(), encrypted.len() - plaintext.len());
23+
24+
// Bob decrypts
25+
let decrypted = bob.decrypt(&encrypted, ctx).expect("decryption failed");
26+
assert_eq!(&decrypted[..], plaintext);
27+
println!("Bob decrypted successfully: {:?}", core::str::from_utf8(&decrypted).unwrap());
28+
29+
// Alice cannot decrypt (wrong key)
30+
let result = alice.decrypt(&encrypted, ctx);
31+
assert!(result.is_err());
32+
println!("Alice cannot decrypt Bob's message: {:?}", result.unwrap_err());
33+
34+
// Wrong context fails
35+
let result = bob.decrypt(&encrypted, b"wrong-context");
36+
assert!(result.is_err());
37+
println!("Wrong context fails: {:?}", result.unwrap_err());
38+
}

signer/src/sr25519.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,49 @@ impl Keypair {
182182
let signature = self.0.sign(context.bytes(message));
183183
Signature(signature.to_bytes())
184184
}
185+
186+
/// Encrypt `plaintext` for `recipient` using ECIES over sr25519.
187+
///
188+
/// The `ctx` parameter provides domain separation — use a unique
189+
/// application-specific byte string (e.g. `b"my-app-v1"`).
190+
///
191+
/// # Example
192+
///
193+
/// ```rust,standalone_crate
194+
/// use subxt_signer::sr25519;
195+
///
196+
/// let alice = sr25519::dev::alice();
197+
/// let bob = sr25519::dev::bob();
198+
///
199+
/// let encrypted = alice.encrypt(b"secret message", &bob.public_key(), b"example")
200+
/// .expect("encryption works");
201+
/// let decrypted = bob.decrypt(&encrypted, b"example")
202+
/// .expect("decryption works");
203+
/// assert_eq!(decrypted, b"secret message");
204+
/// ```
205+
#[cfg(feature = "ecies")]
206+
pub fn encrypt(
207+
&self,
208+
plaintext: &[u8],
209+
recipient: &PublicKey,
210+
ctx: &[u8],
211+
) -> Result<alloc::vec::Vec<u8>, schnorrkel::ecies::EciesError> {
212+
let recipient_pk = schnorrkel::PublicKey::from_bytes(&recipient.0)
213+
.map_err(|_| schnorrkel::ecies::EciesError::InvalidEphemeralKey)?;
214+
schnorrkel::ecies::encrypt(plaintext, &recipient_pk, ctx)
215+
}
216+
217+
/// Decrypt an ECIES ciphertext using this keypair's secret key.
218+
///
219+
/// The `ctx` must match the context used during encryption.
220+
#[cfg(feature = "ecies")]
221+
pub fn decrypt(
222+
&self,
223+
ciphertext: &[u8],
224+
ctx: &[u8],
225+
) -> Result<alloc::vec::Vec<u8>, schnorrkel::ecies::EciesError> {
226+
schnorrkel::ecies::decrypt(ciphertext, &self.0.secret, ctx)
227+
}
185228
}
186229

187230
/// Verify that some signature for a message was created by the owner of the [`PublicKey`].

0 commit comments

Comments
 (0)