PrivateV1 Convo Ids (#54)

* Add conversation_ids for privateV1

* Skip handling of unknown payloads

* Tag initial ContentData as new

* Add Integration test

* truncate convo_id to size

* Clippy fixes

* cleanup

* Apply suggestion from @osmaczko

Co-authored-by: osmaczko <33099791+osmaczko@users.noreply.github.com>

* Apply suggestion from @osmaczko

Co-authored-by: osmaczko <33099791+osmaczko@users.noreply.github.com>

* Linter fixes

---------

Co-authored-by: osmaczko <33099791+osmaczko@users.noreply.github.com>
This commit is contained in:
Jazz Turner-Baggs 2026-02-11 14:10:21 -08:00 committed by GitHub
parent 57fe656728
commit 3b69f946fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 125 additions and 12 deletions

View File

@ -79,7 +79,7 @@ impl Context {
match convo_id {
c if c == self.inbox.id() => self.dispatch_to_inbox(enc),
c if self.store.has(&c) => self.dispatch_to_convo(&c, enc),
_ => Err(ChatError::NoConvo(convo_id)),
_ => Ok(None),
}
}
@ -137,4 +137,54 @@ mod tests {
let convo = store.get_mut(&convo_id).ok_or(0);
convo.unwrap();
}
fn send_and_verify(
sender: &mut Context,
receiver: &mut Context,
convo_id: ConversationId,
content: &[u8],
) {
let payloads = sender.send_content(convo_id, content).unwrap();
let payload = payloads.first().unwrap();
let received = receiver
.handle_payload(&payload.data)
.unwrap()
.expect("expected content");
assert_eq!(content, received.data.as_slice());
assert!(!received.is_new_convo); // Check that `is_new_convo` is FALSE
}
#[test]
fn ctx_integration() {
let mut saro = Context::new();
let mut raya = Context::new();
// Raya creates intro bundle and sends to Saro
let bundle = raya.create_intro_bundle().unwrap();
let intro = Introduction::try_from(bundle.as_slice()).unwrap();
// Saro initiates conversation with Raya
let mut content = vec![10];
let (saro_convo_id, payloads) = saro.create_private_convo(&intro, &content);
// Raya receives initial message
let payload = payloads.first().unwrap();
let initial_content = raya
.handle_payload(&payload.data)
.unwrap()
.expect("expected initial content");
let raya_convo_id = initial_content.conversation_id;
assert_eq!(content, initial_content.data);
assert!(initial_content.is_new_convo);
// Exchange messages back and forth
for _ in 0..10 {
content.push(content.last().unwrap() + 1);
send_and_verify(&mut raya, &mut saro, &raya_convo_id, &content);
content.push(content.last().unwrap() + 1);
send_and_verify(&mut saro, &mut raya, &saro_convo_id, &content);
}
}
}

View File

@ -1,3 +1,7 @@
use blake2::{
Blake2b, Blake2bMac, Digest,
digest::{FixedOutput, consts::U18},
};
use chat_proto::logoschat::{
convos::private_v1::{PrivateV1Frame, private_v1_frame::FrameType},
encryption::{Doubleratchet, EncryptedPayload, encrypted_payload::Encryption},
@ -16,25 +20,79 @@ use crate::{
utils::timestamp_millis,
};
// Represents the potential participant roles in this Conversation
enum Role {
Initiator,
Responder,
}
impl Role {
const fn as_str(&self) -> &'static str {
match self {
Self::Initiator => "I",
Self::Responder => "R",
}
}
}
struct BaseConvoId([u8; 18]);
impl BaseConvoId {
fn new(key: &SecretKey) -> Self {
let base = Blake2bMac::<U18>::new_with_salt_and_personal(key.as_slice(), b"", b"L-PV1-CID")
.expect("fixed inputs should never fail");
Self(base.finalize_fixed().into())
}
fn id_for_participant(&self, role: Role) -> String {
let hash = Blake2b::<U18>::new()
.chain_update(self.0)
.chain_update(role.as_str())
.finalize();
hex::encode(hash)
}
}
pub struct PrivateV1Convo {
local_convo_id: String,
remote_convo_id: String,
dr_state: RatchetState,
}
impl PrivateV1Convo {
pub fn new_initiator(seed_key: SecretKey, remote: PublicKey) -> Self {
let base_convo_id = BaseConvoId::new(&seed_key);
let local_convo_id = base_convo_id.id_for_participant(Role::Initiator);
let remote_convo_id = base_convo_id.id_for_participant(Role::Responder);
// 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();
let dr_state = RatchetState::init_sender(shared_secret, remote);
Self {
dr_state: RatchetState::init_sender(shared_secret, remote),
local_convo_id,
remote_convo_id,
dr_state,
}
}
pub fn new_responder(seed_key: SecretKey, dh_self: InstallationKeyPair) -> Self {
pub fn new_responder(
seed_key: SecretKey,
dh_self: InstallationKeyPair, // TODO: (P3) Rename; This accepts a Ephemeral key in most cases
) -> Self {
let base_convo_id = BaseConvoId::new(&seed_key);
let local_convo_id = base_convo_id.id_for_participant(Role::Responder);
let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator);
// TODO: Danger - Fix double-ratchets types to Accept SecretKey
let dr_state = RatchetState::init_receiver(seed_key.as_bytes().to_owned(), dh_self);
Self {
// TODO: Danger - Fix double-ratchets types to Accept SecretKey
dr_state: RatchetState::init_receiver(seed_key.as_bytes().to_owned(), dh_self),
local_convo_id,
remote_convo_id,
dr_state,
}
}
@ -102,8 +160,7 @@ impl PrivateV1Convo {
impl Id for PrivateV1Convo {
fn id(&self) -> ConversationId<'_> {
// TODO: implementation
"private_v1_convo_id"
&self.local_convo_id
}
}
@ -150,8 +207,7 @@ impl Convo for PrivateV1Convo {
}
fn remote_id(&self) -> String {
//TODO: Implement as per spec
self.id().into()
self.remote_convo_id.clone()
}
}

View File

@ -137,12 +137,19 @@ impl Inbox {
PrivateV1Convo::new_responder(seed_key, ephemeral_key.clone().into());
let Some(enc_payload) = _invite_private_v1.initial_message else {
return Err(ChatError::Protocol("Invite: missing initial".into()));
return Err(ChatError::Protocol("missing initial encpayload".into()));
};
let content = convo.handle_frame(enc_payload)?;
// Set is_new_convo for content data
let content = match convo.handle_frame(enc_payload)? {
Some(v) => ContentData {
is_new_convo: true,
..v
},
None => return Err(ChatError::Protocol("expected contentData".into())),
};
Ok((Box::new(convo), content))
Ok((Box::new(convo), Some(content)))
}
}
}