Keystore
Keystore
The keystore is the foundation of the stack. Its purpose is to securely store all
client keying material on-device so that key material survives process restarts and
is never written to disk in plaintext.On native targets (Linux, macOS, Windows, iOS, Android) the keystore uses
SQLCipher — an encrypted SQLite fork that
applies AES-256-CBC encryption on every 4096-byte page, with each page receiving a
freshly generated random IV and an HMAC-SHA-512 authentication tag.On WASM (browsers and Electron) the backing store is the browser’s
IndexedDB
(via the idb crate). Because IndexedDB has no
native encryption layer, CoreCrypto applies AES-256-GCM value-level encryption using
RustCrypto’s aes-gcm crate before any record
is written.The two central types exported from the keystore crate are:Database— the connection handle (cheap to clone via internalArc)DatabaseKey— a 32-byte AES key, zeroed on drop viazeroize
MlsProvider
MlsProvider
MlsProvider (MlsCryptoProvider internally) bridges the keystore to OpenMLS by
implementing the OpenMlsCryptoProvider trait. OpenMLS calls into this provider for
every cryptographic operation and every key-material read or write.The provider composes two sub-providers:- Crypto —
RustCrypto, providing the underlying primitives (HKDF, AES-GCM, Ed25519, P-256, P-384, X25519) - Key store — the
Databaseinstance described above
MlsProvider directly; it is created internally
when TransactionContext::mls_init is called.CoreCrypto struct
CoreCrypto struct
CoreCrypto (crypto/src/lib.rs) is the top-level owned handle that an application
creates once and keeps for the lifetime of the process.- Cheap to clone — all interior state is stored behind
Arc, so cloning is O(1) reference-count increments. - Protocol agnostic — holds both an optional MLS
Sessionand an optionalProteusCentral. Either or both can be initialised at runtime. - Read-only by design —
CoreCryptoitself exposes no mutating methods. All mutations must go through aTransactionContext(see below).
Database handle:TransactionContext
TransactionContext
All state-mutating operations in CoreCrypto — creating conversations, adding
members, generating key packages, initialising Proteus — must be performed
through a Once
TransactionContext.A transaction is opened with CoreCrypto::new_transaction() and closed with
either finish() (commit) or abort() (rollback). The context buffers all
keystore writes in memory; they are flushed to the underlying database in a
single atomic operation when finish() is called.finish() or abort() has been called, the context transitions to an
Invalid state and further calls return Error::InvalidTransactionContext.
This design ensures that UniFFI bindings (which cannot enforce Rust ownership
rules across the FFI boundary) still get deterministic transaction semantics.The TransactionContext holds:- A shared reference to the
mls_sessionArc<RwLock<Option<Session>>> - A local
mls_groupsGroupStore<MlsConversation>for in-flight changes - A shared reference to
proteus_central(feature-gated)
Conceptual model: Central / Client / Member / Conversation
Conceptual model: Central / Client / Member / Conversation
CoreCrypto uses four logical entities that map directly onto MLS RFC 9420 terminology:
On the MLS side,
| CoreCrypto term | MLS equivalent | Description |
|---|---|---|
| Central (Session) | Client | The entry point once the library is initialised. Owns local keying material. One per user-device. |
| Client | Client | The local device identity that can produce key material (key packages, commits). |
| Member | Member | A remote participant in a conversation. Cannot produce local keying material. |
| Conversation | Group | An MLS group (or a set of Proteus pairwise sessions) in which the local Client participates. |
Session (previously MlsCentral) acts as the Central — it
finds or creates the local client identity and restores persisted conversation
groups on startup.CoreCryptoFFI
CoreCryptoFFI
The FFI layer exposes
CoreCrypto to non-Rust consumers without exposing any Rust
types directly.Two separate strategies are used depending on the target:iOS and Android use UniFFI to generate
Kotlin and Swift bindings from a UDL (UniFFI Definition Language) file. UniFFI
generates type-safe wrappers, async support, and error enumerations automatically.Web / Electron targets compile CoreCrypto to WebAssembly. The
wasm-bindgen crate generates a
JavaScript/TypeScript package from #[wasm_bindgen] annotations on the Rust types.
Async functions are exposed as Promise-returning methods.Both strategies present the same logical API surface — the same conversation
lifecycle, the same error codes — so application-level code can stay platform-
agnostic with thin adapter layers.The UniFFI path (
#[cfg(not(target_os = "unknown"))]) and the wasm-bindgen path
(#[cfg(target_os = "unknown")]) share the same core Rust logic but differ in how
async traits are expressed: native targets use async_trait with Send bounds,
while WASM targets use async_trait(?Send) because browser APIs are inherently
single-threaded and values cannot be moved across thread boundaries.