From daeecbd6799c13cbe103296025c1cd7ba4110b2d Mon Sep 17 00:00:00 2001 From: kaichao Date: Wed, 25 Mar 2026 08:45:22 +0800 Subject: [PATCH 1/3] Persist identity (#67) * feat: storage for conversations * fix: db types conversion * feat: run migrations from sql files * feat: persist identity * fix: revert double ratchet storage refactor * fix: clean * refactor: use result wrapper for ffi * refactor: uniform storage error into chat error * fix: zeroize identity record * fix: zeroize for secret keys in db operations * fix: transactional sql migration * fix: remove destroy_string * refactor: inline identity record name clone --- Cargo.lock | 3 + conversations/Cargo.toml | 5 + conversations/src/api.rs | 38 ++++++ conversations/src/context.rs | 67 ++++++++++- conversations/src/errors.rs | 4 + conversations/src/ffi/mod.rs | 1 + conversations/src/ffi/utils.rs | 8 ++ conversations/src/identity.rs | 7 ++ conversations/src/lib.rs | 2 + conversations/src/storage/db.rs | 111 ++++++++++++++++++ conversations/src/storage/migrations.rs | 46 ++++++++ .../storage/migrations/001_initial_schema.sql | 9 ++ conversations/src/storage/mod.rs | 7 ++ conversations/src/storage/types.rs | 57 +++++++++ storage/src/errors.rs | 4 + storage/src/lib.rs | 2 +- storage/src/sqlite.rs | 7 ++ 17 files changed, 372 insertions(+), 6 deletions(-) create mode 100644 conversations/src/ffi/mod.rs create mode 100644 conversations/src/ffi/utils.rs create mode 100644 conversations/src/storage/db.rs create mode 100644 conversations/src/storage/migrations.rs create mode 100644 conversations/src/storage/migrations/001_initial_schema.sql create mode 100644 conversations/src/storage/mod.rs create mode 100644 conversations/src/storage/types.rs diff --git a/Cargo.lock b/Cargo.lock index bcdb99d..4cd52f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,8 +501,11 @@ dependencies = [ "prost", "rand_core", "safer-ffi", + "storage", + "tempfile", "thiserror", "x25519-dalek", + "zeroize", ] [[package]] diff --git a/conversations/Cargo.toml b/conversations/Cargo.toml index cdb02a9..4ea9408 100644 --- a/conversations/Cargo.toml +++ b/conversations/Cargo.toml @@ -18,3 +18,8 @@ rand_core = { version = "0.6" } safer-ffi = "0.1.13" thiserror = "2.0.17" x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } +storage = { path = "../storage" } +zeroize = { version = "1.8.2", features = ["derive"] } + +[dev-dependencies] +tempfile = "3" diff --git a/conversations/src/api.rs b/conversations/src/api.rs index bd1e300..75319f9 100644 --- a/conversations/src/api.rs +++ b/conversations/src/api.rs @@ -13,9 +13,12 @@ use safer_ffi::{ prelude::{c_slice, repr_c}, }; +use storage::StorageConfig; + use crate::{ context::{Context, Introduction}, errors::ChatError, + ffi::utils::CResult, types::ContentData, }; @@ -54,6 +57,41 @@ pub fn create_context(name: repr_c::String) -> repr_c::Box { Box::new(ContextHandle(Context::new_with_name(&*name))).into() } +/// Creates a new libchat Context with file-based persistent storage. +/// +/// The identity will be loaded from storage if it exists, or created and saved if not. +/// +/// # Parameters +/// - name: Friendly name for the identity (used if creating new identity) +/// - db_path: Path to the SQLite database file +/// - db_secret: Secret key for encrypting the database +/// +/// # Returns +/// CResult with context handle on success, or error string on failure. +/// On success, the context handle must be freed with `destroy_context()` after usage. +/// On error, the error string must be freed with `destroy_string()` after usage. +#[ffi_export] +pub fn create_context_with_storage( + name: repr_c::String, + db_path: repr_c::String, + db_secret: repr_c::String, +) -> CResult, repr_c::String> { + let config = StorageConfig::Encrypted { + path: db_path.to_string(), + key: db_secret.to_string(), + }; + match Context::open(&*name, config) { + Ok(ctx) => CResult { + ok: Some(Box::new(ContextHandle(ctx)).into()), + err: None, + }, + Err(e) => CResult { + ok: None, + err: Some(e.to_string().into()), + }, + } +} + /// Returns the friendly name of the contexts installation. /// #[ffi_export] diff --git a/conversations/src/context.rs b/conversations/src/context.rs index 2b36f68..bec37a5 100644 --- a/conversations/src/context.rs +++ b/conversations/src/context.rs @@ -1,11 +1,14 @@ use std::rc::Rc; +use storage::StorageConfig; + use crate::{ conversation::{ConversationId, ConversationStore, Convo, Id}, errors::ChatError, identity::Identity, inbox::Inbox, proto::{EncryptedPayload, EnvelopeV1, Message}, + storage::ChatStorage, types::{AddressedEnvelope, ContentData}, }; @@ -18,17 +21,44 @@ pub struct Context { _identity: Rc, store: ConversationStore, inbox: Inbox, + #[allow(dead_code)] // Will be used for conversation persistence + storage: ChatStorage, } impl Context { - pub fn new_with_name(name: impl Into) -> Self { - let identity = Rc::new(Identity::new(name)); - let inbox = Inbox::new(Rc::clone(&identity)); // - Self { + /// 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)?; + let name = name.into(); + + // Load or create identity + let identity = if let Some(identity) = storage.load_identity()? { + identity + } else { + let identity = Identity::new(&name); + storage.save_identity(&identity)?; + identity + }; + + let identity = Rc::new(identity); + let inbox = Inbox::new(Rc::clone(&identity)); + + Ok(Self { _identity: identity, store: ConversationStore::new(), inbox, - } + 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 { @@ -195,4 +225,31 @@ mod tests { send_and_verify(&mut saro, &mut raya, &saro_convo_id, &content); } } + + #[test] + fn identity_persistence() { + // Use file-based storage to test real persistence + let dir = tempfile::tempdir().unwrap(); + let db_path = dir + .path() + .join("test_identity.db") + .to_string_lossy() + .to_string(); + let config = StorageConfig::File(db_path); + + // Create context - this should create and save a new identity + let ctx1 = Context::open("alice", config.clone()).unwrap(); + let pubkey1 = ctx1._identity.public_key(); + let name1 = ctx1.installation_name().to_string(); + + // Drop and reopen - should load the same identity + drop(ctx1); + let ctx2 = Context::open("alice", config).unwrap(); + let pubkey2 = ctx2._identity.public_key(); + let name2 = ctx2.installation_name().to_string(); + + // Identity should be the same + assert_eq!(pubkey1, pubkey2, "public key should persist"); + assert_eq!(name1, name2, "name should persist"); + } } diff --git a/conversations/src/errors.rs b/conversations/src/errors.rs index d551960..f47004c 100644 --- a/conversations/src/errors.rs +++ b/conversations/src/errors.rs @@ -1,5 +1,7 @@ pub use thiserror::Error; +use storage::StorageError; + #[derive(Error, Debug)] pub enum ChatError { #[error("protocol error: {0:?}")] @@ -20,6 +22,8 @@ pub enum ChatError { BadParsing(&'static str), #[error("convo with id: {0} was not found")] NoConvo(String), + #[error("storage error: {0}")] + Storage(#[from] StorageError), } #[derive(Error, Debug)] diff --git a/conversations/src/ffi/mod.rs b/conversations/src/ffi/mod.rs new file mode 100644 index 0000000..b5614dd --- /dev/null +++ b/conversations/src/ffi/mod.rs @@ -0,0 +1 @@ +pub mod utils; diff --git a/conversations/src/ffi/utils.rs b/conversations/src/ffi/utils.rs new file mode 100644 index 0000000..7989631 --- /dev/null +++ b/conversations/src/ffi/utils.rs @@ -0,0 +1,8 @@ +use safer_ffi::prelude::*; + +#[derive_ReprC] +#[repr(C)] +pub struct CResult { + pub ok: Option, + pub err: Option, +} diff --git a/conversations/src/identity.rs b/conversations/src/identity.rs index 76c2700..8ca27be 100644 --- a/conversations/src/identity.rs +++ b/conversations/src/identity.rs @@ -24,6 +24,13 @@ impl Identity { } } + pub fn from_secret(name: impl Into, secret: PrivateKey) -> Self { + Self { + name: name.into(), + secret, + } + } + pub fn public_key(&self) -> PublicKey { PublicKey::from(&self.secret) } diff --git a/conversations/src/lib.rs b/conversations/src/lib.rs index 79d6a5a..5490629 100644 --- a/conversations/src/lib.rs +++ b/conversations/src/lib.rs @@ -3,9 +3,11 @@ mod context; mod conversation; mod crypto; mod errors; +mod ffi; mod identity; mod inbox; mod proto; +mod storage; mod types; mod utils; diff --git a/conversations/src/storage/db.rs b/conversations/src/storage/db.rs new file mode 100644 index 0000000..c855416 --- /dev/null +++ b/conversations/src/storage/db.rs @@ -0,0 +1,111 @@ +//! Chat-specific storage implementation. + +use storage::{RusqliteError, SqliteDb, StorageConfig, StorageError, params}; +use zeroize::Zeroize; + +use super::migrations; +use super::types::IdentityRecord; +use crate::identity::Identity; + +/// Chat-specific storage operations. +/// +/// 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, +} + +impl ChatStorage { + /// Creates a new ChatStorage with the given configuration. + pub fn new(config: StorageConfig) -> Result { + let db = SqliteDb::new(config)?; + Self::run_migrations(db) + } + + /// Applies all migrations and returns the storage instance. + fn run_migrations(mut db: SqliteDb) -> Result { + migrations::apply_migrations(db.connection_mut())?; + Ok(Self { db }) + } + + // ==================== Identity Operations ==================== + + /// Saves the identity (secret key). + /// + /// Note: The secret key bytes are explicitly zeroized after use to minimize + /// the time sensitive data remains in stack memory. + pub fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError> { + let mut secret_bytes = identity.secret().DANGER_to_bytes(); + let result = self.db.connection().execute( + "INSERT OR REPLACE INTO identity (id, name, secret_key) VALUES (1, ?1, ?2)", + params![identity.get_name(), secret_bytes.as_slice()], + ); + secret_bytes.zeroize(); + result?; + Ok(()) + } + + /// Loads the identity if it exists. + /// + /// Note: Secret key bytes are zeroized after being copied into IdentityRecord, + /// which handles its own zeroization via ZeroizeOnDrop. + pub fn load_identity(&self) -> Result, StorageError> { + let mut stmt = self + .db + .connection() + .prepare("SELECT name, secret_key FROM identity WHERE id = 1")?; + + let result = stmt.query_row([], |row| { + let name: String = row.get(0)?; + let secret_key: Vec = row.get(1)?; + Ok((name, secret_key)) + }); + + match result { + Ok((name, mut secret_key_vec)) => { + let bytes: Result<[u8; 32], _> = secret_key_vec.as_slice().try_into(); + let bytes = match bytes { + Ok(b) => b, + Err(_) => { + secret_key_vec.zeroize(); + return Err(StorageError::InvalidData( + "Invalid secret key length".into(), + )); + } + }; + secret_key_vec.zeroize(); + let record = IdentityRecord { + name, + secret_key: bytes, + }; + Ok(Some(Identity::from(record))) + } + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_identity_roundtrip() { + let mut storage = ChatStorage::new(StorageConfig::InMemory).unwrap(); + + // Initially no identity + assert!(storage.load_identity().unwrap().is_none()); + + // Save identity + let identity = Identity::new("default"); + let pubkey = identity.public_key(); + storage.save_identity(&identity).unwrap(); + + // Load identity + let loaded = storage.load_identity().unwrap().unwrap(); + assert_eq!(loaded.public_key(), pubkey); + } +} diff --git a/conversations/src/storage/migrations.rs b/conversations/src/storage/migrations.rs new file mode 100644 index 0000000..014bb96 --- /dev/null +++ b/conversations/src/storage/migrations.rs @@ -0,0 +1,46 @@ +//! Database migrations module. +//! +//! SQL migrations are embedded at compile time and applied in order. +//! Each migration is applied atomically within a transaction. + +use storage::{Connection, StorageError}; + +/// Embeds and returns all migration SQL files in order. +pub fn get_migrations() -> Vec<(&'static str, &'static str)> { + vec![( + "001_initial_schema", + include_str!("migrations/001_initial_schema.sql"), + )] +} + +/// Applies all migrations to the database. +/// +/// Uses a simple version tracking table to avoid re-running migrations. +pub fn apply_migrations(conn: &mut Connection) -> Result<(), StorageError> { + // Create migrations tracking table if it doesn't exist + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS _migrations ( + name TEXT PRIMARY KEY, + applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + );", + )?; + + for (name, sql) in get_migrations() { + // Check if migration already applied + let already_applied: bool = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM _migrations WHERE name = ?1)", + [name], + |row| row.get(0), + )?; + + if !already_applied { + // Apply migration and record it atomically in a transaction + let tx = conn.transaction()?; + tx.execute_batch(sql)?; + tx.execute("INSERT INTO _migrations (name) VALUES (?1)", [name])?; + tx.commit()?; + } + } + + Ok(()) +} diff --git a/conversations/src/storage/migrations/001_initial_schema.sql b/conversations/src/storage/migrations/001_initial_schema.sql new file mode 100644 index 0000000..5a97bfe --- /dev/null +++ b/conversations/src/storage/migrations/001_initial_schema.sql @@ -0,0 +1,9 @@ +-- Initial schema for chat storage +-- Migration: 001_initial_schema + +-- Identity table (single row) +CREATE TABLE IF NOT EXISTS identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + name TEXT NOT NULL, + secret_key BLOB NOT NULL +); diff --git a/conversations/src/storage/mod.rs b/conversations/src/storage/mod.rs new file mode 100644 index 0000000..9364aeb --- /dev/null +++ b/conversations/src/storage/mod.rs @@ -0,0 +1,7 @@ +//! Storage module for persisting chat state. + +mod db; +mod migrations; +pub(crate) mod types; + +pub(crate) use db::ChatStorage; diff --git a/conversations/src/storage/types.rs b/conversations/src/storage/types.rs new file mode 100644 index 0000000..c34f9be --- /dev/null +++ b/conversations/src/storage/types.rs @@ -0,0 +1,57 @@ +//! Storage record types for serialization/deserialization. + +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::crypto::PrivateKey; +use crate::identity::Identity; + +/// Record for storing identity (secret key). +/// Implements ZeroizeOnDrop to securely clear secret key from memory. +#[derive(Debug, Zeroize, ZeroizeOnDrop)] +pub struct IdentityRecord { + /// The identity name. + pub name: String, + /// The secret key bytes (32 bytes). + pub secret_key: [u8; 32], +} + +impl From for Identity { + fn from(record: IdentityRecord) -> Self { + let secret = PrivateKey::from(record.secret_key); + Identity::from_secret(record.name.clone(), secret) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_identity_record_zeroize() { + let secret_key = [0xAB_u8; 32]; + let mut record = IdentityRecord { + name: "test".to_string(), + secret_key, + }; + + // Get a pointer to the secret key before zeroizing + let ptr = record.secret_key.as_ptr(); + + // Manually zeroize (simulates what ZeroizeOnDrop does) + record.zeroize(); + + // Verify the memory is zeroed + // SAFETY: ptr still points to valid memory within record + unsafe { + let slice = std::slice::from_raw_parts(ptr, 32); + assert!(slice.iter().all(|&b| b == 0), "secret_key should be zeroed"); + } + + // Also verify via the struct field + assert!( + record.secret_key.iter().all(|&b| b == 0), + "secret_key field should be zeroed" + ); + assert!(record.name.is_empty(), "name should be cleared"); + } +} diff --git a/storage/src/errors.rs b/storage/src/errors.rs index 1410f85..9d65d64 100644 --- a/storage/src/errors.rs +++ b/storage/src/errors.rs @@ -26,6 +26,10 @@ pub enum StorageError { /// Transaction error. #[error("transaction error: {0}")] Transaction(String), + + /// Invalid data error. + #[error("invalid data: {0}")] + InvalidData(String), } impl From for StorageError { diff --git a/storage/src/lib.rs b/storage/src/lib.rs index bacc9b6..9240dc2 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -12,4 +12,4 @@ pub use errors::StorageError; pub use sqlite::{SqliteDb, StorageConfig}; // Re-export rusqlite types that domain crates will need -pub use rusqlite::{Error as RusqliteError, Transaction, params}; +pub use rusqlite::{Connection, Error as RusqliteError, Transaction, params}; diff --git a/storage/src/sqlite.rs b/storage/src/sqlite.rs index 4d42e9d..c6b00ab 100644 --- a/storage/src/sqlite.rs +++ b/storage/src/sqlite.rs @@ -66,6 +66,13 @@ impl SqliteDb { &self.conn } + /// Returns a mutable reference to the underlying connection. + /// + /// Use this for operations that require mutable access, such as transactions. + pub fn connection_mut(&mut self) -> &mut Connection { + &mut self.conn + } + /// Begins a transaction. pub fn transaction(&mut self) -> Result, StorageError> { Ok(self.conn.transaction()?) From 9a94f9a6d6cb2f4f898ecfb9e5289942e3fcb135 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:21:00 -0700 Subject: [PATCH 2/3] Flatten Repos (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * move to “crates” style folder * Update workspace * clear crate names * Rename crate folders based on feedback * Use workspace dependencies instead of paths * Move updated files from core --- Cargo.lock | 7 +++++++ Cargo.toml | 12 +++++++----- core/README.md | 7 +++++++ .../conversations}/Cargo.toml | 0 .../conversations}/src/api.rs | 0 .../conversations}/src/context.rs | 0 .../conversations}/src/conversation.rs | 0 .../src/conversation/group_test.rs | 0 .../src/conversation/privatev1.rs | 0 .../conversations}/src/crypto.rs | 0 .../conversations}/src/errors.rs | 0 .../conversations}/src/ffi/mod.rs | 0 .../conversations}/src/ffi/utils.rs | 0 .../conversations}/src/identity.rs | 0 .../conversations}/src/inbox.rs | 0 .../conversations}/src/inbox/handler.rs | 0 .../conversations}/src/inbox/handshake.rs | 0 .../conversations}/src/inbox/introduction.rs | 0 .../conversations}/src/lib.rs | 2 ++ .../conversations}/src/proto.rs | 0 .../conversations}/src/storage/db.rs | 0 .../conversations}/src/storage/migrations.rs | 0 .../storage/migrations/001_initial_schema.sql | 0 .../conversations}/src/storage/mod.rs | 0 .../conversations}/src/storage/types.rs | 0 .../conversations}/src/types.rs | 0 .../conversations}/src/utils.rs | 0 {crypto => core/crypto}/Cargo.toml | 0 {crypto => core/crypto}/src/keys.rs | 0 {crypto => core/crypto}/src/lib.rs | 0 {crypto => core/crypto}/src/x3dh.rs | 0 {crypto => core/crypto}/src/xeddsa_sign.rs | 0 .../double-ratchets}/Cargo.toml | 0 .../double-ratchets}/README.md | 0 .../examples/double_ratchet_basic.rs | 0 .../examples/out_of_order_demo.rs | 0 .../examples/serialization_demo.rs | 0 .../double-ratchets}/examples/storage_demo.rs | 0 .../ffi-nim-example/ffi_nim_example.nimble | 0 .../ffi-nim-example/src/ffi_nim_example.nim | 0 .../double-ratchets}/src/aead.rs | 0 .../src/bin/generate-headers.rs | 0 .../double-ratchets}/src/errors.rs | 0 .../double-ratchets}/src/ffi/doubleratchet.rs | 0 .../double-ratchets}/src/ffi/key.rs | 0 .../double-ratchets}/src/ffi/mod.rs | 0 .../double-ratchets}/src/ffi/utils.rs | 0 .../double-ratchets}/src/hkdf.rs | 0 .../double-ratchets}/src/keypair.rs | 0 .../double-ratchets}/src/lib.rs | 0 .../double-ratchets}/src/reader.rs | 0 .../double-ratchets}/src/state.rs | 0 .../double-ratchets}/src/storage/db.rs | 0 .../double-ratchets}/src/storage/errors.rs | 0 .../double-ratchets}/src/storage/mod.rs | 0 .../double-ratchets}/src/storage/session.rs | 0 .../double-ratchets}/src/storage/types.rs | 0 .../double-ratchets}/src/types.rs | 0 {storage => core/storage}/Cargo.toml | 0 {storage => core/storage}/src/errors.rs | 0 {storage => core/storage}/src/lib.rs | 0 {storage => core/storage}/src/sqlite.rs | 0 crates/client/Cargo.toml | 10 ++++++++++ crates/client/src/client.rs | 18 ++++++++++++++++++ crates/client/src/lib.rs | 3 +++ 65 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 core/README.md rename {conversations => core/conversations}/Cargo.toml (100%) rename {conversations => core/conversations}/src/api.rs (100%) rename {conversations => core/conversations}/src/context.rs (100%) rename {conversations => core/conversations}/src/conversation.rs (100%) rename {conversations => core/conversations}/src/conversation/group_test.rs (100%) rename {conversations => core/conversations}/src/conversation/privatev1.rs (100%) rename {conversations => core/conversations}/src/crypto.rs (100%) rename {conversations => core/conversations}/src/errors.rs (100%) rename {conversations => core/conversations}/src/ffi/mod.rs (100%) rename {conversations => core/conversations}/src/ffi/utils.rs (100%) rename {conversations => core/conversations}/src/identity.rs (100%) rename {conversations => core/conversations}/src/inbox.rs (100%) rename {conversations => core/conversations}/src/inbox/handler.rs (100%) rename {conversations => core/conversations}/src/inbox/handshake.rs (100%) rename {conversations => core/conversations}/src/inbox/introduction.rs (100%) rename {conversations => core/conversations}/src/lib.rs (96%) rename {conversations => core/conversations}/src/proto.rs (100%) rename {conversations => core/conversations}/src/storage/db.rs (100%) rename {conversations => core/conversations}/src/storage/migrations.rs (100%) rename {conversations => core/conversations}/src/storage/migrations/001_initial_schema.sql (100%) rename {conversations => core/conversations}/src/storage/mod.rs (100%) rename {conversations => core/conversations}/src/storage/types.rs (100%) rename {conversations => core/conversations}/src/types.rs (100%) rename {conversations => core/conversations}/src/utils.rs (100%) rename {crypto => core/crypto}/Cargo.toml (100%) rename {crypto => core/crypto}/src/keys.rs (100%) rename {crypto => core/crypto}/src/lib.rs (100%) rename {crypto => core/crypto}/src/x3dh.rs (100%) rename {crypto => core/crypto}/src/xeddsa_sign.rs (100%) rename {double-ratchets => core/double-ratchets}/Cargo.toml (100%) rename {double-ratchets => core/double-ratchets}/README.md (100%) rename {double-ratchets => core/double-ratchets}/examples/double_ratchet_basic.rs (100%) rename {double-ratchets => core/double-ratchets}/examples/out_of_order_demo.rs (100%) rename {double-ratchets => core/double-ratchets}/examples/serialization_demo.rs (100%) rename {double-ratchets => core/double-ratchets}/examples/storage_demo.rs (100%) rename {double-ratchets => core/double-ratchets}/ffi-nim-example/ffi_nim_example.nimble (100%) rename {double-ratchets => core/double-ratchets}/ffi-nim-example/src/ffi_nim_example.nim (100%) rename {double-ratchets => core/double-ratchets}/src/aead.rs (100%) rename {double-ratchets => core/double-ratchets}/src/bin/generate-headers.rs (100%) rename {double-ratchets => core/double-ratchets}/src/errors.rs (100%) rename {double-ratchets => core/double-ratchets}/src/ffi/doubleratchet.rs (100%) rename {double-ratchets => core/double-ratchets}/src/ffi/key.rs (100%) rename {double-ratchets => core/double-ratchets}/src/ffi/mod.rs (100%) rename {double-ratchets => core/double-ratchets}/src/ffi/utils.rs (100%) rename {double-ratchets => core/double-ratchets}/src/hkdf.rs (100%) rename {double-ratchets => core/double-ratchets}/src/keypair.rs (100%) rename {double-ratchets => core/double-ratchets}/src/lib.rs (100%) rename {double-ratchets => core/double-ratchets}/src/reader.rs (100%) rename {double-ratchets => core/double-ratchets}/src/state.rs (100%) rename {double-ratchets => core/double-ratchets}/src/storage/db.rs (100%) rename {double-ratchets => core/double-ratchets}/src/storage/errors.rs (100%) rename {double-ratchets => core/double-ratchets}/src/storage/mod.rs (100%) rename {double-ratchets => core/double-ratchets}/src/storage/session.rs (100%) rename {double-ratchets => core/double-ratchets}/src/storage/types.rs (100%) rename {double-ratchets => core/double-ratchets}/src/types.rs (100%) rename {storage => core/storage}/Cargo.toml (100%) rename {storage => core/storage}/src/errors.rs (100%) rename {storage => core/storage}/src/lib.rs (100%) rename {storage => core/storage}/src/sqlite.rs (100%) create mode 100644 crates/client/Cargo.toml create mode 100644 crates/client/src/client.rs create mode 100644 crates/client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4cd52f0..d8fbc00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,13 @@ dependencies = [ "zeroize", ] +[[package]] +name = "client" +version = "0.1.0" +dependencies = [ + "libchat", +] + [[package]] name = "const-oid" version = "0.9.6" diff --git a/Cargo.toml b/Cargo.toml index 274eb12..ca37bad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,15 +3,17 @@ resolver = "3" members = [ - "conversations", - "crypto", - "double-ratchets", - "storage", + "core/conversations", + "core/crypto", + "core/double-ratchets", + "core/storage", + "crates/client", ] [workspace.dependencies] blake2 = "0.10" -storage = { path = "storage" } +libchat = { path = "core/conversations" } +storage = { path = "core/storage" } # Panicking across FFI boundaries is UB; abort is the correct strategy for a # C FFI library. diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..0115b00 --- /dev/null +++ b/core/README.md @@ -0,0 +1,7 @@ +# Core + +Crates in this directory will one day be separated into a separate shared repository. + +They could be moved now, but it's desirable to have a monorepo setup at this time. + +These crates MUST not depend on any code outside of this folder. \ No newline at end of file diff --git a/conversations/Cargo.toml b/core/conversations/Cargo.toml similarity index 100% rename from conversations/Cargo.toml rename to core/conversations/Cargo.toml diff --git a/conversations/src/api.rs b/core/conversations/src/api.rs similarity index 100% rename from conversations/src/api.rs rename to core/conversations/src/api.rs diff --git a/conversations/src/context.rs b/core/conversations/src/context.rs similarity index 100% rename from conversations/src/context.rs rename to core/conversations/src/context.rs diff --git a/conversations/src/conversation.rs b/core/conversations/src/conversation.rs similarity index 100% rename from conversations/src/conversation.rs rename to core/conversations/src/conversation.rs diff --git a/conversations/src/conversation/group_test.rs b/core/conversations/src/conversation/group_test.rs similarity index 100% rename from conversations/src/conversation/group_test.rs rename to core/conversations/src/conversation/group_test.rs diff --git a/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs similarity index 100% rename from conversations/src/conversation/privatev1.rs rename to core/conversations/src/conversation/privatev1.rs diff --git a/conversations/src/crypto.rs b/core/conversations/src/crypto.rs similarity index 100% rename from conversations/src/crypto.rs rename to core/conversations/src/crypto.rs diff --git a/conversations/src/errors.rs b/core/conversations/src/errors.rs similarity index 100% rename from conversations/src/errors.rs rename to core/conversations/src/errors.rs diff --git a/conversations/src/ffi/mod.rs b/core/conversations/src/ffi/mod.rs similarity index 100% rename from conversations/src/ffi/mod.rs rename to core/conversations/src/ffi/mod.rs diff --git a/conversations/src/ffi/utils.rs b/core/conversations/src/ffi/utils.rs similarity index 100% rename from conversations/src/ffi/utils.rs rename to core/conversations/src/ffi/utils.rs diff --git a/conversations/src/identity.rs b/core/conversations/src/identity.rs similarity index 100% rename from conversations/src/identity.rs rename to core/conversations/src/identity.rs diff --git a/conversations/src/inbox.rs b/core/conversations/src/inbox.rs similarity index 100% rename from conversations/src/inbox.rs rename to core/conversations/src/inbox.rs diff --git a/conversations/src/inbox/handler.rs b/core/conversations/src/inbox/handler.rs similarity index 100% rename from conversations/src/inbox/handler.rs rename to core/conversations/src/inbox/handler.rs diff --git a/conversations/src/inbox/handshake.rs b/core/conversations/src/inbox/handshake.rs similarity index 100% rename from conversations/src/inbox/handshake.rs rename to core/conversations/src/inbox/handshake.rs diff --git a/conversations/src/inbox/introduction.rs b/core/conversations/src/inbox/introduction.rs similarity index 100% rename from conversations/src/inbox/introduction.rs rename to core/conversations/src/inbox/introduction.rs diff --git a/conversations/src/lib.rs b/core/conversations/src/lib.rs similarity index 96% rename from conversations/src/lib.rs rename to core/conversations/src/lib.rs index 5490629..d81eb91 100644 --- a/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -12,6 +12,8 @@ mod types; mod utils; pub use api::*; +pub use context::{Context, Introduction}; +pub use errors::ChatError; #[cfg(test)] mod tests { diff --git a/conversations/src/proto.rs b/core/conversations/src/proto.rs similarity index 100% rename from conversations/src/proto.rs rename to core/conversations/src/proto.rs diff --git a/conversations/src/storage/db.rs b/core/conversations/src/storage/db.rs similarity index 100% rename from conversations/src/storage/db.rs rename to core/conversations/src/storage/db.rs diff --git a/conversations/src/storage/migrations.rs b/core/conversations/src/storage/migrations.rs similarity index 100% rename from conversations/src/storage/migrations.rs rename to core/conversations/src/storage/migrations.rs diff --git a/conversations/src/storage/migrations/001_initial_schema.sql b/core/conversations/src/storage/migrations/001_initial_schema.sql similarity index 100% rename from conversations/src/storage/migrations/001_initial_schema.sql rename to core/conversations/src/storage/migrations/001_initial_schema.sql diff --git a/conversations/src/storage/mod.rs b/core/conversations/src/storage/mod.rs similarity index 100% rename from conversations/src/storage/mod.rs rename to core/conversations/src/storage/mod.rs diff --git a/conversations/src/storage/types.rs b/core/conversations/src/storage/types.rs similarity index 100% rename from conversations/src/storage/types.rs rename to core/conversations/src/storage/types.rs diff --git a/conversations/src/types.rs b/core/conversations/src/types.rs similarity index 100% rename from conversations/src/types.rs rename to core/conversations/src/types.rs diff --git a/conversations/src/utils.rs b/core/conversations/src/utils.rs similarity index 100% rename from conversations/src/utils.rs rename to core/conversations/src/utils.rs diff --git a/crypto/Cargo.toml b/core/crypto/Cargo.toml similarity index 100% rename from crypto/Cargo.toml rename to core/crypto/Cargo.toml diff --git a/crypto/src/keys.rs b/core/crypto/src/keys.rs similarity index 100% rename from crypto/src/keys.rs rename to core/crypto/src/keys.rs diff --git a/crypto/src/lib.rs b/core/crypto/src/lib.rs similarity index 100% rename from crypto/src/lib.rs rename to core/crypto/src/lib.rs diff --git a/crypto/src/x3dh.rs b/core/crypto/src/x3dh.rs similarity index 100% rename from crypto/src/x3dh.rs rename to core/crypto/src/x3dh.rs diff --git a/crypto/src/xeddsa_sign.rs b/core/crypto/src/xeddsa_sign.rs similarity index 100% rename from crypto/src/xeddsa_sign.rs rename to core/crypto/src/xeddsa_sign.rs diff --git a/double-ratchets/Cargo.toml b/core/double-ratchets/Cargo.toml similarity index 100% rename from double-ratchets/Cargo.toml rename to core/double-ratchets/Cargo.toml diff --git a/double-ratchets/README.md b/core/double-ratchets/README.md similarity index 100% rename from double-ratchets/README.md rename to core/double-ratchets/README.md diff --git a/double-ratchets/examples/double_ratchet_basic.rs b/core/double-ratchets/examples/double_ratchet_basic.rs similarity index 100% rename from double-ratchets/examples/double_ratchet_basic.rs rename to core/double-ratchets/examples/double_ratchet_basic.rs diff --git a/double-ratchets/examples/out_of_order_demo.rs b/core/double-ratchets/examples/out_of_order_demo.rs similarity index 100% rename from double-ratchets/examples/out_of_order_demo.rs rename to core/double-ratchets/examples/out_of_order_demo.rs diff --git a/double-ratchets/examples/serialization_demo.rs b/core/double-ratchets/examples/serialization_demo.rs similarity index 100% rename from double-ratchets/examples/serialization_demo.rs rename to core/double-ratchets/examples/serialization_demo.rs diff --git a/double-ratchets/examples/storage_demo.rs b/core/double-ratchets/examples/storage_demo.rs similarity index 100% rename from double-ratchets/examples/storage_demo.rs rename to core/double-ratchets/examples/storage_demo.rs diff --git a/double-ratchets/ffi-nim-example/ffi_nim_example.nimble b/core/double-ratchets/ffi-nim-example/ffi_nim_example.nimble similarity index 100% rename from double-ratchets/ffi-nim-example/ffi_nim_example.nimble rename to core/double-ratchets/ffi-nim-example/ffi_nim_example.nimble diff --git a/double-ratchets/ffi-nim-example/src/ffi_nim_example.nim b/core/double-ratchets/ffi-nim-example/src/ffi_nim_example.nim similarity index 100% rename from double-ratchets/ffi-nim-example/src/ffi_nim_example.nim rename to core/double-ratchets/ffi-nim-example/src/ffi_nim_example.nim diff --git a/double-ratchets/src/aead.rs b/core/double-ratchets/src/aead.rs similarity index 100% rename from double-ratchets/src/aead.rs rename to core/double-ratchets/src/aead.rs diff --git a/double-ratchets/src/bin/generate-headers.rs b/core/double-ratchets/src/bin/generate-headers.rs similarity index 100% rename from double-ratchets/src/bin/generate-headers.rs rename to core/double-ratchets/src/bin/generate-headers.rs diff --git a/double-ratchets/src/errors.rs b/core/double-ratchets/src/errors.rs similarity index 100% rename from double-ratchets/src/errors.rs rename to core/double-ratchets/src/errors.rs diff --git a/double-ratchets/src/ffi/doubleratchet.rs b/core/double-ratchets/src/ffi/doubleratchet.rs similarity index 100% rename from double-ratchets/src/ffi/doubleratchet.rs rename to core/double-ratchets/src/ffi/doubleratchet.rs diff --git a/double-ratchets/src/ffi/key.rs b/core/double-ratchets/src/ffi/key.rs similarity index 100% rename from double-ratchets/src/ffi/key.rs rename to core/double-ratchets/src/ffi/key.rs diff --git a/double-ratchets/src/ffi/mod.rs b/core/double-ratchets/src/ffi/mod.rs similarity index 100% rename from double-ratchets/src/ffi/mod.rs rename to core/double-ratchets/src/ffi/mod.rs diff --git a/double-ratchets/src/ffi/utils.rs b/core/double-ratchets/src/ffi/utils.rs similarity index 100% rename from double-ratchets/src/ffi/utils.rs rename to core/double-ratchets/src/ffi/utils.rs diff --git a/double-ratchets/src/hkdf.rs b/core/double-ratchets/src/hkdf.rs similarity index 100% rename from double-ratchets/src/hkdf.rs rename to core/double-ratchets/src/hkdf.rs diff --git a/double-ratchets/src/keypair.rs b/core/double-ratchets/src/keypair.rs similarity index 100% rename from double-ratchets/src/keypair.rs rename to core/double-ratchets/src/keypair.rs diff --git a/double-ratchets/src/lib.rs b/core/double-ratchets/src/lib.rs similarity index 100% rename from double-ratchets/src/lib.rs rename to core/double-ratchets/src/lib.rs diff --git a/double-ratchets/src/reader.rs b/core/double-ratchets/src/reader.rs similarity index 100% rename from double-ratchets/src/reader.rs rename to core/double-ratchets/src/reader.rs diff --git a/double-ratchets/src/state.rs b/core/double-ratchets/src/state.rs similarity index 100% rename from double-ratchets/src/state.rs rename to core/double-ratchets/src/state.rs diff --git a/double-ratchets/src/storage/db.rs b/core/double-ratchets/src/storage/db.rs similarity index 100% rename from double-ratchets/src/storage/db.rs rename to core/double-ratchets/src/storage/db.rs diff --git a/double-ratchets/src/storage/errors.rs b/core/double-ratchets/src/storage/errors.rs similarity index 100% rename from double-ratchets/src/storage/errors.rs rename to core/double-ratchets/src/storage/errors.rs diff --git a/double-ratchets/src/storage/mod.rs b/core/double-ratchets/src/storage/mod.rs similarity index 100% rename from double-ratchets/src/storage/mod.rs rename to core/double-ratchets/src/storage/mod.rs diff --git a/double-ratchets/src/storage/session.rs b/core/double-ratchets/src/storage/session.rs similarity index 100% rename from double-ratchets/src/storage/session.rs rename to core/double-ratchets/src/storage/session.rs diff --git a/double-ratchets/src/storage/types.rs b/core/double-ratchets/src/storage/types.rs similarity index 100% rename from double-ratchets/src/storage/types.rs rename to core/double-ratchets/src/storage/types.rs diff --git a/double-ratchets/src/types.rs b/core/double-ratchets/src/types.rs similarity index 100% rename from double-ratchets/src/types.rs rename to core/double-ratchets/src/types.rs diff --git a/storage/Cargo.toml b/core/storage/Cargo.toml similarity index 100% rename from storage/Cargo.toml rename to core/storage/Cargo.toml diff --git a/storage/src/errors.rs b/core/storage/src/errors.rs similarity index 100% rename from storage/src/errors.rs rename to core/storage/src/errors.rs diff --git a/storage/src/lib.rs b/core/storage/src/lib.rs similarity index 100% rename from storage/src/lib.rs rename to core/storage/src/lib.rs diff --git a/storage/src/sqlite.rs b/core/storage/src/sqlite.rs similarity index 100% rename from storage/src/sqlite.rs rename to core/storage/src/sqlite.rs diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml new file mode 100644 index 0000000..d3cfb2a --- /dev/null +++ b/crates/client/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "client" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["rlib"] + +[dependencies] +libchat = { workspace = true } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs new file mode 100644 index 0000000..a26908a --- /dev/null +++ b/crates/client/src/client.rs @@ -0,0 +1,18 @@ +use libchat::ChatError; +use libchat::Context; + +pub struct ChatClient { + ctx: Context, +} + +impl ChatClient { + pub fn new(name: impl Into) -> Self { + Self { + ctx: Context::new_with_name(name), + } + } + + pub fn create_bundle(&mut self) -> Result, ChatError> { + self.ctx.create_intro_bundle() + } +} diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs new file mode 100644 index 0000000..008d68a --- /dev/null +++ b/crates/client/src/lib.rs @@ -0,0 +1,3 @@ +mod client; + +pub use client::ChatClient; From 8cddd9ddcfb446deeff96fd5a68d6e4b14927d9f Mon Sep 17 00:00:00 2001 From: kaichao Date: Fri, 27 Mar 2026 10:23:33 +0800 Subject: [PATCH 3/3] refactor: use new styling for conversations crate without mod.rs (#72) * refactor: use new styling for conversations crate without mod.rs * refactor: remove put crate for identity types --- core/conversations/src/api.rs | 38 ------------------- core/conversations/src/ffi/mod.rs | 1 - core/conversations/src/ffi/utils.rs | 8 ---- core/conversations/src/lib.rs | 1 - .../src/{storage/db.rs => storage.rs} | 7 ++-- core/conversations/src/storage/mod.rs | 7 ---- 6 files changed, 4 insertions(+), 58 deletions(-) delete mode 100644 core/conversations/src/ffi/mod.rs delete mode 100644 core/conversations/src/ffi/utils.rs rename core/conversations/src/{storage/db.rs => storage.rs} (97%) delete mode 100644 core/conversations/src/storage/mod.rs diff --git a/core/conversations/src/api.rs b/core/conversations/src/api.rs index 75319f9..bd1e300 100644 --- a/core/conversations/src/api.rs +++ b/core/conversations/src/api.rs @@ -13,12 +13,9 @@ use safer_ffi::{ prelude::{c_slice, repr_c}, }; -use storage::StorageConfig; - use crate::{ context::{Context, Introduction}, errors::ChatError, - ffi::utils::CResult, types::ContentData, }; @@ -57,41 +54,6 @@ pub fn create_context(name: repr_c::String) -> repr_c::Box { Box::new(ContextHandle(Context::new_with_name(&*name))).into() } -/// Creates a new libchat Context with file-based persistent storage. -/// -/// The identity will be loaded from storage if it exists, or created and saved if not. -/// -/// # Parameters -/// - name: Friendly name for the identity (used if creating new identity) -/// - db_path: Path to the SQLite database file -/// - db_secret: Secret key for encrypting the database -/// -/// # Returns -/// CResult with context handle on success, or error string on failure. -/// On success, the context handle must be freed with `destroy_context()` after usage. -/// On error, the error string must be freed with `destroy_string()` after usage. -#[ffi_export] -pub fn create_context_with_storage( - name: repr_c::String, - db_path: repr_c::String, - db_secret: repr_c::String, -) -> CResult, repr_c::String> { - let config = StorageConfig::Encrypted { - path: db_path.to_string(), - key: db_secret.to_string(), - }; - match Context::open(&*name, config) { - Ok(ctx) => CResult { - ok: Some(Box::new(ContextHandle(ctx)).into()), - err: None, - }, - Err(e) => CResult { - ok: None, - err: Some(e.to_string().into()), - }, - } -} - /// Returns the friendly name of the contexts installation. /// #[ffi_export] diff --git a/core/conversations/src/ffi/mod.rs b/core/conversations/src/ffi/mod.rs deleted file mode 100644 index b5614dd..0000000 --- a/core/conversations/src/ffi/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod utils; diff --git a/core/conversations/src/ffi/utils.rs b/core/conversations/src/ffi/utils.rs deleted file mode 100644 index 7989631..0000000 --- a/core/conversations/src/ffi/utils.rs +++ /dev/null @@ -1,8 +0,0 @@ -use safer_ffi::prelude::*; - -#[derive_ReprC] -#[repr(C)] -pub struct CResult { - pub ok: Option, - pub err: Option, -} diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index d81eb91..de0c023 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -3,7 +3,6 @@ mod context; mod conversation; mod crypto; mod errors; -mod ffi; mod identity; mod inbox; mod proto; diff --git a/core/conversations/src/storage/db.rs b/core/conversations/src/storage.rs similarity index 97% rename from core/conversations/src/storage/db.rs rename to core/conversations/src/storage.rs index c855416..c0130a2 100644 --- a/core/conversations/src/storage/db.rs +++ b/core/conversations/src/storage.rs @@ -1,11 +1,12 @@ //! Chat-specific storage implementation. +mod migrations; +mod types; + use storage::{RusqliteError, SqliteDb, StorageConfig, StorageError, params}; use zeroize::Zeroize; -use super::migrations; -use super::types::IdentityRecord; -use crate::identity::Identity; +use crate::{identity::Identity, storage::types::IdentityRecord}; /// Chat-specific storage operations. /// diff --git a/core/conversations/src/storage/mod.rs b/core/conversations/src/storage/mod.rs deleted file mode 100644 index 9364aeb..0000000 --- a/core/conversations/src/storage/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Storage module for persisting chat state. - -mod db; -mod migrations; -pub(crate) mod types; - -pub(crate) use db::ChatStorage;