Skip to main content
The CoreCrypto keystore is a purpose-built encrypted key-value store for cryptographic material. It provides the persistence layer for MLS group state, Proteus sessions, key packages, credentials, and all other long-lived keying objects.

Goals

  • Persistent storage that survives process restarts
  • Encryption at rest for all persisted keying material
  • Compatibility with OpenMLS and Proteus storage traits
  • Cross-platform: native (Linux/macOS/Windows), iOS, Android, and WebAssembly

Architecture overview

Native / iOS / Android             WASM (Browser / Electron)
──────────────────────             ──────────────────────────
Keystore Entities                  Keystore Entities
     │                                  │
     ▼                                  ▼
  rusqlite                        AES-256-GCM encryption
     │                                  │
     ▼                                  ▼
  SQLCipher                          Rexie (idb)
  AES-256-CBC per-page                  │
  PBKDF2 key derivation                 ▼
     │                              IndexedDB

  File on disk

Core types

DatabaseKey

DatabaseKey is a 32-byte AES encryption key. It is Zeroize + ZeroizeOnDrop, meaning the memory is zeroed when the value is dropped:
pub struct DatabaseKey([u8; 32]);

impl DatabaseKey {
    /// Generate a cryptographically random key
    pub fn generate() -> DatabaseKey { ... }
}

impl TryFrom<&[u8]> for DatabaseKey { ... } // must be exactly 32 bytes
The Debug implementation prints only the first 10 bytes of the SHA-256 hash to prevent accidental key leakage in logs.

Database

Database is the connection handle. It is cheap to clone (all state is behind Arc):
pub struct Database {
    conn: Arc<Mutex<Option<KeystoreDatabaseConnection>>>,
    transaction: Arc<Mutex<Option<KeystoreTransaction>>>,
    transaction_semaphore: Arc<Semaphore>,
}
Only one transaction can be active at a time (enforced by the Semaphore). All mutating operations require an active transaction — calling save or remove without one returns CryptoKeystoreError::MutatingOperationWithoutTransaction.

ConnectionType

pub enum ConnectionType<'a> {
    Persistent(&'a str),  // file path
    InMemory,             // for tests and ephemeral usage
}

Opening a database

use core_crypto_keystore::{ConnectionType, Database, DatabaseKey};

// Persistent database
let key = DatabaseKey::generate();
let db = Database::open(ConnectionType::Persistent("/path/to/cc.db"), &key).await?;

// In-memory database (useful for tests)
let db = Database::open(ConnectionType::InMemory, &key).await?;

Platform implementations

The native backend delegates entirely to SQLCipher, an encrypted fork of SQLite.Encryption details:
  • Backing store: encrypted SQLite file
  • Page size: 4096 bytes (SQLCipher default)
  • Cipher: AES-256-CBC, applied per-page
  • Per-page IV: freshly generated on every page write, appended to the page
  • Page authentication: HMAC-SHA-512 tag, appended to the page
  • Key derivation: PBKDF2-HMAC-SHA512 from the provided passphrase
    • The 16-byte KDF salt is stored in the first 16 bytes of the database file
  • Crypto provider: OpenSSL (default), configurable to NSS, LibTomCrypt, or Security.framework on Apple platforms
iOS-specific note: iOS inspects the first 16–32 bytes of SQLite files to identify them as databases, which is important for background tasks (encrypted push notifications, Watch App access). The SQLCipher page salt placement is compatible with this requirement.
// Native usage — identical API to WASM
let key = DatabaseKey::generate();
let db = Database::open(
    ConnectionType::Persistent("/var/mobile/Containers/Data/Application/.../cc.db"),
    &key,
).await?;
The WASM keystore’s AES-256-GCM value-level encryption implementation has not been independently audited. Paper cuts and edge cases are possible. For high-assurance deployments that run in a browser, consider this when performing a threat model.

Key derivation from a passphrase

When using the SQLCipher backend, the DatabaseKey bytes are passed directly as the SQLCipher key material. SQLCipher internally derives the AES-256 page-encryption key via PBKDF2-HMAC-SHA512 and stores the 16-byte salt in the first 16 bytes of the file. If your application derives the DatabaseKey from a user-supplied passphrase, do so before calling Database::open:
use sha2::Sha256;
use pbkdf2::pbkdf2_hmac;
use core_crypto_keystore::DatabaseKey;

let mut raw_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(passphrase, salt, 210_000, &mut raw_key);
let db_key = DatabaseKey::try_from(raw_key.as_slice())?;

Key rotation

let new_key = DatabaseKey::generate();
db.update_key(&new_key).await?;
On SQLCipher this calls PRAGMA rekey which re-encrypts the entire database in-place.

Migrating from passphrase-string keys to DatabaseKey

Early versions of CoreCrypto accepted raw passphrase strings directly. The migrate_database_key_type_to_bytes helper migrates an existing database opened with a string passphrase to the current DatabaseKey (byte-array) format:
Database::migrate_db_key_type_to_bytes(
    "/path/to/cc.db",  // database path
    "old-passphrase",  // previous string key
    &new_key,          // new DatabaseKey
).await?;
After migration the database can be opened normally with the DatabaseKey.

Blob size limits

The keystore enforces a maximum blob length of 1 GB (1_000_000_000 bytes) for all platforms. On non-WASM platforms an additional SQLite limit of i32::MAX bytes applies per value. In practice, individual cryptographic objects are orders of magnitude smaller.

Transactions

The keystore has its own transaction layer that CoreCrypto’s TransactionContext builds on top of. All writes are buffered in a KeystoreTransaction and flushed atomically on commit_transaction():
db.new_transaction().await?;
// ... save / remove operations ...
db.commit_transaction().await?;

// Or roll back:
db.rollback_transaction().await?;
Only one concurrent transaction is allowed (enforced by an async Semaphore). Starting a new transaction blocks until the previous one completes or is rolled back.

Build docs developers (and LLMs) love