diff --git a/Cargo.lock b/Cargo.lock index 21fda8353..397d4f79c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,15 @@ dependencies = [ "regex", ] +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -30,13 +39,14 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if 1.0.0", "cipher", "cpufeatures", + "zeroize", ] [[package]] @@ -136,6 +146,12 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "arrayvec" version = "0.7.2" @@ -250,6 +266,21 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16ct" version = "0.1.1" @@ -648,6 +679,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -705,7 +737,7 @@ dependencies = [ "k256 0.13.1", "lazy_static", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -722,7 +754,7 @@ dependencies = [ "once_cell", "pbkdf2 0.12.1", "rand 0.8.5", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -741,7 +773,7 @@ dependencies = [ "ripemd", "serde", "serde_derive", - "sha2 0.10.7", + "sha2 0.10.8", "sha3 0.10.8", "thiserror", ] @@ -911,20 +943,33 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.2" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "packed_simd_2", "platforms", + "rustc_version", "serde", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + [[package]] name = "data-encoding" version = "2.4.0" @@ -1141,15 +1186,15 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0-rc.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a" +checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ "curve25519-dalek", "ed25519", "rand_core 0.6.4", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "zeroize", ] @@ -1285,7 +1330,7 @@ dependencies = [ "scrypt", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "sha3 0.10.8", "thiserror", "uuid 0.8.2", @@ -1669,7 +1714,7 @@ dependencies = [ "ethers-core 2.0.4", "hex", "rand 0.8.5", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", "tracing", ] @@ -1778,9 +1823,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" [[package]] name = "fixed-hash" @@ -2062,6 +2107,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "glob" version = "0.3.1" @@ -2284,7 +2335,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -2584,7 +2635,7 @@ dependencies = [ "cfg-if 1.0.0", "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.7", + "sha2 0.10.8", "sha3 0.10.8", ] @@ -2598,7 +2649,7 @@ dependencies = [ "ecdsa 0.15.1", "elliptic-curve 0.12.3", "once_cell", - "sha2 0.10.7", + "sha2 0.10.8", "signature 2.0.0", ] @@ -2612,7 +2663,7 @@ dependencies = [ "ecdsa 0.16.7", "elliptic-curve 0.13.5", "once_cell", - "sha2 0.10.7", + "sha2 0.10.8", "signature 2.0.0", ] @@ -2680,15 +2731,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" - -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libsqlite3-sys" @@ -3000,6 +3045,15 @@ dependencies = [ "syn 2.0.16", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -3097,16 +3151,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if 1.0.0", - "libm", -] - [[package]] name = "parity-scale-codec" version = "2.3.1" @@ -3264,7 +3308,7 @@ dependencies = [ "digest 0.10.7", "hmac", "password-hash", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -3386,9 +3430,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -4003,6 +4047,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -4184,7 +4234,7 @@ dependencies = [ "hmac", "pbkdf2 0.11.0", "salsa20", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -4396,9 +4446,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -4493,6 +4543,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "soketto" version = "0.7.1" @@ -4624,7 +4684,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", "url", "zip", @@ -4793,11 +4853,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", + "backtrace", "bytes 1.4.0", "libc", "mio 0.8.6", @@ -4805,7 +4865,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.4", "tokio-macros", "windows-sys 0.48.0", ] @@ -5226,7 +5286,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "subtle", "thiserror", "x25519-dalek", @@ -5732,9 +5792,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-rc.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ "curve25519-dalek", "rand_core 0.6.4", @@ -5767,9 +5827,12 @@ dependencies = [ name = "xmtp" version = "0.1.0" dependencies = [ + "aes", "anyhow", + "arrayref", "async-trait", "base64 0.21.1", + "ctr", "diesel", "diesel_migrations", "env_logger", @@ -5777,18 +5840,21 @@ dependencies = [ "ethers-core 2.0.4", "futures", "hex", + "hkdf", "libsqlite3-sys", "log", "prost", "rand 0.8.5", "serde", "serde_json", + "sha2 0.10.8", "tempfile", "thiserror", "tokio", "toml 0.7.4", "uuid 1.3.3", "vodozemac", + "x25519-dalek", "xmtp_cryptography", "xmtp_proto", ] @@ -5827,7 +5893,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "sha3 0.10.8", "thiserror", "tokio", @@ -5864,7 +5930,7 @@ dependencies = [ "rand_chacha 0.3.1", "rlp", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "sha3 0.10.8", "thiserror", "tokio", diff --git a/xmtp/Cargo.toml b/xmtp/Cargo.toml index c449e2103..a0ead0cff 100644 --- a/xmtp/Cargo.toml +++ b/xmtp/Cargo.toml @@ -17,11 +17,13 @@ grpc = ["xmtp_proto/grpc"] native = ["libsqlite3-sys/bundled-sqlcipher-vendored-openssl"] [dependencies] +aes = { version = "0.8.3", features = ["zeroize"] } serde = "1.0.160" +ctr = { version = "0.9.2", features = ["zeroize"] } serde_json = "1.0.96" thiserror = "1.0.40" vodozemac = {git= "https://github.com/xmtp/vodozemac", branch="dev"} -xmtp_proto = { path = "../xmtp_proto", features = ["proto_full"] } +xmtp_proto = { path = "../xmtp_proto", features = ["proto_full", "xmtp-message_api-v1"] } async-trait = "0.1.68" xmtp_cryptography = { path = "../xmtp_cryptography"} hex = "0.4.3" @@ -36,8 +38,12 @@ ethers-core = "2.0.4" prost = { version = "0.11", features = ["prost-derive"] } futures = "0.3.28" base64 = "0.21.1" -tokio = "1.28.1" +tokio = { version = "1.32.0", features = ["full"] } anyhow = "1.0.71" +x25519-dalek = "2.0.0" +hkdf = "0.12.3" +arrayref = "0.3.7" +sha2 = "0.10.8" [dev-dependencies] tempfile = "3.5.0" diff --git a/xmtp/src/cryptography.rs b/xmtp/src/cryptography.rs new file mode 100644 index 000000000..773fed132 --- /dev/null +++ b/xmtp/src/cryptography.rs @@ -0,0 +1,80 @@ +use aes::cipher::{KeyIvInit, StreamCipher}; +use aes::Aes256; +use ethers_core::k256::elliptic_curve::subtle::ConstantTimeEq; +use hkdf::hmac::{Hmac, Mac}; +use sha2::Sha256; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EncryptionError { + #[error("bad data")] + BadKeyOrIv, + #[error("unknown error")] + Unknown, +} + +#[derive(Debug, Error)] +pub enum DecryptionError { + #[error("bad ciphertext {0}")] + BadCiphertext(String), + #[error("bad data")] + BadKeyOrIv, + #[error("unknown error")] + Unknown, +} + +pub(crate) fn aes_256_ctr_encrypt(ptext: &[u8], key: &[u8]) -> Result, EncryptionError> { + let key: [u8; 32] = key.try_into().map_err(|_| EncryptionError::BadKeyOrIv)?; + + let zero_nonce = [0u8; 16]; + let mut cipher = ctr::Ctr32BE::::new(key[..].into(), zero_nonce[..].into()); + + let mut ctext = ptext.to_vec(); + cipher.apply_keystream(&mut ctext); + Ok(ctext) +} + +fn aes_256_ctr_decrypt(ctext: &[u8], key: &[u8]) -> Result, DecryptionError> { + aes_256_ctr_encrypt(ctext, key).map_err(|e| match e { + EncryptionError::BadKeyOrIv => DecryptionError::BadKeyOrIv, + EncryptionError::Unknown => DecryptionError::Unknown, + }) +} + +pub(crate) fn hmac_sha256(key: &[u8], input: &[u8]) -> [u8; 32] { + let mut hmac = Hmac::::new_from_slice(key).expect("HMAC-SHA256 failed to create"); + hmac.update(input); + hmac.finalize().into_bytes().into() +} + +pub(crate) fn aes256_ctr_hmac_sha256_encrypt( + msg: &[u8], + cipher_key: &[u8], + mac_key: &[u8], +) -> Result, EncryptionError> { + let mut ctext = aes_256_ctr_encrypt(msg, cipher_key)?; + let mac = hmac_sha256(mac_key, &ctext); + ctext.extend_from_slice(&mac[..10]); + Ok(ctext) +} + +pub(crate) fn aes256_ctr_hmac_sha256_decrypt( + ciphertext: &[u8], + cipher_key: &[u8], + mac_key: &[u8], +) -> Result, DecryptionError> { + if ciphertext.len() < 10 { + return Err(DecryptionError::BadCiphertext( + "truncated ciphertext".to_string(), + )); + } + let ptext_len = ciphertext.len() - 10; + let our_mac = hmac_sha256(mac_key, &ciphertext[..ptext_len]); + let same: bool = our_mac[..10].ct_eq(&ciphertext[ptext_len..]).into(); + if !same { + return Err(DecryptionError::BadCiphertext( + "MAC verification failed".to_string(), + )); + } + aes_256_ctr_decrypt(&ciphertext[..ptext_len], cipher_key) +} diff --git a/xmtp/src/lib.rs b/xmtp/src/lib.rs index aca727d50..aea4641ca 100644 --- a/xmtp/src/lib.rs +++ b/xmtp/src/lib.rs @@ -6,10 +6,12 @@ mod codecs; pub mod contact; pub mod conversation; pub mod conversations; +mod cryptography; pub mod message; pub mod mock_xmtp_api_client; pub mod owner; pub mod persistence; +pub mod sealed_sender; pub mod session; pub mod storage; mod test_utils; diff --git a/xmtp/src/sealed_sender.rs b/xmtp/src/sealed_sender.rs new file mode 100644 index 000000000..5f0d45a65 --- /dev/null +++ b/xmtp/src/sealed_sender.rs @@ -0,0 +1,222 @@ +use crate::cryptography::{ + aes256_ctr_hmac_sha256_decrypt, aes256_ctr_hmac_sha256_encrypt, DecryptionError, + EncryptionError, +}; +use arrayref::array_ref; +use ethers_core::k256::sha2; + +use rand::thread_rng; +use x25519_dalek::{PublicKey, StaticSecret}; + +const EPHEMERAL_KEYS_KDF_LEN: usize = 96; +const SALT_PREFIX: &[u8] = b"SealedSender"; + +pub struct SealedSenderMessage { + pub ephemeral_public_key: Vec, + pub encrypted_static_key: Vec, + pub encrypted_message: Vec, +} + +impl SealedSenderMessage { + fn new( + ephemeral_public_key: Vec, + encrypted_static_key: Vec, + encrypted_message: Vec, + ) -> Self { + Self { + ephemeral_public_key, + encrypted_static_key, + encrypted_message, + } + } +} + +pub(super) struct EphemeralKeys { + pub(super) ephemeral_public_key: [u8; 32], + pub(super) chain_key: [u8; 32], + pub(super) cipher_key: [u8; 32], + pub(super) mac_key: [u8; 32], +} + +impl EphemeralKeys { + pub(super) fn build( + our_pub_key: &PublicKey, + // TODO: Replace with EphemeralSecret once I figure out how to initialize it + our_priv_key: &StaticSecret, + their_pub_key: &PublicKey, + is_sender: bool, + ) -> Self { + let our_pub_key_bytes = our_pub_key.as_bytes(); + let their_pub_key_bytes = their_pub_key.as_bytes(); + let ephemeral_salt = match is_sender { + true => [SALT_PREFIX, their_pub_key_bytes, our_pub_key_bytes], + false => [SALT_PREFIX, our_pub_key_bytes, their_pub_key_bytes], + } + .concat(); + + let shared_secret = our_priv_key.diffie_hellman(their_pub_key); + let mut derived_values = [0; EPHEMERAL_KEYS_KDF_LEN]; + // Generate new keys using the salt and shared secret + hkdf::Hkdf::::new(Some(&ephemeral_salt), shared_secret.as_bytes()) + .expand(&[], &mut derived_values) + .expect("valid output length"); + + Self { + ephemeral_public_key: *our_pub_key_bytes, + // Slice the new key up into a chain key, cipher key, and mac key + chain_key: *array_ref![&derived_values, 0, 32], + cipher_key: *array_ref![&derived_values, 32, 32], + mac_key: *array_ref![&derived_values, 64, 32], + } + } +} + +pub(super) struct StaticKeys { + pub(super) cipher_key: [u8; 32], + pub(super) mac_key: [u8; 32], +} + +impl StaticKeys { + pub(super) fn build( + our_priv_key: &StaticSecret, + their_key: &PublicKey, + chain_key: &[u8; 32], + ctext: &[u8], + ) -> Self { + let salt = [chain_key, ctext].concat(); + + let shared_secret = our_priv_key.diffie_hellman(their_key); + + let mut derived_values = [0; 96]; + hkdf::Hkdf::::new(Some(&salt), shared_secret.as_bytes()) + .expand(&[], &mut derived_values) + .expect("valid output length"); + + Self { + cipher_key: *array_ref![&derived_values, 32, 32], + mac_key: *array_ref![&derived_values, 64, 32], + } + } +} + +pub fn sealed_sender_encrypt( + our_pub_key: &PublicKey, + our_private_key: &StaticSecret, + recipient_pub_key: &PublicKey, + message: &[u8], +) -> Result { + let (ephem_priv_key, ephem_pub_key) = generate_keypair(); + // Generate the ephemeral chain key, cipher key, and mac key from the randomly generated keypair + let e_keys = EphemeralKeys::build(&ephem_pub_key, &ephem_priv_key, recipient_pub_key, true); + let static_key_ciphertext = aes256_ctr_hmac_sha256_encrypt( + our_pub_key.as_bytes(), + &e_keys.cipher_key, + &e_keys.mac_key, + )?; + + // These are the keys used for message encryption + let static_keys = StaticKeys::build( + our_private_key, + recipient_pub_key, + &e_keys.chain_key, + static_key_ciphertext.as_slice(), + ); + + // Actually encrypt the message + let encrypted_message = + aes256_ctr_hmac_sha256_encrypt(message, &static_keys.cipher_key, &static_keys.mac_key)?; + + Ok(SealedSenderMessage::new( + e_keys.ephemeral_public_key.to_vec(), + static_key_ciphertext, + encrypted_message, + )) +} + +pub fn sealed_sender_decrypt( + our_pub_key: &PublicKey, + our_priv_key: &StaticSecret, + message: &SealedSenderMessage, +) -> Result, DecryptionError> { + // Convert to [u8; 32] so it can be cast to a PublicKey + let ephem_pub_bytes: [u8; 32] = message.ephemeral_public_key.as_slice().try_into().unwrap(); + let ephem_keys = + EphemeralKeys::build(our_pub_key, our_priv_key, &ephem_pub_bytes.into(), false); + + // Decrypt the message key and coerce into [u8; 32] + let message_key_bytes: [u8; 32] = aes256_ctr_hmac_sha256_decrypt( + message.encrypted_static_key.as_slice(), + &ephem_keys.cipher_key, + &ephem_keys.mac_key, + )? + .as_slice() + .try_into() + .map_err(|_| DecryptionError::BadCiphertext("invalid input length".to_string()))?; + + let static_key: PublicKey = message_key_bytes.into(); + + // Now creat the Static Keys + let static_keys = StaticKeys::build( + our_priv_key, + &static_key, + &ephem_keys.chain_key, + &message.encrypted_static_key, + ); + + // Actually decrypt the message + let message_bytes = aes256_ctr_hmac_sha256_decrypt( + &message.encrypted_message, + &static_keys.cipher_key, + &static_keys.mac_key, + )?; + + Ok(message_bytes) +} + +fn generate_keypair() -> (StaticSecret, PublicKey) { + let rng = thread_rng(); + let ephemeral_private_key = StaticSecret::random_from_rng(rng); + let ephemeral_public_key = PublicKey::from(&ephemeral_private_key); + + (ephemeral_private_key, ephemeral_public_key) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt() { + let (my_priv_key, my_pub_key) = generate_keypair(); + let (_, recipient_pub_key) = generate_keypair(); + let message = b"hello world"; + + let encrypted = + sealed_sender_encrypt(&my_pub_key, &my_priv_key, &recipient_pub_key, message) + .expect("encryption failed"); + + assert!(encrypted.ephemeral_public_key.len() == 32); + } + + #[test] + fn test_round_trip() { + let (sender_priv_key, sender_pub_key) = generate_keypair(); + let (recipient_priv_key, recipient_pub_key) = generate_keypair(); + let message = b"hello world"; + + let encrypted = sealed_sender_encrypt( + &sender_pub_key, + &sender_priv_key, + &recipient_pub_key, + message, + ) + .expect("encryption failed"); + + assert!(encrypted.ephemeral_public_key.len() == 32); + + let decrypted = + sealed_sender_decrypt(&recipient_pub_key, &recipient_priv_key, &encrypted).unwrap(); + + assert_eq!(decrypted, message); + } +}