mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-04-03 10:03:14 +00:00
feat: abstract double ratchet traits
This commit is contained in:
parent
4cb2ec1608
commit
cbcf5e3eb6
@ -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<S = ChatStorage, R = RatchetStorage>
|
||||
where
|
||||
S: ChatStore,
|
||||
R: DefaultRatchetStore,
|
||||
{
|
||||
_identity: Rc<Identity>,
|
||||
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<String>, config: StorageConfig) -> Result<Self, ChatError> {
|
||||
let mut storage = ChatStorage::new(config.clone())?;
|
||||
let ratchet_storage = RatchetStorage::from_config(config)?;
|
||||
impl<S, R> Context<S, R>
|
||||
where
|
||||
S: ChatStore,
|
||||
R: DefaultRatchetStore,
|
||||
{
|
||||
pub fn with_stores(
|
||||
name: impl Into<String>,
|
||||
mut storage: S,
|
||||
ratchet_storage: R,
|
||||
) -> Result<Self, ChatError> {
|
||||
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<ChatStorage, RatchetStorage> {
|
||||
/// 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<String>, config: StorageConfig) -> Result<Self, ChatError> {
|
||||
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<String>) -> Self {
|
||||
Self::open(name, StorageConfig::InMemory).expect("in-memory storage should not fail")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn installation_name(&self) -> &str {
|
||||
self._identity.get_name()
|
||||
}
|
||||
|
||||
impl<S, R> Context<S, R>
|
||||
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<S1, R1, S2, R2>(
|
||||
sender: &mut Context<S1, R1>,
|
||||
receiver: &mut Context<S2, R2>,
|
||||
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
|
||||
|
||||
@ -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<str>;
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<D: HkdfInfo>(
|
||||
&mut self,
|
||||
conversation_id: &str,
|
||||
state: &RatchetState<D>,
|
||||
) -> Result<(), StorageError> {
|
||||
RatchetStorage::save(self, conversation_id, state)
|
||||
}
|
||||
|
||||
fn load<D: HkdfInfo>(&self, conversation_id: &str) -> Result<RatchetState<D>, StorageError> {
|
||||
RatchetStorage::load(self, conversation_id)
|
||||
}
|
||||
|
||||
fn exists(&self, conversation_id: &str) -> Result<bool, StorageError> {
|
||||
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,
|
||||
|
||||
@ -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<D>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
) -> Result<Self, SessionError> {
|
||||
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<String>,
|
||||
state: RatchetState<D>,
|
||||
) -> Result<Self, SessionError> {
|
||||
@ -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]
|
||||
|
||||
3
core/double-ratchets/src/store.rs
Normal file
3
core/double-ratchets/src/store.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod ratchets;
|
||||
|
||||
pub use ratchets::{DefaultRatchetStore, RatchetStore};
|
||||
75
core/double-ratchets/src/store/ratchets.rs
Normal file
75
core/double-ratchets/src/store/ratchets.rs
Normal file
@ -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<D: HkdfInfo>(
|
||||
&mut self,
|
||||
conversation_id: &str,
|
||||
state: &RatchetState<D>,
|
||||
) -> Result<(), StorageError>;
|
||||
|
||||
/// Loads the ratchet state for a conversation.
|
||||
fn load<D: HkdfInfo>(&self, conversation_id: &str) -> Result<RatchetState<D>, StorageError>;
|
||||
|
||||
/// Checks whether a ratchet state exists for the conversation.
|
||||
fn exists(&self, conversation_id: &str) -> Result<bool, StorageError>;
|
||||
|
||||
/// 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<DefaultDomain>,
|
||||
) -> Result<(), StorageError>;
|
||||
|
||||
/// Loads the default-domain ratchet state for a conversation.
|
||||
fn load_default(
|
||||
&self,
|
||||
conversation_id: &str,
|
||||
) -> Result<RatchetState<DefaultDomain>, StorageError>;
|
||||
|
||||
/// Checks whether a ratchet state exists for the conversation.
|
||||
fn exists_default(&self, conversation_id: &str) -> Result<bool, StorageError>;
|
||||
|
||||
/// Deletes the ratchet state and any related skipped keys for the conversation.
|
||||
fn delete_default(&mut self, conversation_id: &str) -> Result<(), StorageError>;
|
||||
}
|
||||
|
||||
impl<T: RatchetStore + ?Sized> DefaultRatchetStore for T {
|
||||
fn save_default(
|
||||
&mut self,
|
||||
conversation_id: &str,
|
||||
state: &RatchetState<DefaultDomain>,
|
||||
) -> Result<(), StorageError> {
|
||||
self.save(conversation_id, state)
|
||||
}
|
||||
|
||||
fn load_default(
|
||||
&self,
|
||||
conversation_id: &str,
|
||||
) -> Result<RatchetState<DefaultDomain>, StorageError> {
|
||||
self.load(conversation_id)
|
||||
}
|
||||
|
||||
fn exists_default(&self, conversation_id: &str) -> Result<bool, StorageError> {
|
||||
self.exists(conversation_id)
|
||||
}
|
||||
|
||||
fn delete_default(&mut self, conversation_id: &str) -> Result<(), StorageError> {
|
||||
self.delete(conversation_id)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user