diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index e79a8c7..43b7c2d 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -11,6 +11,9 @@ use crate::{ inbox::Inbox, proto::{EncryptedPayload, EnvelopeV1, Message}, storage::ChatStorage, + store::{ + ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore, + }, types::{AddressedEnvelope, ContentData}, }; @@ -37,11 +40,11 @@ impl Context { let name = name.into(); // Load or create identity - let identity = if let Some(identity) = storage.load_identity()? { + let identity = if let Some(identity) = IdentityStore::load_identity(&storage)? { identity } else { let identity = Identity::new(&name); - storage.save_identity(&identity)?; + IdentityStore::save_identity(&mut storage, &identity)?; identity }; @@ -88,7 +91,7 @@ impl Context { } pub fn list_conversations(&self) -> Result, ChatError> { - let records = self.storage.load_conversations()?; + let records = ConversationStore::load_conversations(&self.storage)?; Ok(records .into_iter() .map(|r| Arc::from(r.local_convo_id.as_str())) @@ -121,7 +124,9 @@ impl Context { let enc = EncryptedPayload::decode(env.payload)?; match convo_id { c if c == self.inbox.id() => self.dispatch_to_inbox(enc), - c if self.storage.has_conversation(&c)? => self.dispatch_to_convo(&c, enc), + c if ConversationStore::has_conversation(&self.storage, &c)? => { + self.dispatch_to_convo(&c, enc) + } _ => Ok(None), } } @@ -133,15 +138,13 @@ impl Context { ) -> Result, ChatError> { // Look up the ephemeral key from storage let key_hex = Inbox::extract_ephemeral_key_hex(&enc_payload)?; - let ephemeral_key = self - .storage - .load_ephemeral_key(&key_hex)? + let ephemeral_key = EphemeralKeyStore::load_ephemeral_key(&self.storage, &key_hex)? .ok_or(ChatError::UnknownEphemeralKey())?; let (convo, content) = self.inbox.handle_frame(&ephemeral_key, enc_payload)?; // Remove consumed ephemeral key from storage - self.storage.remove_ephemeral_key(&key_hex)?; + EphemeralKeyStore::remove_ephemeral_key(&mut self.storage, &key_hex)?; self.persist_convo(convo.as_ref()); Ok(content) @@ -163,39 +166,38 @@ impl Context { pub fn create_intro_bundle(&mut self) -> Result, ChatError> { let (intro, public_key_hex, private_key) = self.inbox.create_intro_bundle(); - self.storage - .save_ephemeral_key(&public_key_hex, &private_key)?; + EphemeralKeyStore::save_ephemeral_key(&mut self.storage, &public_key_hex, &private_key)?; Ok(intro.into()) } /// Loads a conversation from DB by constructing it from metadata + ratchet state. fn load_convo(&self, convo_id: ConversationId) -> Result { - let record = self - .storage - .load_conversation(convo_id)? + let meta = ConversationStore::load_conversation(&self.storage, convo_id)? .ok_or_else(|| ChatError::NoConvo(convo_id.into()))?; - if record.convo_type != "private_v1" { - return Err(ChatError::BadBundleValue(format!( - "unsupported conversation type: {}", - record.convo_type - ))); + match meta.kind { + ConversationKind::PrivateV1 => { + let dr_state: RatchetState = self.ratchet_storage.load(&meta.local_convo_id)?; + + Ok(PrivateV1Convo::from_stored( + meta.local_convo_id, + meta.remote_convo_id, + dr_state, + )) + } + ConversationKind::Unknown(kind) => Err(ChatError::UnsupportedConvoType(kind)), } - - let dr_state: RatchetState = self.ratchet_storage.load(&record.local_convo_id)?; - - Ok(PrivateV1Convo::from_stored( - record.local_convo_id, - record.remote_convo_id, - dr_state, - )) } /// Persists a conversation's metadata and ratchet state to DB. fn persist_convo(&mut self, convo: &dyn Convo) -> ConversationIdOwned { - let _ = self - .storage - .save_conversation(convo.id(), &convo.remote_id(), convo.convo_type()); + let meta = ConversationMeta { + local_convo_id: convo.id().to_string(), + remote_convo_id: convo.remote_id(), + kind: ConversationKind::from_db(convo.convo_type()), + }; + + let _ = ConversationStore::save_conversation(&mut self.storage, &meta); let _ = convo.save_ratchet_state(&mut self.ratchet_storage); Arc::from(convo.id()) } @@ -328,13 +330,13 @@ mod tests { let content = alice.handle_payload(&payload.data).unwrap().unwrap(); assert!(content.is_new_convo); - let convos = alice.storage.load_conversations().unwrap(); + let convos = ConversationStore::load_conversations(&alice.storage).unwrap(); assert_eq!(convos.len(), 1); - assert_eq!(convos[0].convo_type, "private_v1"); + assert_eq!(convos[0].kind, ConversationKind::PrivateV1); drop(alice); let alice2 = Context::open("alice", config).unwrap(); - let convos = alice2.storage.load_conversations().unwrap(); + let convos = ConversationStore::load_conversations(&alice2.storage).unwrap(); assert_eq!(convos.len(), 1, "conversation metadata should persist"); } diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index de0c023..58e0a69 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -7,6 +7,7 @@ mod identity; mod inbox; mod proto; mod storage; +pub mod store; mod types; mod utils; diff --git a/core/conversations/src/storage.rs b/core/conversations/src/storage.rs index 8df51c4..2170a8f 100644 --- a/core/conversations/src/storage.rs +++ b/core/conversations/src/storage.rs @@ -10,6 +10,9 @@ use zeroize::Zeroize; use crate::{ identity::Identity, storage::types::{ConversationRecord, IdentityRecord}, + store::{ + ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore, + }, }; /// Chat-specific storage operations. @@ -204,6 +207,15 @@ impl ChatStorage { } } + /// Removes a conversation record by its local ID. + pub fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError> { + self.db.connection().execute( + "DELETE FROM conversations WHERE local_convo_id = ?1", + params![local_convo_id], + )?; + Ok(()) + } + /// Loads all conversation records. pub fn load_conversations(&self) -> Result, StorageError> { let mut stmt = self @@ -225,6 +237,79 @@ impl ChatStorage { } } +impl IdentityStore for ChatStorage { + fn load_identity(&self) -> Result, StorageError> { + ChatStorage::load_identity(self) + } + + fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError> { + ChatStorage::save_identity(self, identity) + } +} + +impl EphemeralKeyStore for ChatStorage { + fn save_ephemeral_key( + &mut self, + public_key_hex: &str, + private_key: &PrivateKey, + ) -> Result<(), StorageError> { + ChatStorage::save_ephemeral_key(self, public_key_hex, private_key) + } + + fn load_ephemeral_key(&self, public_key_hex: &str) -> Result, StorageError> { + ChatStorage::load_ephemeral_key(self, public_key_hex) + } + + fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), StorageError> { + ChatStorage::remove_ephemeral_key(self, public_key_hex) + } +} + +impl ConversationStore for ChatStorage { + fn save_conversation(&mut self, meta: &ConversationMeta) -> Result<(), StorageError> { + ChatStorage::save_conversation( + self, + &meta.local_convo_id, + &meta.remote_convo_id, + meta.kind.as_db(), + ) + } + + fn load_conversation( + &self, + local_convo_id: &str, + ) -> Result, StorageError> { + let record = ChatStorage::load_conversation(self, local_convo_id)?; + + Ok(record.map(|record| ConversationMeta { + local_convo_id: record.local_convo_id, + remote_convo_id: record.remote_convo_id, + kind: ConversationKind::from_db(&record.convo_type), + })) + } + + fn load_conversations(&self) -> Result, StorageError> { + let records = ChatStorage::load_conversations(self)?; + + Ok(records + .into_iter() + .map(|record| ConversationMeta { + local_convo_id: record.local_convo_id, + remote_convo_id: record.remote_convo_id, + kind: ConversationKind::from_db(&record.convo_type), + }) + .collect()) + } + + fn has_conversation(&self, local_convo_id: &str) -> Result { + ChatStorage::has_conversation(self, local_convo_id) + } + + fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError> { + ChatStorage::remove_conversation(self, local_convo_id) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/conversations/src/store.rs b/core/conversations/src/store.rs new file mode 100644 index 0000000..4cbf189 --- /dev/null +++ b/core/conversations/src/store.rs @@ -0,0 +1,11 @@ +mod conversations; +mod ephemeral_keys; +mod identity; + +pub use conversations::{ConversationKind, ConversationMeta, ConversationStore}; +pub use ephemeral_keys::EphemeralKeyStore; +pub use identity::IdentityStore; + +pub trait ChatStore: IdentityStore + EphemeralKeyStore + ConversationStore {} + +impl ChatStore for T where T: IdentityStore + EphemeralKeyStore + ConversationStore {} diff --git a/core/conversations/src/store/conversations.rs b/core/conversations/src/store/conversations.rs new file mode 100644 index 0000000..b7c9e74 --- /dev/null +++ b/core/conversations/src/store/conversations.rs @@ -0,0 +1,45 @@ +use storage::StorageError; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConversationKind { + PrivateV1, + Unknown(String), +} + +impl ConversationKind { + pub fn from_db(value: &str) -> Self { + match value { + "private_v1" => Self::PrivateV1, + other => Self::Unknown(other.to_string()), + } + } + + pub fn as_db(&self) -> &str { + match self { + Self::PrivateV1 => "private_v1", + Self::Unknown(value) => value.as_str(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConversationMeta { + pub local_convo_id: String, + pub remote_convo_id: String, + pub kind: ConversationKind, +} + +pub trait ConversationStore { + fn save_conversation(&mut self, meta: &ConversationMeta) -> Result<(), StorageError>; + + fn load_conversation( + &self, + local_convo_id: &str, + ) -> Result, StorageError>; + + fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), StorageError>; + + fn load_conversations(&self) -> Result, StorageError>; + + fn has_conversation(&self, local_convo_id: &str) -> Result; +} diff --git a/core/conversations/src/store/ephemeral_keys.rs b/core/conversations/src/store/ephemeral_keys.rs new file mode 100644 index 0000000..0f50e6a --- /dev/null +++ b/core/conversations/src/store/ephemeral_keys.rs @@ -0,0 +1,14 @@ +use crypto::PrivateKey; +use storage::StorageError; + +pub trait EphemeralKeyStore { + fn save_ephemeral_key( + &mut self, + public_key_hex: &str, + private_key: &PrivateKey, + ) -> Result<(), StorageError>; + + fn load_ephemeral_key(&self, public_key_hex: &str) -> Result, StorageError>; + + fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), StorageError>; +} diff --git a/core/conversations/src/store/identity.rs b/core/conversations/src/store/identity.rs new file mode 100644 index 0000000..8f4e9f4 --- /dev/null +++ b/core/conversations/src/store/identity.rs @@ -0,0 +1,12 @@ +use storage::StorageError; + +use crate::identity::Identity; + +/// Persistence operations for installation identity data. +pub trait IdentityStore { + /// Loads the stored identity if one exists. + fn load_identity(&self) -> Result, StorageError>; + + /// Persists the installation identity. + fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError>; +}