Integrate DR into PrivateV1 (#32)

* Add crate

* Initialize session

* Add encrypt / decrypt
This commit is contained in:
Jazz Turner-Baggs 2026-02-04 06:17:45 +07:00 committed by GitHub
parent 74695877fa
commit d5c16f51de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 166 additions and 30 deletions

1
Cargo.lock generated
View File

@ -506,6 +506,7 @@ dependencies = [
"blake2",
"chat-proto",
"crypto",
"double-ratchets",
"hex",
"prost",
"rand_core",

View File

@ -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" }

View File

@ -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<PrivateV1Frame, EncryptionError> {
// 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
);
}
}

View File

@ -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),
}

View File

@ -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",
)

View File

@ -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![]))
}

View File

@ -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;

View File

@ -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 {

View File

@ -37,3 +37,13 @@ impl InstallationKeyPair {
Self { secret, public }
}
}
impl From<StaticSecret> for InstallationKeyPair {
fn from(value: StaticSecret) -> Self {
let public = PublicKey::from(&value);
Self {
secret: value,
public,
}
}
}