diff --git a/app/src/service-worker/schema.d.ts b/app/src/service-worker/schema.d.ts index 0a21e5b..7d7fd78 100644 --- a/app/src/service-worker/schema.d.ts +++ b/app/src/service-worker/schema.d.ts @@ -9,15 +9,19 @@ export type User = { */ name?: string; }; - +export type Message = { + sentAt: Date; + receivedAt: Date; + text: string; +}; export type Group = { id: string; /** * The users appearance in the group. The user can chose a different appearance per group. */ - user: User; + user?: User; friend: Friend; - messages: string[]; + messages: Message[]; }; interface Schema extends DBSchema { diff --git a/app/src/service-worker/serviceWorker.ts b/app/src/service-worker/serviceWorker.ts index 4ff4984..91bdd41 100644 --- a/app/src/service-worker/serviceWorker.ts +++ b/app/src/service-worker/serviceWorker.ts @@ -2,7 +2,7 @@ import { precacheAndRoute } from "workbox-precaching"; import { openDB } from "idb"; import init, { Client, DecodedPackage, Friend } from "meal-core"; import { expose } from "../crackle"; -import { Group, Schema } from "./schema"; +import { Group, Message, Schema } from "./schema"; /** * @import { Schema } from "./schema" @@ -148,6 +148,27 @@ function assertNotShared( throw new Error("Uint8Array uses a SharedArrayBuffer"); } +async function getGroup(groupId: string): Promise { + const database = await openDatabase; + const transaction = database.transaction("groups", "readonly"); + const store = transaction.objectStore("groups"); + return await store.get(groupId); +} + +async function updateGroup(group: Group) { + const database = await openDatabase; + const transaction = database.transaction("groups", "readwrite"); + const store = transaction.objectStore("groups"); + store.put(group); +} + +async function insertGroup(group: Group) { + const database = await openDatabase; + const transaction = database.transaction("groups", "readwrite"); + const store = transaction.objectStore("groups"); + store.add(group); +} + const handler = { async getIsOnboarded() { console.debug("[Service worker] Getting isOnboarded"); @@ -216,11 +237,7 @@ const handler = { messages: [], }; - const database = await openDatabase; - const transaction = database.transaction("groups", "readwrite"); - const store = transaction.objectStore("groups"); - store.add(group); - + await insertGroup(group); return group; }, @@ -265,6 +282,42 @@ self.addEventListener("activate", () => { async function receiveMessage(event: MessageEvent) { const client = await getClient; + const message = client.process_message(new Uint8Array(event.data)); + void updateClient(client); + switch (message.type) { + case "Welcome": { + console.debug("Processed welcome", message); + const configuration = await getConfiguration(); + const group: Group = { + id: message.group_id, + friend: message.friend, + messages: [], + // TODO load user identity that theuser chose to be associated with the key package + user: configuration.defaultUser, + }; + await insertGroup(group); + return; + } + case "Private": + { + console.debug("Processed private message", message); + const messageEntry: Message = { + receivedAt: new Date(), + sentAt: new Date(message.content.sent), + text: message.content.text, + }; + + const group = await getGroup(message.group_id); + if (group === undefined) + //TODO are we able to reconstruct the group? + throw new Error("Got message for group that does not exist"); + + // Assume it is sorted by time + group.messages.push(messageEntry); + await updateGroup(group); + } + return; + } } async function setupWebsocket() { diff --git a/core/src/lib.rs b/core/src/lib.rs index 9c1f67a..b307dcd 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -55,16 +55,17 @@ pub struct Friend { } #[derive(serde::Serialize, serde::Deserialize)] -struct MessageContent { +pub struct MessageContent { /// The time the sender said they supposedly sent the message #[serde(with = "time::serde::iso8601")] sent: OffsetDateTime, text: String, } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, Tsify)] +#[tsify(into_wasm_abi)] #[serde(tag = "type")] -enum Message { +pub enum Message { Private { group_id: String, content: MessageContent, diff --git a/core/src/v2/serializable.rs b/core/src/v2/serializable.rs index f397b26..0ecf631 100644 --- a/core/src/v2/serializable.rs +++ b/core/src/v2/serializable.rs @@ -5,19 +5,20 @@ //! Running in the service worker allows sending MLS messages through push notifications for example and avoids state management issues //! with multiple tabs effectively being multiple simultaneous clients that use the same storage. -use std::{collections::HashSet, rc::Rc}; +use std::{collections::HashSet, rc::Rc, vec::IntoIter}; use base64::prelude::*; use nanoid::nanoid; use openmls::prelude::*; use openmls_basic_credential::SignatureKeyPair; +use openmls_rust_crypto::MemoryStorageError; use serde::{Deserialize, Serialize}; use tls_codec::Serialize as _; use wasm_bindgen::{JsError, prelude::wasm_bindgen}; use crate::{ - ApplicationMessage, CIPHERSUITE, DecodedPackage, Friend, ID_LENGTH, encode_application_id, - v2::provider::Provider, + ApplicationMessage, CIPHERSUITE, DecodedPackage, Friend, ID_LENGTH, Message, + encode_application_id, v2::provider::Provider, }; #[derive(Serialize, Deserialize)] @@ -37,6 +38,36 @@ pub enum CreateGroupError { #[error("Group not found")] pub struct GroupNotFound; +#[derive(Debug, thiserror::Error)] +pub enum ProcessPrivateMessageError { + #[error("Group not found")] + GroupNotFound, + #[error("Error loading group: {0}")] + LoadGroup(#[from] MemoryStorageError), + #[error("Error processing message with MLS group: {0}")] + ProcessMessage(#[from] ProcessMessageError), + #[error("Unexpected message content type: {0:?}")] + UnexpectedMessageContent(ProcessedMessageContent), + #[error("Error deserializing message content: {0}")] + DeserializeMessageContent(#[from] postcard::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum ProcessWelcomeMessageError { + #[error("Unexpected message count in payload. Expected {expected}, got {actual}")] + UnexpectedMessageCount { expected: usize, actual: usize }, + #[error("Error processing welcome message: {0:?}")] + ProcessWelcome(#[from] WelcomeError), + #[error("Unexpected message body type for introduction message: {0:?}")] + UnexpectedIntroductionBody(MlsMessageBodyIn), + #[error("Error processing message with group")] + GroupProcess(#[from] ProcessMessageError), + #[error("Unexpected message content type: {0:?}")] + UnexpectedContent(ProcessedMessageContent), + #[error("Error deserializing message content: {0}")] + DeserializeMessageContent(#[from] postcard::Error), +} + #[derive(Serialize, Deserialize)] #[wasm_bindgen] pub struct Client { @@ -223,4 +254,102 @@ impl Client { vector.push(message); Ok(TlsSliceU8(&vector).tls_serialize_detached()?) } + + fn process_private_message( + &mut self, + message: PrivateMessageIn, + ) -> Result { + let message = ProtocolMessage::from(message); + let mut group = MlsGroup::load(self.provider.storage(), message.group_id())? + .ok_or_else(|| ProcessPrivateMessageError::GroupNotFound)?; + + let message = group.process_message(&self.provider, message)?; + let content = match message.into_content() { + ProcessedMessageContent::ApplicationMessage(content) => content, + other => return Err(ProcessPrivateMessageError::UnexpectedMessageContent(other)), + }; + + let content = postcard::from_bytes(&content.into_bytes())?; + let js_group_id = BASE64_URL_SAFE_NO_PAD.encode(group.group_id().as_slice()); + + Ok(Message::Private { + group_id: js_group_id, + content, + }) + } + + fn process_welcome( + &mut self, + welcome: Welcome, + mut rest: IntoIter, + ) -> Result { + // Step 1: Process welcome + let configuration = MlsGroupJoinConfig::builder() + .use_ratchet_tree_extension(true) + .build(); + + let welcome: StagedWelcome = + StagedWelcome::new_from_welcome(&self.provider, &configuration, welcome, None)?; + + let mut group = welcome.into_group(&self.provider)?; + + // Step 2: Process introduction + let introduction = + rest.next() + .ok_or_else(|| ProcessWelcomeMessageError::UnexpectedMessageCount { + expected: 2, + actual: 1, + })?; + + let introduction = match introduction.extract() { + MlsMessageBodyIn::PrivateMessage(introduction) => introduction, + other => { + return Err(ProcessWelcomeMessageError::UnexpectedIntroductionBody( + other, + )); + } + }; + + // Extract introduction application message with new group now + let introduction = group.process_message(&self.provider, introduction)?; + let content = match introduction.into_content() { + ProcessedMessageContent::ApplicationMessage(content) => content, + other => { + return Err(ProcessWelcomeMessageError::UnexpectedContent(other)); + } + }; + + let ApplicationMessage::Introduction { + id, + user_name: name, + } = postcard::from_bytes(&content.into_bytes())?; + + let js_group_id = BASE64_URL_SAFE_NO_PAD.encode(group.group_id().as_slice()); + self.groups.insert(group.group_id().clone()); + + Ok(Message::Welcome { + friend: Friend { id, name }, + group_id: js_group_id, + }) + } + + pub fn process_message(&mut self, data: &[u8]) -> Result { + let mut messages = TlsVecU8::::tls_deserialize_exact_bytes(data)? + .into_vec() + .into_iter(); + + let message = messages + .next() + .ok_or_else(|| JsError::new("Payload deserialized but there was no message"))?; + + let value = match message.extract() { + MlsMessageBodyIn::PrivateMessage(message) => self.process_private_message(message)?, + MlsMessageBodyIn::Welcome(welcome) => self.process_welcome(welcome, messages)?, + MlsMessageBodyIn::PublicMessage(_public_message_in) => todo!("Public message in"), + MlsMessageBodyIn::GroupInfo(_verifiable_group_info) => todo!("Group info in"), + MlsMessageBodyIn::KeyPackage(_key_package_in) => todo!("key package in"), + }; + + Ok(value) + } }