diff --git a/Cargo.lock b/Cargo.lock index fd1735a7..0e648412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1212,6 +1212,30 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "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 = "chrono" version = "0.4.41" @@ -1271,6 +1295,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -2311,6 +2336,7 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", "pkcs8", "rand_core 0.6.4", "sec1", @@ -5082,6 +5108,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -6431,12 +6468,15 @@ name = "shared" version = "0.1.0" dependencies = [ "borsh 1.5.7", + "chacha20poly1305", "cosmwasm-std", "cw-storage-plus", "hex", + "hkdf", "k256", "rand 0.8.5", "serde", + "sha2 0.10.9", "thiserror 2.0.12", ] diff --git a/Cargo.toml b/Cargo.toml index 9fb95b4a..4363dd71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,8 @@ fjall = "2.9.0" futures = "0.3.31" futures-util = "0.3.31" hex = "0.4.3" +chacha20poly1305 = "0.10.1" +hkdf = "0.12.4" jiff = { version = "0.2.3", features = ["serde"] } k256 = "0.13.4" kademlia-discovery = { path = "packages/examples/kademlia-discovery" } diff --git a/packages/shared/Cargo.toml b/packages/shared/Cargo.toml index 23cdcea1..8b10d2e8 100644 --- a/packages/shared/Cargo.toml +++ b/packages/shared/Cargo.toml @@ -6,19 +6,30 @@ edition = "2021" license = "MIT" [dependencies] +chacha20poly1305 = { workspace = true, optional = true } borsh = { workspace = true, features = ["derive"], optional = true } cosmwasm-std = { workspace = true, optional = true } cw-storage-plus = { workspace = true, optional = true } +hkdf = { workspace = true, optional = true } hex = { workspace = true, optional = true } -k256 = { workspace = true, features = ["ecdsa", "serde", "sha256"], optional = true } +k256 = { workspace = true, features = ["ecdsa", "ecdh", "serde", "sha256"], optional = true } rand = { workspace = true, optional = true } serde = { workspace = true } +sha2 = { workspace = true, optional = true } thiserror = { workspace = true } [features] chaincryptography = [] cosmwasm = ["dep:cosmwasm-std", "dep:cw-storage-plus"] -realcryptography = ["borsh?/std", "dep:hex", "dep:k256", "dep:rand"] +realcryptography = [ + "borsh?/std", + "dep:chacha20poly1305", + "dep:hex", + "dep:hkdf", + "dep:k256", + "dep:rand", + "dep:sha2", +] solana = ["dep:borsh", "dep:hex"] [lints] diff --git a/packages/shared/src/cryptography/real.rs b/packages/shared/src/cryptography/real.rs index dfeec5ff..128a2c31 100644 --- a/packages/shared/src/cryptography/real.rs +++ b/packages/shared/src/cryptography/real.rs @@ -1,7 +1,16 @@ pub use rand::rngs::ThreadRng; +use chacha20poly1305::{ + aead::{AeadInPlace, KeyInit}, + ChaCha20Poly1305, Key, Nonce, Tag, +}; +use k256::ecdh::{diffie_hellman, EphemeralSecret, SharedSecret}; +use rand::{CryptoRng, RngCore}; +use sha2::Sha256; use std::{fmt::Display, str::FromStr}; +const KEY_DERIVATION_INFO: &[u8] = b"kolme/chacha20poly1305-v1"; + /// Newtype wrapper around [k256::PublicKey] to provide consistent serialization. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct PublicKey(k256::PublicKey); @@ -55,6 +64,41 @@ impl PublicKey { bytes: bytes.to_owned(), }) } + + /// Encrypt data so it can be decrypted by the corresponding [SecretKey]. + pub fn encrypt( + &self, + plaintext: impl AsRef<[u8]>, + ) -> Result { + self.encrypt_with(&mut rand::thread_rng(), plaintext) + } + + pub fn encrypt_with( + &self, + rng: &mut (impl CryptoRng + RngCore), + plaintext: impl AsRef<[u8]>, + ) -> Result { + let ephemeral_secret = EphemeralSecret::random(rng); + let ephemeral_public_key: PublicKey = k256::PublicKey::from(&ephemeral_secret).into(); + let shared_secret = ephemeral_secret.diffie_hellman(&self.0); + let key = derive_encryption_key(&shared_secret)?; + + let mut nonce_bytes = [0u8; 12]; + rng.fill_bytes(&mut nonce_bytes); + + let cipher = ChaCha20Poly1305::new(&key); + let mut buffer = plaintext.as_ref().to_vec(); + let tag = cipher + .encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), &[], &mut buffer) + .map_err(|_| EncryptionError::EncryptionFailed)?; + + Ok(EncryptedMessage { + ephemeral_public_key, + nonce: nonce_bytes, + ciphertext: buffer, + tag: tag.into(), + }) + } } impl From for PublicKey { @@ -157,6 +201,24 @@ pub enum SecretKeyError { InvalidBytes { source: k256::elliptic_curve::Error }, } +#[derive(thiserror::Error, Debug)] +pub enum EncryptionError { + #[error("Key derivation failed")] + KeyDerivationFailed, + #[error("Encryption failed")] + EncryptionFailed, + #[error("Decryption failed")] + DecryptionFailed, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EncryptedMessage { + pub ephemeral_public_key: PublicKey, + pub nonce: [u8; 12], + pub ciphertext: Vec, + pub tag: [u8; 16], +} + #[derive(Clone, PartialEq, Eq)] pub struct SecretKey(k256::SecretKey); @@ -215,6 +277,23 @@ impl SecretKey { pub fn reveal_as_hex(&self) -> String { hex::encode(self.0.to_bytes()) } + + pub fn decrypt(&self, message: &EncryptedMessage) -> Result, EncryptionError> { + let shared_secret = diffie_hellman( + self.0.to_nonzero_scalar(), + message.ephemeral_public_key.0.as_affine(), + ); + let key = derive_encryption_key(&shared_secret)?; + + let cipher = ChaCha20Poly1305::new(&key); + let mut buffer = message.ciphertext.clone(); + let tag = Tag::from_slice(&message.tag); + cipher + .decrypt_in_place_detached(Nonce::from_slice(&message.nonce), &[], &mut buffer, tag) + .map_err(|_| EncryptionError::DecryptionFailed)?; + + Ok(buffer) + } } impl FromStr for SecretKey { @@ -231,6 +310,15 @@ impl std::fmt::Debug for SecretKey { } } +fn derive_encryption_key(shared_secret: &SharedSecret) -> Result { + let hkdf = shared_secret.extract::(None); + let mut okm = [0u8; 32]; + hkdf.expand(KEY_DERIVATION_INFO, &mut okm) + .map_err(|_| EncryptionError::KeyDerivationFailed)?; + + Ok(Key::clone_from_slice(&okm)) +} + mod sigerr { #[derive(thiserror::Error, Debug)] pub enum SignatureError { @@ -416,3 +504,42 @@ impl SignatureWithRecovery { PublicKey::recover_from_msg(msg, &self.sig, self.recid) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_decrypt_roundtrip() { + let receiver = SecretKey::random(); + let plaintext = b"hello world"; + + let encrypted = receiver.public_key().encrypt(plaintext).unwrap(); + let decrypted = receiver.decrypt(&encrypted).unwrap(); + + assert_eq!(decrypted, plaintext); + } + + #[test] + fn encrypt_is_randomized() { + let receiver = SecretKey::random(); + let plaintext = b"stable text"; + + let first = receiver.public_key().encrypt(plaintext).unwrap(); + let second = receiver.public_key().encrypt(plaintext).unwrap(); + + assert_ne!(first.nonce, second.nonce); + assert_ne!(first.ciphertext, second.ciphertext); + assert_ne!(first.ephemeral_public_key, second.ephemeral_public_key); + } + + #[test] + fn tampering_fails() { + let receiver = SecretKey::random(); + let mut encrypted = receiver.public_key().encrypt(b"super secret").unwrap(); + + encrypted.ciphertext[0] ^= 0b0000_0001; + + assert!(receiver.decrypt(&encrypted).is_err()); + } +} diff --git a/solana/Cargo.lock b/solana/Cargo.lock index 81f65ff8..f6f6d503 100644 --- a/solana/Cargo.lock +++ b/solana/Cargo.lock @@ -545,6 +545,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "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 = "chrono" version = "0.4.40" @@ -568,6 +592,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -875,6 +900,7 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", "pkcs8", "rand_core 0.6.4", "sec1", @@ -1176,6 +1202,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.8.1" @@ -2098,6 +2133,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -2563,10 +2609,13 @@ name = "shared" version = "0.1.0" dependencies = [ "borsh 1.5.7", + "chacha20poly1305", "hex", + "hkdf", "k256", "rand 0.8.5", "serde", + "sha2 0.10.8", "thiserror 2.0.12", ]