mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-03-26 22:23:14 +00:00
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:
parent
57fe656728
commit
3b69f946fd
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user