Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ bip39 = { version = "2.1.0", default-features = false }
bip32 = { version = "0.5.2", default-features = false }
hmac = { version = "0.12.1", default-features = false }
pbkdf2 = { version = "0.12.2", default-features = false }
schnorrkel = { version = "0.11.4", default-features = false }
schnorrkel = { version = "0.11.5", default-features = false, git = "https://github.com/hitchhooker/schnorrkel", branch = "ecies" }
Copy link
Copy Markdown
Collaborator

@jsdw jsdw Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until the original non-fork version of this has the relevant feature and is available on crates.io, we won't be able to merge this: git URLs prevent publishing, and I would not want to deviate from the Parity crate, in large part for security reasons :)

secp256k1 = { version = "0.30.0", default-features = false }
keccak-hash = { version = "0.11.0", default-features = false }
secrecy = "0.10.3"
Expand Down
1 change: 1 addition & 0 deletions signer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ std = [
# ecdsa compiling to WASM on my mac; following this comment helped:
# https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699
sr25519 = ["schnorrkel"]
ecies = ["sr25519", "schnorrkel/ecies"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether this feature would be better called "sr25519-ecies" since it seems specific to that (and then it would be a stronger hint that this enables some feature in the sr25519 module)

ecdsa = ["secp256k1"]
unstable-eth = ["keccak-hash", "ecdsa", "secp256k1", "bip32"]

Expand Down
38 changes: 38 additions & 0 deletions signer/examples/ecies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2019-2026 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.

//! ECIES encryption/decryption example using sr25519 dev accounts.

use subxt_signer::sr25519;

fn main() {
let alice = sr25519::dev::alice();
let bob = sr25519::dev::bob();

let plaintext = b"The quick brown fox jumps over the lazy dog";
let ctx = b"example-ecies-v1";

// Alice encrypts a message for Bob
let encrypted = alice
.encrypt(plaintext, &bob.public_key(), ctx)
.expect("encryption failed");

println!("Plaintext: {} bytes", plaintext.len());
println!("Ciphertext: {} bytes (overhead: {} bytes)", encrypted.len(), encrypted.len() - plaintext.len());

// Bob decrypts
let decrypted = bob.decrypt(&encrypted, ctx).expect("decryption failed");
assert_eq!(&decrypted[..], plaintext);
println!("Bob decrypted successfully: {:?}", core::str::from_utf8(&decrypted).unwrap());

// Alice cannot decrypt (wrong key)
let result = alice.decrypt(&encrypted, ctx);
assert!(result.is_err());
println!("Alice cannot decrypt Bob's message: {:?}", result.unwrap_err());

// Wrong context fails
let result = bob.decrypt(&encrypted, b"wrong-context");
assert!(result.is_err());
println!("Wrong context fails: {:?}", result.unwrap_err());
}
43 changes: 43 additions & 0 deletions signer/src/sr25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,49 @@ impl Keypair {
let signature = self.0.sign(context.bytes(message));
Signature(signature.to_bytes())
}

/// Encrypt `plaintext` for `recipient` using ECIES over sr25519.
///
/// The `ctx` parameter provides domain separation — use a unique
/// application-specific byte string (e.g. `b"my-app-v1"`).
///
/// # Example
///
/// ```rust,standalone_crate
/// use subxt_signer::sr25519;
///
/// let alice = sr25519::dev::alice();
/// let bob = sr25519::dev::bob();
///
/// let encrypted = alice.encrypt(b"secret message", &bob.public_key(), b"example")
/// .expect("encryption works");
/// let decrypted = bob.decrypt(&encrypted, b"example")
/// .expect("decryption works");
/// assert_eq!(decrypted, b"secret message");
/// ```
#[cfg(feature = "ecies")]
pub fn encrypt(
&self,
plaintext: &[u8],
recipient: &PublicKey,
ctx: &[u8],
) -> Result<alloc::vec::Vec<u8>, schnorrkel::ecies::EciesError> {
let recipient_pk = schnorrkel::PublicKey::from_bytes(&recipient.0)
.map_err(|_| schnorrkel::ecies::EciesError::InvalidEphemeralKey)?;
schnorrkel::ecies::encrypt(plaintext, &recipient_pk, ctx)
}

/// Decrypt an ECIES ciphertext using this keypair's secret key.
///
/// The `ctx` must match the context used during encryption.
#[cfg(feature = "ecies")]
pub fn decrypt(
&self,
ciphertext: &[u8],
ctx: &[u8],
) -> Result<alloc::vec::Vec<u8>, schnorrkel::ecies::EciesError> {
schnorrkel::ecies::decrypt(ciphertext, &self.0.secret, ctx)
}
}

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