diff --git a/Cargo.lock b/Cargo.lock index 1f43b1b..5b316e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,6 +506,7 @@ dependencies = [ "blake2", "chat-proto", "crypto", + "double-ratchets", "hex", "prost", "rand_core", diff --git a/conversations/Cargo.toml b/conversations/Cargo.toml index 0fd90ed..2355137 100644 --- a/conversations/Cargo.toml +++ b/conversations/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["staticlib","dylib"] blake2.workspace = true chat-proto = { git = "https://github.com/logos-messaging/chat_proto" } crypto = { path = "../crypto" } +double-ratchets = { path = "../double-ratchets" } hex = "0.4.3" prost = "0.14.1" rand_core = { version = "0.6" } diff --git a/conversations/src/conversation/privatev1.rs b/conversations/src/conversation/privatev1.rs index c40c18e..704918b 100644 --- a/conversations/src/conversation/privatev1.rs +++ b/conversations/src/conversation/privatev1.rs @@ -3,35 +3,92 @@ use chat_proto::logoschat::{ encryption::{Doubleratchet, EncryptedPayload, encrypted_payload::Encryption}, }; use crypto::SecretKey; +use double_ratchets::{Header, InstallationKeyPair, RatchetState}; use prost::{Message, bytes::Bytes}; +use std::fmt::Debug; +use x25519_dalek::PublicKey; use crate::{ conversation::{ChatError, ConversationId, Convo, Id}, + errors::EncryptionError, + proto, types::AddressedEncryptedPayload, utils::timestamp_millis, }; -#[derive(Debug)] -pub struct PrivateV1Convo {} +pub struct PrivateV1Convo { + dr_state: RatchetState, +} impl PrivateV1Convo { - pub fn new(_seed_key: SecretKey) -> Self { - Self {} + pub fn new_initiator(seed_key: SecretKey, remote: PublicKey) -> Self { + // TODO: Danger - Fix double-ratchets types to Accept SecretKey + // perhaps update the DH to work with cryptocrate. + // init_sender doesn't take ownership of the key so a reference can be used. + let shared_secret: [u8; 32] = seed_key.as_bytes().to_vec().try_into().unwrap(); + Self { + dr_state: RatchetState::init_sender(shared_secret, remote), + } } - fn encrypt(&self, frame: PrivateV1Frame) -> EncryptedPayload { - // TODO: Integrate DR + pub fn new_responder(seed_key: SecretKey, dh_self: InstallationKeyPair) -> Self { + Self { + // TODO: Danger - Fix double-ratchets types to Accept SecretKey + dr_state: RatchetState::init_receiver(seed_key.as_bytes().to_owned(), dh_self), + } + } + + fn encrypt(&mut self, frame: PrivateV1Frame) -> EncryptedPayload { + let encoded_bytes = frame.encode_to_vec(); + let (cipher_text, header) = self.dr_state.encrypt_message(&encoded_bytes); EncryptedPayload { encryption: Some(Encryption::Doubleratchet(Doubleratchet { - dh: Bytes::from(vec![]), - msg_num: 0, - prev_chain_len: 1, - ciphertext: Bytes::from(frame.encode_to_vec()), + dh: Bytes::from(Vec::from(header.dh_pub.to_bytes())), + msg_num: header.msg_num, + prev_chain_len: header.prev_chain_len, + ciphertext: Bytes::from(cipher_text), aux: "".into(), })), } } + + fn decrypt(&mut self, payload: EncryptedPayload) -> Result { + // Validate and extract the encryption header or return errors + let dr_header = if let Some(enc) = payload.encryption { + if let proto::Encryption::Doubleratchet(dr) = enc { + dr + } else { + return Err(EncryptionError::Decryption( + "incorrect encryption type".into(), + )); + } + } else { + return Err(EncryptionError::Decryption("missing payload".into())); + }; + + // Turn the bytes into a PublicKey + let byte_arr: [u8; 32] = dr_header + .dh + .to_vec() + .try_into() + .map_err(|_| EncryptionError::Decryption("invalid public key length".into()))?; + let dh_pub = PublicKey::from(byte_arr); + + // Build the Header that DR impl expects + let header = Header { + dh_pub, + msg_num: dr_header.msg_num, + prev_chain_len: dr_header.prev_chain_len, + }; + + // Decrypt into Frame + let content_bytes = self + .dr_state + .decrypt_message(&dr_header.ciphertext, header) + .map_err(|e| EncryptionError::Decryption(e.to_string()))?; + Ok(PrivateV1Frame::decode(content_bytes.as_slice()).unwrap()) + } } impl Id for PrivateV1Convo { @@ -66,3 +123,53 @@ impl Convo for PrivateV1Convo { self.id().into() } } + +impl Debug for PrivateV1Convo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PrivateV1Convo") + .field("dr_state", &"******") + .finish() + } +} + +#[cfg(test)] +mod tests { + use x25519_dalek::StaticSecret; + + use super::*; + + #[test] + fn test_encrypt_roundtrip() { + let saro = StaticSecret::random(); + let raya = StaticSecret::random(); + + let pub_raya = PublicKey::from(&raya); + + let seed_key = saro.diffie_hellman(&pub_raya); + let send_content_bytes = vec![0, 2, 4, 6, 8]; + let mut sr_convo = + PrivateV1Convo::new_initiator(SecretKey::from(seed_key.to_bytes()), pub_raya); + + let installation_key_pair = InstallationKeyPair::from(raya); + let mut rs_convo = PrivateV1Convo::new_responder( + SecretKey::from(seed_key.to_bytes()), + installation_key_pair, + ); + + let send_frame = PrivateV1Frame { + conversation_id: "_".into(), + sender: Bytes::new(), + timestamp: timestamp_millis(), + frame_type: Some(FrameType::Content(Bytes::from(send_content_bytes.clone()))), + }; + let payload = sr_convo.encrypt(send_frame.clone()); + let recv_frame = rs_convo.decrypt(payload).unwrap(); + + assert!( + recv_frame == send_frame, + "{:?}. {:?}", + recv_frame, + send_content_bytes + ); + } +} diff --git a/conversations/src/errors.rs b/conversations/src/errors.rs index 19bfbe2..06cd563 100644 --- a/conversations/src/errors.rs +++ b/conversations/src/errors.rs @@ -21,3 +21,11 @@ pub enum ChatError { #[error("convo with handle: {0} was not found")] NoConvo(u32), } + +#[derive(Error, Debug)] +pub enum EncryptionError { + #[error("encryption: {0}")] + Encryption(String), + #[error("decryption: {0}")] + Decryption(String), +} diff --git a/conversations/src/inbox/handshake.rs b/conversations/src/inbox/handshake.rs index bbcb088..e0576f4 100644 --- a/conversations/src/inbox/handshake.rs +++ b/conversations/src/inbox/handshake.rs @@ -63,7 +63,7 @@ impl InboxHandshake { /// Derive keys from X3DH shared secret fn derive_keys_from_shared_secret(shared_secret: SecretKey) -> SecretKey { let seed_key: [u8; 32] = Blake2bMac256::new_with_salt_and_personal( - shared_secret.as_bytes(), + shared_secret.as_slice(), &[], // No salt - input already has high entropy b"InboxV1-Seed", ) diff --git a/conversations/src/inbox/inbox.rs b/conversations/src/inbox/inbox.rs index f117a3c..c19fd39 100644 --- a/conversations/src/inbox/inbox.rs +++ b/conversations/src/inbox/inbox.rs @@ -1,3 +1,4 @@ +use double_ratchets::InstallationKeyPair; use hex; use prost::Message; use prost::bytes::Bytes; @@ -88,7 +89,7 @@ impl Inbox { let (seed_key, ephemeral_pub) = InboxHandshake::perform_as_initiator(&self.ident.secret(), &pkb, &mut rng); - let mut convo = PrivateV1Convo::new(seed_key); + let mut convo = PrivateV1Convo::new_initiator(seed_key, remote_bundle.ephemeral_key); let mut payloads = convo.send_message(initial_message.as_bytes())?; @@ -139,16 +140,11 @@ impl Inbox { fn perform_handshake( &self, - payload: proto::EncryptedPayload, + ephemeral_key: &StaticSecret, + header: proto::InboxHeaderV1, + bytes: Bytes, ) -> Result<(SecretKey, proto::InboxV1Frame), ChatError> { - let handshake = Self::extract_payload(payload)?; - let header = handshake - .header - .ok_or(ChatError::UnexpectedPayload("InboxV1Header".into()))?; - let pubkey_hex = hex::encode(header.responder_ephemeral.as_ref()); - - let ephemeral_key = self.lookup_ephemeral_key(&pubkey_hex)?; - + // Get PublicKeys from protobuf let initator_static = PublicKey::from( <[u8; 32]>::try_from(header.initiator_static.as_ref()) .map_err(|_| ChatError::BadBundleValue("wrong size - initator static".into()))?, @@ -168,7 +164,7 @@ impl Inbox { ); // TODO: Decrypt Content - let frame = proto::InboxV1Frame::decode(handshake.payload)?; + let frame = proto::InboxV1Frame::decode(bytes)?; Ok((seed_key, frame)) } @@ -215,14 +211,23 @@ impl ConvoFactory for Inbox { return Err(ChatError::Protocol("Example error".into())); } - let ep = proto::EncryptedPayload::decode(message)?; - let (seed_key, frame) = self.perform_handshake(ep)?; + let handshake = Self::extract_payload(proto::EncryptedPayload::decode(message)?)?; + + let header = handshake + .header + .ok_or(ChatError::UnexpectedPayload("InboxV1Header".into()))?; + + // Get Ephemeral key used by the initator + let key_index = hex::encode(header.responder_ephemeral.as_ref()); + let ephemeral_key = self.lookup_ephemeral_key(&key_index)?; + + // Perform handshake and decrypt frame + let (seed_key, frame) = self.perform_handshake(ephemeral_key, header, handshake.payload)?; match frame.frame_type.unwrap() { - chat_proto::logoschat::inbox::inbox_v1_frame::FrameType::InvitePrivateV1( - _invite_private_v1, - ) => { - let convo = PrivateV1Convo::new(seed_key); + proto::inbox_v1_frame::FrameType::InvitePrivateV1(_invite_private_v1) => { + let convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key.clone().into()); + // TODO: Update PrivateV1 Constructor with DR, initial_message Ok((Box::new(convo), vec![])) } diff --git a/conversations/src/proto.rs b/conversations/src/proto.rs index 1697de5..18f3119 100644 --- a/conversations/src/proto.rs +++ b/conversations/src/proto.rs @@ -2,7 +2,7 @@ pub use chat_proto::logoschat::encryption::encrypted_payload::Encryption; pub use chat_proto::logoschat::encryption::inbox_handshake_v1::InboxHeaderV1; pub use chat_proto::logoschat::encryption::{EncryptedPayload, InboxHandshakeV1}; pub use chat_proto::logoschat::envelope::EnvelopeV1; -pub use chat_proto::logoschat::inbox::InboxV1Frame; +pub use chat_proto::logoschat::inbox::{InboxV1Frame, inbox_v1_frame}; pub use chat_proto::logoschat::invite::InvitePrivateV1; pub use prost::Message; diff --git a/crypto/src/keys.rs b/crypto/src/keys.rs index 1b78ea7..4eeccb1 100644 --- a/crypto/src/keys.rs +++ b/crypto/src/keys.rs @@ -7,9 +7,13 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; pub struct SecretKey([u8; 32]); impl SecretKey { - pub fn as_bytes(&self) -> &[u8] { + pub fn as_slice(&self) -> &[u8] { self.0.as_slice() } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } } impl From<[u8; 32]> for SecretKey { diff --git a/double-ratchets/src/keypair.rs b/double-ratchets/src/keypair.rs index 7943646..88852c2 100644 --- a/double-ratchets/src/keypair.rs +++ b/double-ratchets/src/keypair.rs @@ -37,3 +37,13 @@ impl InstallationKeyPair { Self { secret, public } } } + +impl From for InstallationKeyPair { + fn from(value: StaticSecret) -> Self { + let public = PublicKey::from(&value); + Self { + secret: value, + public, + } + } +}