diff --git a/conversations/src/chat.rs b/conversations/src/chat.rs index 1e6025d..4d7ce6b 100644 --- a/conversations/src/chat.rs +++ b/conversations/src/chat.rs @@ -6,6 +6,8 @@ use std::collections::HashMap; use std::rc::Rc; +use double_ratchets::storage::RatchetStorage; + use crate::{ common::{Chat, HasChatId, InboundMessageHandler}, dm::privatev1::PrivateV1Convo, @@ -67,7 +69,10 @@ pub struct ChatManager { /// In-memory cache of active chats. Chats are loaded from storage on demand. chats: HashMap, inbox: Inbox, + /// Storage for chat metadata (identity, inbox keys, chat records). storage: ChatStorage, + /// Storage for ratchet state (delegated to double-ratchets crate). + ratchet_storage: RatchetStorage, } impl ChatManager { @@ -78,7 +83,10 @@ impl ChatManager { /// /// Inbox ephemeral keys are loaded lazily when handling incoming handshakes. pub fn open(config: StorageConfig) -> Result { - let mut storage = ChatStorage::new(config)?; + let mut storage = ChatStorage::new(config.clone())?; + + // Initialize ratchet storage (delegated to double-ratchets crate) + let ratchet_storage = RatchetStorage::with_config(config)?; // Load or create identity let identity = if let Some(identity) = storage.load_identity()? { @@ -100,6 +108,7 @@ impl ChatManager { chats: HashMap::new(), inbox, storage, + ratchet_storage, }) } @@ -160,10 +169,8 @@ impl ChatManager { ); self.storage.save_chat(&chat_record)?; - // Persist ratchet state - let (state, skipped_keys) = convo.to_storage(); - self.storage - .save_ratchet_state(&chat_id, &state, &skipped_keys)?; + // Persist ratchet state (delegated to double-ratchets storage) + self.ratchet_storage.save(&chat_id, convo.ratchet_state())?; // Store in memory cache self.chats.insert(chat_id.clone(), convo); @@ -189,10 +196,8 @@ impl ChatManager { let payloads = chat.send_message(content)?; - // Persist updated ratchet state - let (state, skipped_keys) = chat.to_storage(); - self.storage - .save_ratchet_state(chat_id, &state, &skipped_keys)?; + // Persist updated ratchet state (delegated to double-ratchets storage) + self.ratchet_storage.save(chat_id, chat.ratchet_state())?; let remote_id = chat.remote_id(); Ok(payloads @@ -207,9 +212,10 @@ impl ChatManager { return Ok(()); } - // Try to load from storage - if let Some((state, skipped_keys)) = self.storage.load_ratchet_state(chat_id)? { - let convo = PrivateV1Convo::from_storage(chat_id.to_string(), state, skipped_keys); + // Try to load ratchet state from double-ratchets storage + if self.ratchet_storage.exists(chat_id)? { + let dr_state = self.ratchet_storage.load(chat_id)?; + let convo = PrivateV1Convo::from_state(chat_id.to_string(), dr_state); self.chats.insert(chat_id.to_string(), convo); Ok(()) } else if self.storage.chat_exists(chat_id)? { @@ -301,6 +307,8 @@ impl ChatManager { pub fn delete_chat(&mut self, chat_id: &str) -> Result<(), ChatManagerError> { self.chats.remove(chat_id); self.storage.delete_chat(chat_id)?; + // Also delete ratchet state from double-ratchets storage + let _ = self.ratchet_storage.delete(chat_id); Ok(()) } } @@ -419,10 +427,9 @@ mod tests { // Scope 1: Create chat and send messages { - let mut alice = ChatManager::open(StorageConfig::File( - db_path.to_str().unwrap().to_string(), - )) - .unwrap(); + let mut alice = + ChatManager::open(StorageConfig::File(db_path.to_str().unwrap().to_string())) + .unwrap(); let result = alice.start_private_chat(&bob_intro, "Message 1").unwrap(); chat_id = result.0; @@ -438,10 +445,9 @@ mod tests { // Scope 2: Reopen and verify chat is restored { - let mut alice2 = ChatManager::open(StorageConfig::File( - db_path.to_str().unwrap().to_string(), - )) - .unwrap(); + let mut alice2 = + ChatManager::open(StorageConfig::File(db_path.to_str().unwrap().to_string())) + .unwrap(); // Chat is in storage but not loaded yet assert!(alice2.list_stored_chats().unwrap().contains(&chat_id)); diff --git a/conversations/src/dm/privatev1.rs b/conversations/src/dm/privatev1.rs index 2a6e74b..89905e6 100644 --- a/conversations/src/dm/privatev1.rs +++ b/conversations/src/dm/privatev1.rs @@ -12,7 +12,6 @@ use crate::{ common::{Chat, ChatId, HasChatId}, errors::{ChatError, EncryptionError}, proto, - storage::types::{RatchetStateRecord, SkippedKeyRecord}, types::AddressedEncryptedPayload, utils::timestamp_millis, }; @@ -34,7 +33,11 @@ impl PrivateV1Convo { } } - pub fn new_responder(chat_id: String, seed_key: SecretKey, dh_self: InstallationKeyPair) -> Self { + pub fn new_responder( + chat_id: String, + seed_key: SecretKey, + dh_self: InstallationKeyPair, + ) -> Self { Self { chat_id, // TODO: Danger - Fix double-ratchets types to Accept SecretKey @@ -42,62 +45,14 @@ impl PrivateV1Convo { } } - /// Restore a conversation from stored ratchet state. - pub fn from_storage( - chat_id: String, - state: RatchetStateRecord, - skipped_keys: Vec, - ) -> Self { - use std::collections::HashMap; - - let dh_self = InstallationKeyPair::from_secret_bytes(state.dh_self_secret); - let dh_remote = state.dh_remote.map(PublicKey::from); - - let skipped: HashMap<(PublicKey, u32), [u8; 32]> = skipped_keys - .into_iter() - .map(|sk| ((PublicKey::from(sk.public_key), sk.msg_num), sk.message_key)) - .collect(); - - let dr_state = RatchetState::from_parts( - state.root_key, - state.sending_chain, - state.receiving_chain, - dh_self, - dh_remote, - state.msg_send, - state.msg_recv, - state.prev_chain_len, - skipped, - ); - + /// Restore a conversation from a loaded RatchetState. + pub fn from_state(chat_id: String, dr_state: RatchetState) -> Self { Self { chat_id, dr_state } } - /// Get the current ratchet state for storage. - pub fn to_storage(&self) -> (RatchetStateRecord, Vec) { - let state = RatchetStateRecord { - root_key: self.dr_state.root_key, - sending_chain: self.dr_state.sending_chain, - receiving_chain: self.dr_state.receiving_chain, - dh_self_secret: *self.dr_state.dh_self.secret_bytes(), - dh_remote: self.dr_state.dh_remote.map(|pk| pk.to_bytes()), - msg_send: self.dr_state.msg_send, - msg_recv: self.dr_state.msg_recv, - prev_chain_len: self.dr_state.prev_chain_len, - }; - - let skipped_keys: Vec = self - .dr_state - .skipped_keys - .iter() - .map(|((pk, msg_num), key)| SkippedKeyRecord { - public_key: pk.to_bytes(), - msg_num: *msg_num, - message_key: *key, - }) - .collect(); - - (state, skipped_keys) + /// Get a reference to the ratchet state for storage. + pub fn ratchet_state(&self) -> &RatchetState { + &self.dr_state } fn encrypt(&mut self, frame: PrivateV1Frame) -> EncryptedPayload { diff --git a/conversations/src/storage/db.rs b/conversations/src/storage/db.rs index 4993e70..313edfc 100644 --- a/conversations/src/storage/db.rs +++ b/conversations/src/storage/db.rs @@ -5,10 +5,11 @@ use std::collections::HashMap; use storage::{RusqliteError, SqliteDb, StorageConfig, StorageError, params}; use x25519_dalek::StaticSecret; -use super::types::{ChatRecord, IdentityRecord, RatchetStateRecord, SkippedKeyRecord}; +use super::types::{ChatRecord, IdentityRecord}; use crate::identity::Identity; /// Schema for chat storage tables. +/// Note: Ratchet state is stored by double_ratchets::RatchetStorage separately. const CHAT_SCHEMA: &str = " -- Identity table (single row) CREATE TABLE IF NOT EXISTS identity ( @@ -33,39 +34,14 @@ const CHAT_SCHEMA: &str = " ); CREATE INDEX IF NOT EXISTS idx_chats_type ON chats(chat_type); - - -- Ratchet state for each conversation - CREATE TABLE IF NOT EXISTS ratchet_state ( - conversation_id TEXT PRIMARY KEY, - root_key BLOB NOT NULL, - sending_chain BLOB, - receiving_chain BLOB, - dh_self_secret BLOB NOT NULL, - dh_remote BLOB, - msg_send INTEGER NOT NULL, - msg_recv INTEGER NOT NULL, - prev_chain_len INTEGER NOT NULL - ); - - -- Skipped message keys (for out-of-order messages) - CREATE TABLE IF NOT EXISTS skipped_keys ( - conversation_id TEXT NOT NULL, - public_key BLOB NOT NULL, - msg_num INTEGER NOT NULL, - message_key BLOB NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - PRIMARY KEY (conversation_id, public_key, msg_num), - FOREIGN KEY (conversation_id) REFERENCES ratchet_state(conversation_id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS idx_skipped_keys_conversation - ON skipped_keys(conversation_id); "; /// Chat-specific storage operations. /// -/// This struct wraps a `SqliteDb` and provides domain-specific -/// storage operations for chat state. +/// This struct wraps a SqliteDb and provides domain-specific +/// storage operations for chat state (identity, inbox keys, chat metadata). +/// +/// Note: Ratchet state persistence is delegated to double_ratchets::RatchetStorage. pub struct ChatStorage { db: SqliteDb, } @@ -125,35 +101,6 @@ impl ChatStorage { } } - /// Saves a chat record. - pub fn save_chat(&mut self, chat: &ChatRecord) -> Result<(), StorageError> { - self.db.connection().execute( - "INSERT OR REPLACE INTO chats (chat_id, chat_type, remote_public_key, remote_address, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - chat.chat_id, - chat.chat_type, - chat.remote_public_key.as_ref().map(|k| k.as_slice()), - chat.remote_address, - chat.created_at, - ], - )?; - Ok(()) - } - - /// Lists all chat IDs. - pub fn list_chat_ids(&self) -> Result, StorageError> { - let mut stmt = self.db.connection().prepare("SELECT chat_id FROM chats")?; - let rows = stmt.query_map([], |row| row.get(0))?; - - let mut ids = Vec::new(); - for row in rows { - ids.push(row?); - } - - Ok(ids) - } - // ==================== Inbox Key Operations ==================== /// Saves an inbox ephemeral key. @@ -230,7 +177,23 @@ impl ChatStorage { Ok(()) } - // ==================== Chat Operations ==================== + // ==================== Chat Metadata Operations ==================== + + /// Saves a chat record. + pub fn save_chat(&mut self, chat: &ChatRecord) -> Result<(), StorageError> { + self.db.connection().execute( + "INSERT OR REPLACE INTO chats (chat_id, chat_type, remote_public_key, remote_address, created_at) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![ + chat.chat_id, + chat.chat_type, + chat.remote_public_key.as_ref().map(|k| k.as_slice()), + chat.remote_address, + chat.created_at, + ], + )?; + Ok(()) + } /// Loads a chat record by ID. pub fn load_chat(&self, chat_id: &str) -> Result, StorageError> { @@ -273,6 +236,19 @@ impl ChatStorage { } } + /// Lists all chat IDs. + pub fn list_chat_ids(&self) -> Result, StorageError> { + let mut stmt = self.db.connection().prepare("SELECT chat_id FROM chats")?; + let rows = stmt.query_map([], |row| row.get(0))?; + + let mut ids = Vec::new(); + for row in rows { + ids.push(row?); + } + + Ok(ids) + } + /// Checks if a chat exists in storage. pub fn chat_exists(&self, chat_id: &str) -> Result { let mut stmt = self @@ -284,181 +260,14 @@ impl ChatStorage { Ok(exists) } - /// Deletes a chat record and its ratchet state. + /// Deletes a chat record. + /// Note: Ratchet state must be deleted separately via RatchetStorage. pub fn delete_chat(&mut self, chat_id: &str) -> Result<(), StorageError> { - let tx = self.db.transaction()?; - // Delete skipped keys first (foreign key constraint) - tx.execute( - "DELETE FROM skipped_keys WHERE conversation_id = ?1", - params![chat_id], - )?; - tx.execute( - "DELETE FROM ratchet_state WHERE conversation_id = ?1", - params![chat_id], - )?; - tx.execute("DELETE FROM chats WHERE chat_id = ?1", params![chat_id])?; - tx.commit()?; + self.db + .connection() + .execute("DELETE FROM chats WHERE chat_id = ?1", params![chat_id])?; Ok(()) } - - // ==================== Ratchet State Operations ==================== - - /// Saves the ratchet state for a conversation. - pub fn save_ratchet_state( - &mut self, - conversation_id: &str, - state: &RatchetStateRecord, - skipped_keys: &[SkippedKeyRecord], - ) -> Result<(), StorageError> { - let tx = self.db.transaction()?; - - // Upsert main state - tx.execute( - " - INSERT INTO ratchet_state ( - conversation_id, root_key, sending_chain, receiving_chain, - dh_self_secret, dh_remote, msg_send, msg_recv, prev_chain_len - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) - ON CONFLICT(conversation_id) DO UPDATE SET - root_key = excluded.root_key, - sending_chain = excluded.sending_chain, - receiving_chain = excluded.receiving_chain, - dh_self_secret = excluded.dh_self_secret, - dh_remote = excluded.dh_remote, - msg_send = excluded.msg_send, - msg_recv = excluded.msg_recv, - prev_chain_len = excluded.prev_chain_len - ", - params![ - conversation_id, - state.root_key.as_slice(), - state.sending_chain.as_ref().map(|c| c.as_slice()), - state.receiving_chain.as_ref().map(|c| c.as_slice()), - state.dh_self_secret.as_slice(), - state.dh_remote.as_ref().map(|c| c.as_slice()), - state.msg_send, - state.msg_recv, - state.prev_chain_len, - ], - )?; - - // Sync skipped keys: delete old ones and insert new - tx.execute( - "DELETE FROM skipped_keys WHERE conversation_id = ?1", - params![conversation_id], - )?; - - for sk in skipped_keys { - tx.execute( - "INSERT INTO skipped_keys (conversation_id, public_key, msg_num, message_key) - VALUES (?1, ?2, ?3, ?4)", - params![ - conversation_id, - sk.public_key.as_slice(), - sk.msg_num, - sk.message_key.as_slice(), - ], - )?; - } - - tx.commit()?; - Ok(()) - } - - /// Loads the ratchet state for a conversation. - pub fn load_ratchet_state( - &self, - conversation_id: &str, - ) -> Result)>, StorageError> { - // Load main state - let state = self.load_ratchet_state_data(conversation_id)?; - let state = match state { - Some(s) => s, - None => return Ok(None), - }; - - // Load skipped keys - let skipped_keys = self.load_skipped_keys(conversation_id)?; - - Ok(Some((state, skipped_keys))) - } - - fn load_ratchet_state_data( - &self, - conversation_id: &str, - ) -> Result, StorageError> { - let conn = self.db.connection(); - let mut stmt = conn.prepare( - " - SELECT root_key, sending_chain, receiving_chain, dh_self_secret, - dh_remote, msg_send, msg_recv, prev_chain_len - FROM ratchet_state - WHERE conversation_id = ?1 - ", - )?; - - let result = stmt.query_row(params![conversation_id], |row| { - Ok(RatchetStateRecord { - root_key: blob_to_array(row.get::<_, Vec>(0)?), - sending_chain: row.get::<_, Option>>(1)?.map(blob_to_array), - receiving_chain: row.get::<_, Option>>(2)?.map(blob_to_array), - dh_self_secret: blob_to_array(row.get::<_, Vec>(3)?), - dh_remote: row.get::<_, Option>>(4)?.map(blob_to_array), - msg_send: row.get(5)?, - msg_recv: row.get(6)?, - prev_chain_len: row.get(7)?, - }) - }); - - match result { - Ok(record) => Ok(Some(record)), - Err(RusqliteError::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(StorageError::Database(e.to_string())), - } - } - - fn load_skipped_keys( - &self, - conversation_id: &str, - ) -> Result, StorageError> { - let conn = self.db.connection(); - let mut stmt = conn.prepare( - " - SELECT public_key, msg_num, message_key - FROM skipped_keys - WHERE conversation_id = ?1 - ", - )?; - - let rows = stmt.query_map(params![conversation_id], |row| { - Ok(SkippedKeyRecord { - public_key: blob_to_array(row.get::<_, Vec>(0)?), - msg_num: row.get(1)?, - message_key: blob_to_array(row.get::<_, Vec>(2)?), - }) - })?; - - rows.collect::, _>>() - .map_err(|e| StorageError::Database(e.to_string())) - } - - /// Checks if a ratchet state exists for a conversation. - pub fn ratchet_state_exists(&self, conversation_id: &str) -> Result { - let conn = self.db.connection(); - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM ratchet_state WHERE conversation_id = ?1", - params![conversation_id], - |row| row.get(0), - )?; - Ok(count > 0) - } -} - -/// Helper to convert a Vec to a fixed-size array. -fn blob_to_array(blob: Vec) -> [u8; 32] { - let mut arr = [0u8; 32]; - arr.copy_from_slice(&blob); - arr } #[cfg(test)] diff --git a/conversations/src/storage/types.rs b/conversations/src/storage/types.rs index 4c3a5bb..553ac1b 100644 --- a/conversations/src/storage/types.rs +++ b/conversations/src/storage/types.rs @@ -1,4 +1,7 @@ //! Storage record types for serialization/deserialization. +//! +//! Note: Ratchet state types (RatchetStateRecord, SkippedKeyRecord) are in +//! double_ratchets::storage module and handled by RatchetStorage. use x25519_dalek::{PublicKey, StaticSecret}; @@ -27,7 +30,7 @@ impl From for Identity { } /// Record for storing chat metadata. -/// Note: The actual double ratchet state is stored separately by the DR storage. +/// Note: The actual double ratchet state is stored separately by RatchetStorage. #[derive(Debug, Clone)] pub struct ChatRecord { /// Unique chat identifier. @@ -57,24 +60,3 @@ impl ChatRecord { } } } - -/// Raw ratchet state data for SQLite storage. -#[derive(Debug, Clone)] -pub struct RatchetStateRecord { - pub root_key: [u8; 32], - pub sending_chain: Option<[u8; 32]>, - pub receiving_chain: Option<[u8; 32]>, - pub dh_self_secret: [u8; 32], - pub dh_remote: Option<[u8; 32]>, - pub msg_send: u32, - pub msg_recv: u32, - pub prev_chain_len: u32, -} - -/// Skipped key record for out-of-order message handling. -#[derive(Debug, Clone)] -pub struct SkippedKeyRecord { - pub public_key: [u8; 32], - pub msg_num: u32, - pub message_key: [u8; 32], -} diff --git a/double-ratchets/src/storage/db.rs b/double-ratchets/src/storage/db.rs index 2c216d7..4ae0557 100644 --- a/double-ratchets/src/storage/db.rs +++ b/double-ratchets/src/storage/db.rs @@ -47,6 +47,12 @@ pub struct RatchetStorage { } impl RatchetStorage { + /// Creates a new RatchetStorage with the given configuration. + pub fn with_config(config: storage::StorageConfig) -> Result { + let db = SqliteDb::new(config)?; + Self::run_migration(db) + } + /// Opens an existing encrypted database file. pub fn new(path: &str, key: &str) -> Result { let db = SqliteDb::sqlcipher(path.to_string(), key.to_string())?;