Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
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
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
42 changes: 42 additions & 0 deletions signer/examples/ecies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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 alice_vk = alice.viewing_key();
let bob_vk = bob.viewing_key();

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

// Alice encrypts a message for Bob
let encrypted = alice
.encrypt(plaintext, bob_vk.ivk_public(), ctx, alice_vk.ovk())
.expect("encryption failed");

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

// Bob decrypts with his viewing key
let decrypted = bob_vk.decrypt_incoming(&encrypted, ctx).expect("decryption failed");
assert_eq!(&decrypted[..], plaintext);
println!("Bob decrypted: {:?}", core::str::from_utf8(&decrypted).unwrap());

// Alice re-reads her outgoing message
let outgoing = alice_vk
.decrypt_outgoing(&encrypted, bob_vk.ivk_public(), ctx)
.expect("outgoing decryption failed");
assert_eq!(&outgoing[..], plaintext);
println!("Alice re-read outgoing: {:?}", core::str::from_utf8(&outgoing).unwrap());

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

/// Derive the full viewing key for this keypair.
///
/// The viewing key allows decrypting incoming and outgoing ECIES
/// messages without signing authority.
#[cfg(feature = "ecies")]
pub fn viewing_key(&self) -> schnorrkel::viewing_key::FullViewingKey {
schnorrkel::viewing_key::FullViewingKey::from_keypair(&self.0)
}

/// Encrypt `plaintext` for `recipient` using ECIES over sr25519.
///
/// `recipient` should be an IVK public key (from the recipient's
/// viewing key), and `sender_ovk` is this keypair's outgoing
/// viewing key so the sender can later re-read the message.
///
/// # Example
///
/// ```rust,standalone_crate
/// use subxt_signer::sr25519;
///
/// let alice = sr25519::dev::alice();
/// let bob = sr25519::dev::bob();
/// let alice_vk = alice.viewing_key();
/// let bob_vk = bob.viewing_key();
///
/// let encrypted = alice.encrypt(
/// b"secret message", bob_vk.ivk_public(), b"example", alice_vk.ovk(),
/// ).expect("encryption works");
///
/// let decrypted = bob_vk.decrypt_incoming(&encrypted, b"example")
/// .expect("decryption works");
/// assert_eq!(decrypted, b"secret message");
/// ```
#[cfg(feature = "ecies")]
pub fn encrypt(
&self,
plaintext: &[u8],
recipient: &schnorrkel::PublicKey,
ctx: &[u8],
sender_ovk: &schnorrkel::viewing_key::OutgoingViewingKey,
) -> Result<alloc::vec::Vec<u8>, schnorrkel::ecies::EciesError> {
schnorrkel::ecies::encrypt(plaintext, recipient, ctx, sender_ovk)
}

/// 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
40 changes: 36 additions & 4 deletions signer/tests/wasm/Cargo.lock

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

1 change: 1 addition & 0 deletions signer/tests/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ subxt-signer = { path = "../../", default-features = false, features = [
"web",
"sr25519",
"ecdsa",
"ecies",
"unstable-eth",
"std",
] }
Expand Down
21 changes: 21 additions & 0 deletions signer/tests/wasm/tests/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ async fn wasm_sr25519_signing_works() {
));
}

#[wasm_bindgen_test]
async fn wasm_sr25519_ecies_works() {
let alice = sr25519::dev::alice();
let bob = sr25519::dev::bob();

let plaintext = b"hello from wasm";
let ctx = b"wasm-ecies-test";

let encrypted = alice.encrypt(plaintext, &bob.public_key(), ctx)
.expect("encryption failed");
let decrypted = bob.decrypt(&encrypted, ctx)
.expect("decryption failed");

assert_eq!(&decrypted[..], plaintext);

// Wrong key should fail
assert!(alice.decrypt(&encrypted, ctx).is_err());
// Wrong context should fail
assert!(bob.decrypt(&encrypted, b"wrong-ctx").is_err());
}

#[wasm_bindgen_test]
async fn wasm_ecdsa_signing_works() {
let alice = ecdsa::dev::alice();
Expand Down