From 5ccd96e47bc72e3cc6e0af0aada1d22b91daf911 Mon Sep 17 00:00:00 2001 From: Cassandra Heart Date: Mon, 30 Dec 2024 19:15:35 -0600 Subject: [PATCH] more tweaks --- Cargo.lock | 64 ++++- crates/channel-wasm/Cargo.toml | 7 + crates/channel-wasm/src/lib.rs | 420 +++++++++++++++++++++++++++++++++ crates/channel/src/lib.rs | 88 ++++++- 4 files changed, 573 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 583f3d1..faaf19b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,12 +231,22 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "block-padding", + "block-padding 0.1.5", "byte-tools", "byteorder", "generic-array 0.12.4", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding 0.2.1", + "generic-array 0.14.7", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -255,6 +265,12 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "bls48581" version = "0.1.0" @@ -358,10 +374,17 @@ dependencies = [ name = "channelwasm" version = "0.1.0" dependencies = [ + "aes-gcm", + "base64", "channel", + "ed448-goldilocks-plus", + "ed448-rust", "getrandom", + "hkdf", + "rand", "serde", "serde_json", + "sha2 0.10.8", "wasm-bindgen", ] @@ -664,6 +687,15 @@ dependencies = [ "generic-array 0.12.4", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "digest" version = "0.10.7" @@ -685,11 +717,27 @@ dependencies = [ "hex 0.4.3", "rand_core", "serde", - "sha3", + "sha3 0.10.8", "subtle", "zeroize", ] +[[package]] +name = "ed448-rust" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428b9b8138c483798ceb6a21004de0f24e3a2311195f13829ce1d059037d703" +dependencies = [ + "lazy_static", + "num-bigint", + "num-integer", + "num-traits", + "opaque-debug 0.3.1", + "rand_core", + "sha3 0.9.1", + "subtle", +] + [[package]] name = "either" version = "1.12.0" @@ -1464,6 +1512,18 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug 0.3.1", +] + [[package]] name = "sha3" version = "0.10.8" diff --git a/crates/channel-wasm/Cargo.toml b/crates/channel-wasm/Cargo.toml index 14fed50..5e65432 100644 --- a/crates/channel-wasm/Cargo.toml +++ b/crates/channel-wasm/Cargo.toml @@ -13,3 +13,10 @@ channel = { path = "../channel", version = "^0.1.0" } getrandom = { version = "0.2", features = ["js"] } serde = "1.0.208" serde_json = "1.0.117" +base64 = "0.22.1" +ed448-goldilocks-plus = "0.11.2" +ed448-rust = "0.1.1" +rand = "0.8.5" +sha2 = "0.10.8" +hkdf = "0.12.4" +aes-gcm = "0.10.3" \ No newline at end of file diff --git a/crates/channel-wasm/src/lib.rs b/crates/channel-wasm/src/lib.rs index b705f19..7ede250 100644 --- a/crates/channel-wasm/src/lib.rs +++ b/crates/channel-wasm/src/lib.rs @@ -1,6 +1,14 @@ +use aes_gcm::{Aes256Gcm, Nonce}; +use aes_gcm::aead::{Aead, Payload}; +use ed448_rust::Ed448Error; +use hkdf::Hkdf; +use rand::{rngs::OsRng, RngCore}; +use sha2::Sha512; use wasm_bindgen::prelude::*; +use base64::prelude::*; use channel::*; use serde::{Deserialize, Serialize}; +use ed448_goldilocks_plus::{elliptic_curve::Group, CompressedEdwardsY, EdwardsPoint, Scalar}; #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct NewDoubleRatchetParameters { @@ -22,6 +30,418 @@ pub struct NewTripleRatchetParameters { pub async_dkg_ratchet: bool } +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct EncryptionKeyPair { + pub public_key: Vec, + pub private_key: Vec, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct SigningKeyPair { + pub public_key: Vec, + pub private_key: Vec, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct SenderX3DH { + pub sending_identity_private_key: Vec, + pub sending_ephemeral_private_key: Vec, + pub receiving_identity_key: Vec, + pub receiving_signed_pre_key: Vec, + pub session_key_length: usize, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct ReceiverX3DH { + pub sending_identity_private_key: Vec, + pub sending_signed_private_key: Vec, + pub receiving_identity_key: Vec, + pub receiving_ephemeral_key: Vec, + pub session_key_length: usize, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] +pub struct MessageCiphertext { + pub ciphertext: String, + pub initialization_vector: String, + pub associated_data: Option, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct SealedInboxMessageDecryptRequest { + pub inbox_private_key: Vec, + pub ephemeral_public_key: Vec, + pub ciphertext: MessageCiphertext, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct SealedInboxMessageEncryptRequest { + pub inbox_public_key: Vec, + pub ephemeral_private_key: Vec, + pub plaintext: Vec, +} + +fn encrypt(plaintext: &[u8], key: &[u8]) + -> Result> { + use aes_gcm::KeyInit; + let mut iv = [0u8; 12]; + OsRng.fill_bytes(&mut iv); + + let cipher = Aes256Gcm::new_from_slice(key).unwrap(); + let nonce = Nonce::from_slice(&iv); + + let mut aad = [0u8; 32]; + OsRng.fill_bytes(&mut aad); + + let ciphertext = cipher.encrypt(nonce, Payload{ + msg: plaintext, + aad: &aad, + }).map_err(|e| format!("Encryption failed: {}", e))?; + + Ok(MessageCiphertext { + ciphertext: BASE64_STANDARD.encode(ciphertext), + initialization_vector: BASE64_STANDARD.encode(iv.to_vec()), + associated_data: Some(BASE64_STANDARD.encode(aad.to_vec())), + }) +} + +fn decrypt(ciphertext: &MessageCiphertext, key: &[u8]) + -> Result, Box> { + use aes_gcm::KeyInit; + if key.len() != 32 { + return Err(format!("Invalid key length").into()); + } + let cipher = Aes256Gcm::new_from_slice(key).unwrap(); + let maybe_iv = BASE64_STANDARD.decode(&ciphertext.initialization_vector); + if maybe_iv.is_err() { + return Err(format!("Decryption failed: {}", maybe_iv.unwrap_err()).into()); + } + + let iv = maybe_iv.unwrap(); + let nonce = Nonce::from_slice(&iv); + + let ad = &ciphertext.associated_data; + let mut associated_data = Vec::::new(); + if ad.is_some() { + let aad = BASE64_STANDARD.decode(&ad.clone().unwrap()); + if aad.is_err() { + return Err(format!("Decryption failed: {}", aad.unwrap_err()).into()); + } + + associated_data = aad.unwrap(); + } + + let ciphertext = BASE64_STANDARD.decode(&ciphertext.ciphertext); + if ciphertext.is_err() { + return Err(format!("Decryption failed: {}", ciphertext.unwrap_err()).into()); + } + + cipher.decrypt(nonce, Payload{ + msg: &ciphertext.unwrap(), + aad: &associated_data, + }).map_err(|e| format!("Decryption failed: {}", e).into()) +} + +#[wasm_bindgen] +pub fn js_decrypt_inbox_message(input: &str) -> String { + let json: Result = serde_json::from_str(input); + match json { + Ok(params) => { + let ephemeral_key = params.ephemeral_public_key; + if ephemeral_key.len() != 57 { + return "invalid ephemeral key length".to_string(); + } + + let inbox_key = params.inbox_private_key; + if inbox_key.len() != 56 { + return "invalid inbox key length".to_string(); + } + + let ephemeral_key_bytes: [u8; 57] = ephemeral_key.try_into().unwrap(); + let inbox_key_bytes: [u8; 56] = inbox_key.try_into().unwrap(); + let priv_key = Scalar::from_bytes(&inbox_key_bytes); + let maybe_eph_key = CompressedEdwardsY(ephemeral_key_bytes).decompress(); + + if maybe_eph_key.is_none().into() { + return "invalid ephemeral key".to_string(); + } + + let dh_output = priv_key * maybe_eph_key.unwrap(); + // TODO: To support authorized inbox sending, change None to Some when an authorization_key is present. + let hkdf = Hkdf::::new(None, &dh_output.compress().to_bytes()); + let mut derived = [0u8; 32]; + let err = hkdf.expand(b"quilibrium-sealed-sender", &mut derived); + if err.is_err() { + return "invalid length".into(); + } + + let result = decrypt(¶ms.ciphertext, &derived); + if result.is_err() { + return result.unwrap_err().to_string(); + } + + match serde_json::to_string(&result.unwrap()) { + Ok(result) => result, + Err(e) => e.to_string(), + } + } + Err(e) => { + return e.to_string(); + } + } +} + +#[wasm_bindgen] +pub fn js_encrypt_inbox_message(input: &str) -> String { + let json: Result = serde_json::from_str(input); + match json { + Ok(params) => { + let key = params.ephemeral_private_key; + if key.len() != 56 { + return "invalid ephemeral key length".to_string(); + } + + let inbox_key = params.inbox_public_key; + if inbox_key.len() != 57 { + return "invalid inbox key length".to_string(); + } + + let key_bytes: [u8; 56] = key.try_into().unwrap(); + let inbox_key_bytes: [u8; 57] = inbox_key.try_into().unwrap(); + let priv_key = Scalar::from_bytes(&key_bytes); + let maybe_pub_key = CompressedEdwardsY(inbox_key_bytes).decompress(); + + if maybe_pub_key.is_none().into() { + return "invalid inbox key".to_string(); + } + + let dh_output = priv_key * maybe_pub_key.unwrap(); + // TODO: To support authorized inbox sending, change None to Some when an authorization_key is present. + let hkdf = Hkdf::::new(None, &dh_output.compress().to_bytes()); + let mut derived = [0u8; 32]; + let err = hkdf.expand(b"quilibrium-sealed-sender", &mut derived); + if err.is_err() { + return "invalid length".into(); + } + + let result = encrypt(¶ms.plaintext, &derived); + if result.is_err() { + return result.unwrap_err().to_string(); + } + + match serde_json::to_string(&result.unwrap()) { + Ok(result) => result, + Err(e) => e.to_string(), + } + } + Err(e) => { + return e.to_string(); + } + } +} + +#[wasm_bindgen] +pub fn js_sender_x3dh(input: &str) -> String { + let json: Result = serde_json::from_str(input); + match json { + Ok(params) => { + return sender_x3dh(¶ms.sending_identity_private_key, ¶ms.sending_ephemeral_private_key, ¶ms.receiving_identity_key, ¶ms.receiving_signed_pre_key, params.session_key_length); + } + Err(e) => { + return e.to_string(); + } + } +} + +#[wasm_bindgen] +pub fn js_receiver_x3dh(input: &str) -> String { + let json: Result = serde_json::from_str(input); + match json { + Ok(params) => { + return receiver_x3dh(¶ms.sending_identity_private_key, ¶ms.sending_signed_private_key, ¶ms.receiving_identity_key, ¶ms.receiving_ephemeral_key, params.session_key_length); + } + Err(e) => { + return e.to_string(); + } + } +} + +#[wasm_bindgen] +pub fn js_generate_x448() -> String { + let priv_key = Scalar::random(&mut rand::thread_rng()); + let pub_key = EdwardsPoint::generator() * priv_key; + let output = serde_json::to_string(&EncryptionKeyPair{ + public_key: pub_key.compress().to_bytes().to_vec(), + private_key: priv_key.to_bytes().to_vec(), + }); + + match output { + Ok(result) => { + result + } + Err(e) => { + e.to_string() + } + } +} + +#[wasm_bindgen] +pub fn js_generate_ed448() -> String { + let priv_key = ed448_rust::PrivateKey::new(&mut rand::thread_rng()); + let pub_key = ed448_rust::PublicKey::from(&priv_key); + let output = serde_json::to_string(&EncryptionKeyPair{ + public_key: pub_key.as_byte().to_vec(), + private_key: priv_key.as_bytes().to_vec(), + }); + + match output { + Ok(result) => { + result + } + Err(e) => { + e.to_string() + } + } +} + +#[wasm_bindgen] +pub fn js_get_pubkey_ed448(key: &str) -> String { + let maybe_key = BASE64_STANDARD.decode(key); + if maybe_key.is_err() { + return maybe_key.unwrap_err().to_string(); + } + + let key = maybe_key.unwrap(); + if key.len() != 57 { + return "invalid key length".to_string(); + } + + let key_bytes: [u8; 57] = key.try_into().unwrap(); + let priv_key = ed448_rust::PrivateKey::from(key_bytes); + let pub_key = ed448_rust::PublicKey::from(&priv_key); + + return format!("\"{}\"", BASE64_STANDARD.encode(pub_key.as_byte())); +} + +#[wasm_bindgen] +pub fn js_get_pubkey_x448(key: &str) -> String { + let maybe_key = BASE64_STANDARD.decode(key); + if maybe_key.is_err() { + return maybe_key.unwrap_err().to_string(); + } + + let key = maybe_key.unwrap(); + if key.len() != 56 { + return "invalid key length".to_string(); + } + + let mut priv_key_bytes = [0u8; 56]; + priv_key_bytes.copy_from_slice(&key); + + let priv_key = Scalar::from_bytes(&priv_key_bytes); + let pub_key = EdwardsPoint::generator() * priv_key; + + return format!("\"{}\"", BASE64_STANDARD.encode(pub_key.compress().to_bytes().to_vec())); +} + +#[wasm_bindgen] +pub fn js_sign_ed448(key: &str, message: &str) -> String { + let maybe_key = BASE64_STANDARD.decode(key); + if maybe_key.is_err() { + return maybe_key.unwrap_err().to_string(); + } + + let maybe_message = BASE64_STANDARD.decode(message); + if maybe_message.is_err() { + return maybe_message.unwrap_err().to_string(); + } + + let key = maybe_key.unwrap(); + if key.len() != 57 { + return "invalid key length".to_string(); + } + + let key_bytes: [u8; 57] = key.try_into().unwrap(); + let priv_key = ed448_rust::PrivateKey::from(key_bytes); + let signature = priv_key.sign(&maybe_message.unwrap(), None); + + match signature { + Ok(output) => { + return format!("\"{}\"", BASE64_STANDARD.encode(output)); + } + Err(Ed448Error::WrongKeyLength) => { + return "invalid key length".to_string(); + } + Err(Ed448Error::WrongPublicKeyLength) => { + return "invalid public key length".to_string(); + } + Err(Ed448Error::WrongSignatureLength) => { + return "invalid signature length".to_string(); + } + Err(Ed448Error::InvalidPoint) => { + return "invalid point".to_string(); + } + Err(Ed448Error::InvalidSignature) => { + return "invalid signature".to_string(); + } + Err(Ed448Error::ContextTooLong) => { + return "context too long".to_string(); + } + } +} + +#[wasm_bindgen] +pub fn js_verify_ed448(public_key: &str, message: &str, signature: &str) -> String { + let maybe_key = BASE64_STANDARD.decode(public_key); + if maybe_key.is_err() { + return maybe_key.unwrap_err().to_string(); + } + + let maybe_message = BASE64_STANDARD.decode(message); + if maybe_message.is_err() { + return maybe_message.unwrap_err().to_string(); + } + + let maybe_signature = BASE64_STANDARD.decode(signature); + if maybe_signature.is_err() { + return maybe_signature.unwrap_err().to_string(); + } + + let key = maybe_key.unwrap(); + if key.len() != 57 { + return "invalid key length".to_string(); + } + + let pub_bytes: [u8; 57] = key.try_into().unwrap(); + + let pub_key = ed448_rust::PublicKey::from(pub_bytes); + let signature = pub_key.verify(&maybe_message.unwrap(), &maybe_signature.unwrap(), None); + + match signature { + Ok(()) => { + return "true".to_string(); + } + Err(Ed448Error::WrongKeyLength) => { + return "invalid key length".to_string(); + } + Err(Ed448Error::WrongPublicKeyLength) => { + return "invalid public key length".to_string(); + } + Err(Ed448Error::WrongSignatureLength) => { + return "invalid signature length".to_string(); + } + Err(Ed448Error::InvalidPoint) => { + return "invalid point".to_string(); + } + Err(Ed448Error::InvalidSignature) => { + return "invalid signature".to_string(); + } + Err(Ed448Error::ContextTooLong) => { + return "context too long".to_string(); + } + } +} + #[wasm_bindgen] pub fn js_new_double_ratchet(params: &str) -> String { let json: Result = serde_json::from_str(params); diff --git a/crates/channel/src/lib.rs b/crates/channel/src/lib.rs index 682eb5c..59c7514 100644 --- a/crates/channel/src/lib.rs +++ b/crates/channel/src/lib.rs @@ -1,9 +1,9 @@ use base64::prelude::*; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{collections::HashMap, io::Read}; -use ed448_goldilocks_plus::{elliptic_curve::group::GroupEncoding, EdwardsPoint, Scalar}; -use protocols::{doubleratchet::{DoubleRatchetParticipant, P2PChannelEnvelope}, tripleratchet::{PeerInfo, TripleRatchetParticipant}}; +use ed448_goldilocks_plus::{elliptic_curve::group::GroupEncoding, CompressedEdwardsY, EdwardsPoint, Scalar}; +use protocols::{doubleratchet::{DoubleRatchetParticipant, P2PChannelEnvelope}, tripleratchet::{PeerInfo, TripleRatchetParticipant}, x3dh}; pub(crate) mod protocols; @@ -39,6 +39,86 @@ pub struct TripleRatchetStateAndMessage { pub message: Vec, } +pub fn sender_x3dh(sending_identity_private_key: &Vec, sending_ephemeral_private_key: &Vec, receiving_identity_key: &Vec, receiving_signed_pre_key: &Vec, session_key_length: usize) -> String { + if sending_identity_private_key.len() != 56 { + return "invalid sending identity private key length".to_string(); + } + + if sending_ephemeral_private_key.len() != 56 { + return "invalid sending ephemeral private key length".to_string(); + } + + if receiving_identity_key.len() != 57 { + return "invalid receiving identity public key length".to_string(); + } + + if receiving_signed_pre_key.len() != 57 { + return "invalid receiving signed public key length".to_string(); + } + + let sidk = Scalar::from_bytes(sending_identity_private_key.as_slice().try_into().unwrap()); + let sepk = Scalar::from_bytes(sending_ephemeral_private_key.as_slice().try_into().unwrap()); + let ridk = CompressedEdwardsY(receiving_identity_key.as_slice().try_into().unwrap()).decompress(); + let rspk = CompressedEdwardsY(receiving_signed_pre_key.as_slice().try_into().unwrap()).decompress(); + + if ridk.is_none().into() { + return "invalid receiving identity public key".to_string(); + } + + if rspk.is_none().into() { + return "invalid receiving signed public key".to_string(); + } + + match x3dh::sender_x3dh(&sidk, &sepk, &ridk.unwrap(), &rspk.unwrap(), session_key_length) { + Some(result) => { + return format!("\"{}\"", BASE64_STANDARD.encode(result)); + } + None => { + return "could not perform key agreement".to_string(); + } + } +} + +pub fn receiver_x3dh(sending_identity_private_key: &Vec, sending_signed_private_key: &Vec, receiving_identity_key: &Vec, receiving_ephemeral_key: &Vec, session_key_length: usize) -> String { + if sending_identity_private_key.len() != 56 { + return "invalid sending identity private key length".to_string(); + } + + if sending_signed_private_key.len() != 56 { + return "invalid sending signed private key length".to_string(); + } + + if receiving_identity_key.len() != 57 { + return "invalid receiving identity public key length".to_string(); + } + + if receiving_ephemeral_key.len() != 57 { + return "invalid receiving ephemeral public key length".to_string(); + } + + let sidk = Scalar::from_bytes(sending_identity_private_key.as_slice().try_into().unwrap()); + let sspk = Scalar::from_bytes(sending_signed_private_key.as_slice().try_into().unwrap()); + let ridk = CompressedEdwardsY(receiving_identity_key.as_slice().try_into().unwrap()).decompress(); + let repk = CompressedEdwardsY(receiving_ephemeral_key.as_slice().try_into().unwrap()).decompress(); + + if ridk.is_none().into() { + return "invalid receiving identity public key".to_string(); + } + + if repk.is_none().into() { + return "invalid receiving signed public key".to_string(); + } + + match x3dh::receiver_x3dh(&sidk, &sspk, &ridk.unwrap(), &repk.unwrap(), session_key_length) { + Some(result) => { + return format!("\"{}\"", BASE64_STANDARD.encode(result)); + } + None => { + return "could not perform key agreement".to_string(); + } + } +} + pub fn new_double_ratchet(session_key: &Vec, sending_header_key: &Vec, next_receiving_header_key: &Vec, is_sender: bool, sending_ephemeral_private_key: &Vec, receiving_ephemeral_key: &Vec) -> String { if sending_ephemeral_private_key.len() != 56 { return "invalid private key length".to_string(); @@ -64,7 +144,7 @@ pub fn new_double_ratchet(session_key: &Vec, sending_header_key: &Vec, n &session_key, &sending_header_key, &next_receiving_header_key, - true, + is_sender, sending_key, receiving_key.unwrap(), );