diff --git a/Cargo.lock b/Cargo.lock index b7ed4711f27..15fbbb6a429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -942,6 +942,19 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -4591,13 +4604,32 @@ dependencies = [ [[package]] name = "schnorrkel" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de18f6d8ba0aad7045f5feae07ec29899c1112584a38509a84ad7b04451eaa0" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec 0.7.6", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "git+https://github.com/hitchhooker/schnorrkel?branch=ecies#f931ad4320461ca5fcc1fbe5074c789ca069cd52" dependencies = [ "aead", "arrayref", "arrayvec 0.7.6", + "chacha20poly1305", "curve25519-dalek", "getrandom_or_panic", "merlin", @@ -5016,7 +5048,7 @@ dependencies = [ "rand", "rand_chacha", "ruzstd", - "schnorrkel", + "schnorrkel 0.11.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "sha2 0.10.9", @@ -5189,7 +5221,7 @@ dependencies = [ "primitive-types", "rand", "scale-info", - "schnorrkel", + "schnorrkel 0.11.5 (registry+https://github.com/rust-lang/crates.io-index)", "secp256k1 0.28.2", "secrecy 0.8.0", "serde", @@ -5621,7 +5653,7 @@ checksum = "ca58ffd742f693dc13d69bdbb2e642ae239e0053f6aab3b104252892f856700a" dependencies = [ "hmac 0.12.1", "pbkdf2", - "schnorrkel", + "schnorrkel 0.11.5 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.10.9", "zeroize", ] @@ -5847,7 +5879,7 @@ dependencies = [ "pbkdf2", "proptest", "regex", - "schnorrkel", + "schnorrkel 0.11.5 (git+https://github.com/hitchhooker/schnorrkel?branch=ecies)", "scrypt", "secp256k1 0.30.0", "secrecy 0.10.3", diff --git a/Cargo.toml b/Cargo.toml index 6e89d444d58..52e36f56dac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } secp256k1 = { version = "0.30.0", default-features = false } keccak-hash = { version = "0.11.0", default-features = false } secrecy = "0.10.3" diff --git a/signer/Cargo.toml b/signer/Cargo.toml index d281f70958e..851ff5af050 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -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"] ecdsa = ["secp256k1"] unstable-eth = ["keccak-hash", "ecdsa", "secp256k1", "bip32"] diff --git a/signer/examples/ecies.rs b/signer/examples/ecies.rs new file mode 100644 index 00000000000..f04be15041e --- /dev/null +++ b/signer/examples/ecies.rs @@ -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()); +} diff --git a/signer/src/sr25519.rs b/signer/src/sr25519.rs index 56aeeeb03f5..d373c47428c 100644 --- a/signer/src/sr25519.rs +++ b/signer/src/sr25519.rs @@ -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, 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, 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`]. diff --git a/signer/tests/wasm/Cargo.lock b/signer/tests/wasm/Cargo.lock index 86e85b12da6..d5ccb235087 100644 --- a/signer/tests/wasm/Cargo.lock +++ b/signer/tests/wasm/Cargo.lock @@ -307,6 +307,19 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "cipher" version = "0.4.4" @@ -789,9 +802,9 @@ dependencies = [ [[package]] name = "frame-decode" -version = "0.16.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63257bb5f8d7a707d626aa1b4e464c3b9720edd168b73cee98f48f0dd6386e4" +checksum = "88cda60c640572c970c544ba5879375a18ecfb2c47c617be8265830b63df193d" dependencies = [ "frame-metadata", "parity-scale-codec", @@ -2105,6 +2118,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "git+https://github.com/hitchhooker/schnorrkel?branch=ecies#78d9d86cc291f66299db0a576c25129d865658a3" +dependencies = [ + "aead", + "arrayref", + "arrayvec 0.7.6", + "chacha20poly1305", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -2397,7 +2429,7 @@ dependencies = [ "rand", "rand_chacha", "ruzstd", - "schnorrkel", + "schnorrkel 0.11.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "sha2 0.10.9", @@ -2660,7 +2692,7 @@ dependencies = [ "parity-scale-codec", "pbkdf2", "regex", - "schnorrkel", + "schnorrkel 0.11.5 (git+https://github.com/hitchhooker/schnorrkel?branch=ecies)", "scrypt", "secp256k1 0.30.0", "secrecy", diff --git a/signer/tests/wasm/Cargo.toml b/signer/tests/wasm/Cargo.toml index 3ee73f727bd..ec4cdb1c24d 100644 --- a/signer/tests/wasm/Cargo.toml +++ b/signer/tests/wasm/Cargo.toml @@ -17,6 +17,7 @@ subxt-signer = { path = "../../", default-features = false, features = [ "web", "sr25519", "ecdsa", + "ecies", "unstable-eth", "std", ] } diff --git a/signer/tests/wasm/tests/wasm.rs b/signer/tests/wasm/tests/wasm.rs index 1051877225c..a3f9148c80c 100644 --- a/signer/tests/wasm/tests/wasm.rs +++ b/signer/tests/wasm/tests/wasm.rs @@ -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();