diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index 43b7c2d..e89d31a 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use std::sync::Arc; -use double_ratchets::{RatchetState, RatchetStorage}; +use double_ratchets::{DefaultRatchetStore, RatchetState, RatchetStorage}; use storage::StorageConfig; use crate::{ @@ -12,7 +12,8 @@ use crate::{ proto::{EncryptedPayload, EnvelopeV1, Message}, storage::ChatStorage, store::{ - ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore, + ChatStore, ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, + IdentityStore, }, types::{AddressedEnvelope, ContentData}, }; @@ -22,21 +23,27 @@ pub use crate::inbox::Introduction; // This is the main entry point to the conversations api. // Ctx manages lifetimes of objects to process and generate payloads. -pub struct Context { +pub struct Context +where + S: ChatStore, + R: DefaultRatchetStore, +{ _identity: Rc, inbox: Inbox, - storage: ChatStorage, - ratchet_storage: RatchetStorage, + storage: S, + ratchet_storage: R, } -impl Context { - /// Opens or creates a Context with the given storage configuration. - /// - /// If an identity exists in storage, it will be restored. - /// Otherwise, a new identity will be created with the given name and saved. - pub fn open(name: impl Into, config: StorageConfig) -> Result { - let mut storage = ChatStorage::new(config.clone())?; - let ratchet_storage = RatchetStorage::from_config(config)?; +impl Context +where + S: ChatStore, + R: DefaultRatchetStore, +{ + pub fn with_stores( + name: impl Into, + mut storage: S, + ratchet_storage: R, + ) -> Result { let name = name.into(); // Load or create identity @@ -59,17 +66,35 @@ impl Context { }) } + pub fn installation_name(&self) -> &str { + self._identity.get_name() + } +} + +impl Context { + /// Opens or creates a Context with the given storage configuration. + /// + /// If an identity exists in storage, it will be restored. + /// Otherwise, a new identity will be created with the given name and saved. + pub fn open(name: impl Into, config: StorageConfig) -> Result { + let storage = ChatStorage::new(config.clone())?; + let ratchet_storage = RatchetStorage::from_config(config)?; + Self::with_stores(name, storage, ratchet_storage) + } + /// Creates a new in-memory Context (for testing). /// /// Uses in-memory SQLite database. Each call creates a new isolated database. pub fn new_with_name(name: impl Into) -> Self { Self::open(name, StorageConfig::InMemory).expect("in-memory storage should not fail") } +} - pub fn installation_name(&self) -> &str { - self._identity.get_name() - } - +impl Context +where + S: ChatStore, + R: DefaultRatchetStore, +{ pub fn create_private_convo( &mut self, remote_bundle: &Introduction, @@ -177,7 +202,8 @@ impl Context { match meta.kind { ConversationKind::PrivateV1 => { - let dr_state: RatchetState = self.ratchet_storage.load(&meta.local_convo_id)?; + let dr_state: RatchetState = + DefaultRatchetStore::load_default(&self.ratchet_storage, &meta.local_convo_id)?; Ok(PrivateV1Convo::from_stored( meta.local_convo_id, @@ -207,12 +233,17 @@ impl Context { mod tests { use super::*; - fn send_and_verify( - sender: &mut Context, - receiver: &mut Context, + fn send_and_verify( + sender: &mut Context, + receiver: &mut Context, convo_id: ConversationId, content: &[u8], - ) { + ) where + S1: ChatStore, + R1: DefaultRatchetStore, + S2: ChatStore, + R2: DefaultRatchetStore, + { let payloads = sender.send_content(convo_id, content).unwrap(); let payload = payloads.first().unwrap(); let received = receiver diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index 4e15373..f2d1135 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -3,7 +3,7 @@ use std::sync::Arc; pub use crate::errors::ChatError; use crate::types::{AddressedEncryptedPayload, ContentData}; -use double_ratchets::RatchetStorage; +use double_ratchets::DefaultRatchetStore; pub type ConversationId<'a> = &'a str; pub type ConversationIdOwned = Arc; @@ -32,7 +32,7 @@ pub trait Convo: Id + Debug { fn convo_type(&self) -> &str; /// Persists ratchet state to storage. Default is no-op. - fn save_ratchet_state(&self, _storage: &mut RatchetStorage) -> Result<(), ChatError> { + fn save_ratchet_state(&self, _storage: &mut dyn DefaultRatchetStore) -> Result<(), ChatError> { Ok(()) } } diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs index 3cd506d..659400c 100644 --- a/core/conversations/src/conversation/privatev1.rs +++ b/core/conversations/src/conversation/privatev1.rs @@ -7,7 +7,7 @@ use chat_proto::logoschat::{ encryption::{Doubleratchet, EncryptedPayload, encrypted_payload::Encryption}, }; use crypto::{PrivateKey, PublicKey, SymmetricKey32}; -use double_ratchets::{Header, InstallationKeyPair, RatchetState}; +use double_ratchets::{DefaultRatchetStore, Header, InstallationKeyPair, RatchetState}; use prost::{Message, bytes::Bytes}; use std::fmt::Debug; @@ -18,7 +18,6 @@ use crate::{ types::{AddressedEncryptedPayload, ContentData}, utils::timestamp_millis, }; -use double_ratchets::RatchetStorage; // Represents the potential participant roles in this Conversation enum Role { @@ -228,8 +227,8 @@ impl Convo for PrivateV1Convo { "private_v1" } - fn save_ratchet_state(&self, storage: &mut RatchetStorage) -> Result<(), ChatError> { - storage.save(&self.local_convo_id, &self.dr_state)?; + fn save_ratchet_state(&self, storage: &mut dyn DefaultRatchetStore) -> Result<(), ChatError> { + storage.save_default(&self.local_convo_id, &self.dr_state)?; Ok(()) } } diff --git a/core/double-ratchets/src/lib.rs b/core/double-ratchets/src/lib.rs index c5abe43..7f42174 100644 --- a/core/double-ratchets/src/lib.rs +++ b/core/double-ratchets/src/lib.rs @@ -6,9 +6,11 @@ pub mod keypair; pub mod reader; pub mod state; pub mod storage; +pub mod store; pub mod types; pub use keypair::InstallationKeyPair; pub use state::{Header, RatchetState, SkippedKey}; pub use storage::StorageConfig; pub use storage::{RatchetSession, RatchetStorage, SessionError}; +pub use store::DefaultRatchetStore; diff --git a/core/double-ratchets/src/storage/db.rs b/core/double-ratchets/src/storage/db.rs index c69d813..3fcd55b 100644 --- a/core/double-ratchets/src/storage/db.rs +++ b/core/double-ratchets/src/storage/db.rs @@ -8,6 +8,7 @@ use super::types::RatchetStateRecord; use crate::{ hkdf::HkdfInfo, state::{RatchetState, SkippedKey}, + store::RatchetStore, }; /// Schema for ratchet state tables. @@ -220,6 +221,28 @@ impl RatchetStorage { } } +impl RatchetStore for RatchetStorage { + fn save( + &mut self, + conversation_id: &str, + state: &RatchetState, + ) -> Result<(), StorageError> { + RatchetStorage::save(self, conversation_id, state) + } + + fn load(&self, conversation_id: &str) -> Result, StorageError> { + RatchetStorage::load(self, conversation_id) + } + + fn exists(&self, conversation_id: &str) -> Result { + RatchetStorage::exists(self, conversation_id) + } + + fn delete(&mut self, conversation_id: &str) -> Result<(), StorageError> { + RatchetStorage::delete(self, conversation_id) + } +} + /// Syncs skipped keys efficiently by computing diff and only inserting/deleting changes. fn sync_skipped_keys( tx: &storage::Transaction, diff --git a/core/double-ratchets/src/storage/session.rs b/core/double-ratchets/src/storage/session.rs index ea3cdfc..d20bd52 100644 --- a/core/double-ratchets/src/storage/session.rs +++ b/core/double-ratchets/src/storage/session.rs @@ -9,20 +9,24 @@ use crate::{ types::SharedSecret, }; -use super::RatchetStorage; +use crate::store::RatchetStore; /// A session wrapper that automatically persists ratchet state after operations. /// Provides rollback semantics - state is only saved if the operation succeeds. -pub struct RatchetSession<'a, D: HkdfInfo + Clone = DefaultDomain> { - storage: &'a mut RatchetStorage, +pub struct RatchetSession< + 'a, + D: HkdfInfo + Clone = DefaultDomain, + S: RatchetStore = crate::RatchetStorage, +> { + storage: &'a mut S, conversation_id: String, state: RatchetState, } -impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> { +impl<'a, D: HkdfInfo + Clone, S: RatchetStore> RatchetSession<'a, D, S> { /// Opens an existing session from storage. pub fn open( - storage: &'a mut RatchetStorage, + storage: &'a mut S, conversation_id: impl Into, ) -> Result { let conversation_id = conversation_id.into(); @@ -36,7 +40,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> { /// Creates a new session and persists the initial state. pub fn create( - storage: &'a mut RatchetStorage, + storage: &'a mut S, conversation_id: impl Into, state: RatchetState, ) -> Result { @@ -51,7 +55,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> { /// Initializes a new session as a sender and persists the initial state. pub fn create_sender_session( - storage: &'a mut RatchetStorage, + storage: &'a mut S, conversation_id: &str, shared_secret: SharedSecret, remote_pub: PublicKey, @@ -65,7 +69,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> { /// Initializes a new session as a receiver and persists the initial state. pub fn create_receiver_session( - storage: &'a mut RatchetStorage, + storage: &'a mut S, conversation_id: &str, shared_secret: SharedSecret, dh_self: InstallationKeyPair, @@ -158,8 +162,8 @@ mod tests { use super::*; use crate::hkdf::DefaultDomain; - fn create_test_storage() -> RatchetStorage { - RatchetStorage::in_memory().unwrap() + fn create_test_storage() -> crate::RatchetStorage { + crate::RatchetStorage::in_memory().unwrap() } #[test] diff --git a/core/double-ratchets/src/store.rs b/core/double-ratchets/src/store.rs new file mode 100644 index 0000000..44aafd0 --- /dev/null +++ b/core/double-ratchets/src/store.rs @@ -0,0 +1,3 @@ +mod ratchets; + +pub use ratchets::{DefaultRatchetStore, RatchetStore}; diff --git a/core/double-ratchets/src/store/ratchets.rs b/core/double-ratchets/src/store/ratchets.rs new file mode 100644 index 0000000..890aa3d --- /dev/null +++ b/core/double-ratchets/src/store/ratchets.rs @@ -0,0 +1,75 @@ +use storage::StorageError; + +use crate::{ + hkdf::{DefaultDomain, HkdfInfo}, + state::RatchetState, +}; + +/// Persistence operations for Double Ratchet conversation state. +pub trait RatchetStore { + /// Saves the ratchet state for a conversation. + fn save( + &mut self, + conversation_id: &str, + state: &RatchetState, + ) -> Result<(), StorageError>; + + /// Loads the ratchet state for a conversation. + fn load(&self, conversation_id: &str) -> Result, StorageError>; + + /// Checks whether a ratchet state exists for the conversation. + fn exists(&self, conversation_id: &str) -> Result; + + /// Deletes the ratchet state and any related skipped keys for the conversation. + fn delete(&mut self, conversation_id: &str) -> Result<(), StorageError>; +} + +/// Object-safe ratchet storage operations for the default HKDF domain. +/// +/// This is useful for crates that need dynamic dispatch, such as `conversations`, +/// while still allowing the more general `RatchetStore` trait to stay generic. +pub trait DefaultRatchetStore { + /// Saves the default-domain ratchet state for a conversation. + fn save_default( + &mut self, + conversation_id: &str, + state: &RatchetState, + ) -> Result<(), StorageError>; + + /// Loads the default-domain ratchet state for a conversation. + fn load_default( + &self, + conversation_id: &str, + ) -> Result, StorageError>; + + /// Checks whether a ratchet state exists for the conversation. + fn exists_default(&self, conversation_id: &str) -> Result; + + /// Deletes the ratchet state and any related skipped keys for the conversation. + fn delete_default(&mut self, conversation_id: &str) -> Result<(), StorageError>; +} + +impl DefaultRatchetStore for T { + fn save_default( + &mut self, + conversation_id: &str, + state: &RatchetState, + ) -> Result<(), StorageError> { + self.save(conversation_id, state) + } + + fn load_default( + &self, + conversation_id: &str, + ) -> Result, StorageError> { + self.load(conversation_id) + } + + fn exists_default(&self, conversation_id: &str) -> Result { + self.exists(conversation_id) + } + + fn delete_default(&mut self, conversation_id: &str) -> Result<(), StorageError> { + self.delete(conversation_id) + } +}