diff --git a/walletkit-core/src/storage/cache/mod.rs b/walletkit-core/src/storage/cache/mod.rs index cd0b9fe41..a6e095076 100644 --- a/walletkit-core/src/storage/cache/mod.rs +++ b/walletkit-core/src/storage/cache/mod.rs @@ -1,4 +1,34 @@ //! Encrypted cache database for credential storage. +//! +//! The cache database (`account.cache.sqlite`) is encrypted via sqlite3mc +//! and integrity-protected, keyed by `K_intermediate`. It stores +//! **non-authoritative**, regenerable cache entries (key/value/ttl): +//! +//! - Per-RP session key material (derived from `K_intermediate`) for session proof flows +//! - Replay-safety entries (nullifier mappings) +//! - Merkle inclusion proof cache +//! +//! May grow large and is subject to aggressive TTL-based pruning. Can be deleted +//! and rebuilt at any time without correctness loss. +//! +//! ## Cache growth bounds +//! +//! Because these caches may grow large, TTL pruning is enforced before inserts: +//! +//! ```sql +//! DELETE FROM cache_entries WHERE expires_at <= now; +//! ``` +//! +//! ## Corruption handling +//! +//! On open during `init`, the implementation MUST run a lightweight sqlite3mc +//! integrity check. If it fails: +//! +//! - Close DB +//! - Delete/recreate `account.cache.sqlite` and schema +//! +//! After rebuild, cache entries are empty (merkle proofs, replay-safety mappings, +//! session keys). use std::path::Path; @@ -18,6 +48,10 @@ mod util; /// /// Stores non-authoritative, regenerable data (proof cache, session keys, replay guard) /// to improve performance without affecting correctness if rebuilt. +/// +/// All cacheable data is stored in a single `cache_entries` table with a unified +/// key/value/TTL structure. Entries are distinguished by a 1-byte key prefix +/// followed by type-specific key material. #[derive(Debug)] pub struct CacheDb { conn: Connection, diff --git a/walletkit-core/src/storage/cache/nullifiers.rs b/walletkit-core/src/storage/cache/nullifiers.rs index b5e6bc7fb..d67dbf3ab 100644 --- a/walletkit-core/src/storage/cache/nullifiers.rs +++ b/walletkit-core/src/storage/cache/nullifiers.rs @@ -1,7 +1,24 @@ //! Used-nullifier cache helpers for replay protection. //! -//! Tracks request ids and nullifiers to enforce single-use disclosures while -//! remaining idempotent for retries within the TTL window. +//! Tracks nullifiers to enforce single-use disclosures while remaining +//! idempotent for retries within the TTL window. +//! +//! ## Nullifier replay safety +//! +//! Within the retention window (TTL): +//! +//! - A `nullifier` may be associated with at most one `request_id`. +//! - A `request_id` always returns the same proof bytes until expiry. +//! +//! Behavior: +//! +//! - Reusing a `request_id` returns the original proof. +//! - Reusing a `nullifier` with a different `request_id` fails. +//! - Expired entries may be pruned. +//! +//! Guarantees: +//! +//! - Enforcement is transactional. use crate::storage::error::StorageResult; use walletkit_db::Connection; diff --git a/walletkit-core/src/storage/cache/schema.rs b/walletkit-core/src/storage/cache/schema.rs index c12b2f517..14baa6595 100644 --- a/walletkit-core/src/storage/cache/schema.rs +++ b/walletkit-core/src/storage/cache/schema.rs @@ -1,4 +1,19 @@ //! Cache database schema management. +//! +//! All cacheable data is stored in a single `cache_entries` table with a unified +//! key/value/TTL structure. Entries are distinguished by a 1-byte key prefix +//! followed by type-specific key material. +//! +//! ## Key format +//! +//! `key_bytes = prefix || key_material`: +//! +//! - `0x01` — Merkle inclusion proof +//! - `value_bytes = proof_bytes` +//! - `0x02 || rp_id` — session key +//! - `value_bytes = k_session` +//! - `0x03 || nullifier` — replay guard nullifier +//! - `value_bytes = marker byte` use crate::storage::error::StorageResult; use walletkit_db::{params, Connection}; diff --git a/walletkit-core/src/storage/credential_storage.rs b/walletkit-core/src/storage/credential_storage.rs index 3d397035a..24d497e78 100644 --- a/walletkit-core/src/storage/credential_storage.rs +++ b/walletkit-core/src/storage/credential_storage.rs @@ -55,7 +55,15 @@ fn remove_db_files(db_path: &std::path::Path) { } } -/// Concrete storage implementation backed by `SQLCipher` databases. +/// Concrete storage implementation backed by sqlite3mc-encrypted databases. +/// +/// This is the public-facing API surface exposed to `WalletKit`. It is the single +/// entry point for all credential persistence, caching, and replay-safety +/// operations. Internally it delegates to a [`VaultDb`] (authoritative) and +/// a [`CacheDb`] (non-authoritative), both keyed by `K_intermediate`. +/// +/// All mutable operations are serialized under a [`Mutex`] and an account-wide +/// storage lock to prevent concurrent modification. #[cfg_attr(not(target_arch = "wasm32"), derive(uniffi::Object))] pub struct CredentialStore { inner: Mutex, @@ -186,6 +194,25 @@ impl CredentialStore { /// Initializes storage and validates the account leaf index. /// + /// On success, the following MUST hold: + /// + /// - An intermediate key `K_intermediate: [u8; 32]` is available in memory. + /// - The vault database is opened as a sqlite3mc-encrypted database keyed by + /// `K_intermediate`. + /// - The cache database is opened as a sqlite3mc-encrypted database keyed by + /// `K_intermediate`. + /// - Session keys are derived as needed from `K_intermediate` and MAY be + /// persisted in cache (`cache_entries` with the session key prefix). + /// + /// # Rules + /// + /// - If no `K_intermediate` exists, it is generated and sealed under the device keystore. + /// - If a `leaf_index` is not yet recorded, it is set. + /// - If a recorded `leaf_index` differs from the provided value, initialization fails. + /// - Once set, `leaf_index` MUST NOT change. + /// - The cache database is non-authoritative and may be rebuilt on integrity failure. + /// - Initialization is serialized under a global storage lock. + /// /// # Errors /// /// Returns an error if initialization fails or the leaf index mismatches. @@ -443,8 +470,18 @@ impl CredentialStore { /// Checks whether a replay guard entry exists for the given nullifier. /// + /// Within the retention window (TTL): + /// + /// - A `nullifier` may be associated with at most one `request_id`. + /// - A `request_id` always returns the same proof bytes until expiry. + /// - Reusing a `nullifier` with a different `request_id` fails. + /// - Expired entries may be pruned. + /// - Enforcement is transactional. + /// /// # Returns - /// - bool: true if a replay guard entry exists (hence signalling a nullifier replay), false otherwise. + /// + /// `true` if a replay guard entry exists (hence signalling a nullifier replay), + /// `false` otherwise. /// /// # Errors /// @@ -460,6 +497,12 @@ impl CredentialStore { /// After a proof has been successfully generated, creates a replay guard entry /// locally to avoid future replays of the same nullifier. /// + /// Within the retention window (TTL): + /// + /// - Reusing a `request_id` returns the original proof. + /// - Reusing a `nullifier` with a different `request_id` fails. + /// - Expired entries may be pruned. + /// /// # Errors /// /// Returns an error if the query to the cache unexpectedly fails. diff --git a/walletkit-core/src/storage/envelope.rs b/walletkit-core/src/storage/envelope.rs index abad8e190..d3cfc2afd 100644 --- a/walletkit-core/src/storage/envelope.rs +++ b/walletkit-core/src/storage/envelope.rs @@ -7,11 +7,21 @@ use super::error::{StorageError, StorageResult}; const ENVELOPE_VERSION: u32 = 1; +/// Account key envelope persisted as `account_keys.bin`. +/// +/// Stores `K_intermediate` sealed under `K_device` (via [`DeviceKeystore`](super::traits::DeviceKeystore)). +/// Opened once per storage initialization and kept in memory for the lifetime +/// of the storage handle. Device-local and not intended to be synced across devices. #[derive(Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] pub(crate) struct AccountKeyEnvelope { + /// Envelope format version. pub(crate) version: u32, + /// `DeviceKeystore::seal(ad_i, K_intermediate)` where + /// `ad_i = "worldid:account-key-envelope"`. pub(crate) wrapped_k_intermediate: Vec, + /// Timestamp of initial envelope creation (unix seconds). pub(crate) created_at: u64, + /// Timestamp of last envelope update (unix seconds). pub(crate) updated_at: u64, } diff --git a/walletkit-core/src/storage/keys.rs b/walletkit-core/src/storage/keys.rs index fa5d532f2..ed72f0838 100644 --- a/walletkit-core/src/storage/keys.rs +++ b/walletkit-core/src/storage/keys.rs @@ -1,4 +1,18 @@ //! Key hierarchy management for credential storage. +//! +//! ## Root and intermediate keys +//! +//! - `K_device` +//! - Device-bound root key provided by the platform keystore +//! (`DeviceKeystore`). +//! - MUST be non-exportable when supported. +//! - `K_intermediate` +//! - 32-byte per-account intermediate key. +//! - Generated randomly on first use. +//! - Stored sealed under `K_device` in `account_keys.bin` +//! (see `AccountKeyEnvelope`). +//! - Loaded once during initialization and retained in memory for the lifetime +//! of the storage session. use rand::{rngs::OsRng, RngCore}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; diff --git a/walletkit-core/src/storage/mod.rs b/walletkit-core/src/storage/mod.rs index c24caacab..a345b3c2a 100644 --- a/walletkit-core/src/storage/mod.rs +++ b/walletkit-core/src/storage/mod.rs @@ -1,4 +1,214 @@ -//! Credential storage primitives: key envelope and key hierarchy helpers. +//! Credential storage: consistent, versioned, encrypted persistence for World ID credentials. +//! +//! # Goal +//! +//! Have a consistent method to store credentials. +//! +//! - Consistent API for different credentials +//! - Store different versions of the same credential +//! - Eventually: +//! - Awareness of a credential on multiple devices (sync, originating authenticator) +//! - Pruning/compaction +//! - Purging +//! +//! # Components +//! +//! The storage system consists of the following components: +//! +//! 1. **Device keystore root (`K_device`)** +//! - A device-bound, preferably non-exportable key. +//! - Backed by Secure Enclave / Android Keystore / `WebCrypto` where available. +//! - Used only to unwrap a per-account intermediate key during initialization. +//! - Never used directly for database encryption. +//! +//! 2. **Account Key Envelope (`account_keys.bin`)** +//! - `K_intermediate` sealed under `K_device`. +//! - Opened once per storage initialization and kept in memory for the lifetime +//! of the storage handle. +//! - Device-local and not intended to be synced across devices. +//! +//! 3. **Encrypted Vault Database (`account.vault.sqlite`)** +//! - Encrypted via sqlite3mc (`SQLite3` Multiple Ciphers, `ChaCha20-Poly1305` default) +//! and integrity-protected. +//! - Opened using `K_intermediate`. +//! - Authoritative storage for: +//! - Credentials and associated blobs +//! - Issuer subject blinding factors +//! - Core account state (leaf index, per-device state) +//! - This implementation is device-local; future cross-device sync would require an +//! explicit key export/import mechanism or an external key provider. +//! - On native targets, sqlite3mc is compiled from the vendored amalgamation via `cc`. +//! On WASM targets, `sqlite-wasm-rs` provides sqlite3mc compiled to WebAssembly. +//! +//! 4. **Encrypted Cache Database (`account.cache.sqlite`)** +//! - Encrypted via sqlite3mc and integrity-protected. +//! - Opened using `K_intermediate`. +//! - Stores non-authoritative, regenerable cache entries (key/value/ttl): +//! - Per-RP session key material (derived from `K_intermediate`) for session proof flows +//! - Replay-safety entries (nullifier mappings) +//! - Merkle inclusion proof cache +//! - May grow large and is subject to aggressive TTL-based pruning. +//! - Can be deleted and rebuilt at any time without correctness loss. +//! +//! # Cryptographic Keys +//! +//! ## Root and intermediate keys +//! +//! - `K_device` +//! - Device-bound root key provided by the platform keystore. +//! - MUST be non-exportable when supported. +//! - `K_intermediate` +//! - 32-byte per-account intermediate key. +//! - Generated randomly on first use. +//! - Stored sealed under `K_device` in `account_keys.bin`. +//! - Loaded once during initialization and retained in memory for the lifetime +//! of the storage session. +//! +//! ## Derived keys (device-local) +//! +//! Derived session keys (**NOTE: This is temporary (~2 months)**) +//! +//! - `K_session = HKDF(IKM = K_intermediate, salt = , info = "worldid:session-key" || rpId)` +//! - `r = HKDF(IKM = K_session, salt = , info = "worldid:session-r" || actionId)` +//! +//! Session keys are derived from `K_intermediate` and therefore are device-local. +//! Persisting them in cache is an optimization and does not change correctness. +//! +//! ## sqlite3mc database keying +//! +//! Both databases (`account.vault.sqlite` and `account.cache.sqlite`) are encrypted +//! using sqlite3mc (`SQLite3` Multiple Ciphers) with `K_intermediate` as the key material. +//! The default cipher is `ChaCha20-Poly1305` (no `OpenSSL` dependency). The same encryption +//! library and PRAGMA dialect is used on all platforms (native and WASM). +//! +//! ## Key hierarchy +//! +//! ```text +//! Level 0 — Device Keystore Root +//! ┌──────────────────────────────────────────────────────────┐ +//! │ K_device │ +//! │ Device-bound root key │ +//! │ Secure Enclave / Android Keystore / WebCrypto │ +//! │ Non-exportable when supported │ +//! └──────────────┬───────────────────────────────────────────┘ +//! │ seal / open (AD = "worldid:account-key-envelope") +//! ▼ +//! Level 1 — Account Key Envelope +//! ┌──────────────────────────────────────────────────────────┐ +//! │ account_keys.bin │ +//! │ Stores: seal(AD_i, K_intermediate) under K_device │ +//! │ │ +//! │ In-memory after init: │ +//! │ K_intermediate (32 bytes) │ +//! │ Per-install intermediate key, unsealed via K_device │ +//! └──────┬───────────────┬───────────────┬───────────────────┘ +//! │ │ │ +//! │ sqlite3mc key │ sqlite3mc key │ HKDF (rpId) +//! ▼ ▼ ▼ +//! ┌────────────┐ ┌────────────┐ Level 2 — Derived Keys +//! │ Vault DB │ │ Cache DB │ ┌──────────────────────────┐ +//! │ .vault. │ │ .cache. │ │ K_session (32B) │ +//! │ sqlite │ │ sqlite │ │ HKDF: IKM=K_intermediate │ +//! │ │ │ │ │ info=session-key || rpId │ +//! │ Stores: │ │ Stores: │ ├──────────────────────────┤ +//! │ -leaf_index│ │ -nullifiers│ │ │ HKDF (actionId)│ +//! │ -creds + │ │ -merkle │ │ ▼ │ +//! │ blobs │ │ cache │ │ r (32B) │ +//! │ │ │ -per-RP │ │ HKDF: IKM=K_session │ +//! │ │ │ sessions │ │ info=session-r || actionId│ +//! └────────────┘ └────────────┘ ├──────────────────────────┤ +//! │ │ hash │ +//! │ ▼ │ +//! │ sessionId │ +//! │ H(DS_C || leafIndex || r)│ +//! └──────────────────────────┘ +//! ``` +//! +//! # On-disk layout +//! +//! Storage root: `/worldid/` +//! +//! ```text +//! account_keys.bin # DeviceKeystore-sealed K_intermediate envelope +//! account.cache.sqlite # sqlite3mc-encrypted SQLite cache DB (keyed by K_intermediate) +//! account.vault.sqlite # sqlite3mc-encrypted SQLite vault DB (keyed by K_intermediate) +//! lock # account-scoped lock +//! ``` +//! +//! # Locking and concurrency +//! +//! All operations that modify any persistent layer execute under an account-wide lock, including: +//! +//! - Writes to `account_keys.bin` +//! - Vault DB writes +//! - Cache DB writes +//! +//! Nullifier replay guard operations are transactional to prevent race conditions +//! between concurrent proof flows. +//! +//! # Security and Privacy Properties +//! +//! - No filesystem paths contain `leaf_index`, RP identifiers, issuer names, or action names. +//! - `AccountId` is not a hash of `leaf_index`; it is derived from `K_intermediate` and +//! network context, preventing brute-force recovery of `leaf_index`. +//! - Vault contents are end-to-end encrypted via sqlite3mc (keyed by `K_intermediate`, +//! `ChaCha20-Poly1305` by default); untrusted storage cannot read credentials. +//! No `OpenSSL` dependency is required. +//! - Account state is device-protected under `K_device`; it contains: +//! - `leaf_index_cache` (sensitive) +//! - `K_session_root` (sensitive) +//! - `used_nullifier_cache` (sensitive; bounded TTL) +//! - Merkle proof cache (sensitive; bounded TTL) +//! - Cache DB entries are sensitive and bounded by TTL: +//! - Replay guard entries +//! - Merkle proof cache +//! - Per-RP session keys +//! - Replay guard entries are intentionally short-lived to avoid creating long-lived +//! "interaction history" on device. +//! - Granular security on each credential stored in the vault: each credential blob +//! is encrypted with a separate key which should be hardware backed (or wrapped) +//! when available. Each key should have access control proportional to the sensitivity +//! of that specific credential. +//! +//! # Operational flow: Unique action proof +//! +//! ```text +//! User ─► RP: (initiates proof request) +//! RP ─► Authenticator: signed proof request (rp_id, action, nonce, signal) +//! +//! ── Account must be initialized/unlocked once per session ── +//! Authenticator ─► Storage: init(leafIndex) +//! Storage: unwrap K_intermediate via device keystore, +//! open sqlite3mc vault + cache DBs keyed by K_intermediate +//! +//! ── Merkle inclusion proof lookup ── +//! Authenticator ─► Storage: merkle_cache_get(registry_kind, root, now) +//! if cache hit (not expired): +//! Storage ─► Authenticator: proof_bytes +//! else (cache miss / expired): +//! Authenticator ─► Indexer: fetch inclusion proof for (registry_kind, root, leafIndex) +//! Indexer ─► Authenticator: proof_bytes +//! Authenticator ─► Storage: merkle_cache_put(registry_kind, root, proof_bytes, now, ttl) +//! +//! ── OPRF query phase (blinded leafIndex + query proof) ── +//! Authenticator ─► OPRF Nodes: query proof + blinded request context +//! OPRF Nodes ─► Authenticator: blinded responses (threshold) +//! +//! Authenticator: construct proof_package + nullifier +//! +//! ── Replay-safety / single-use disclosure enforcement (transactional) ── +//! Authenticator ─► Storage: begin_proof_disclosure(request_id, nullifier, proof_package, now, ttl) +//! if replay (same request_id): +//! Storage ─► Authenticator: same proof_package bytes +//! if fresh: +//! Storage ─► Authenticator: proof_package bytes +//! if conflict (same nullifier, different request_id): +//! Storage ─► Authenticator: error (NullifierAlreadyDisclosed) +//! Authenticator: MUST NOT disclose a new proof_package for that nullifier +//! +//! Authenticator ─► RP: proof_package +//! RP ─► Authenticator: success/failure +//! ``` pub mod cache; pub mod credential_storage; diff --git a/walletkit-core/src/storage/paths.rs b/walletkit-core/src/storage/paths.rs index ee37944c4..24265cf8f 100644 --- a/walletkit-core/src/storage/paths.rs +++ b/walletkit-core/src/storage/paths.rs @@ -1,4 +1,6 @@ //! Storage path helpers. +//! +//! All credential storage artifacts live under `/worldid/`. use std::path::{Path, PathBuf}; @@ -12,6 +14,19 @@ const QUERY_GRAPH_FILENAME: &str = "OPRFQueryGraph.bin"; const NULLIFIER_GRAPH_FILENAME: &str = "OPRFNullifierGraph.bin"; /// Paths for credential storage artifacts under `/worldid`. +/// +/// ```text +/// /worldid/ +/// account_keys.bin # DeviceKeystore-sealed K_intermediate envelope +/// account.cache.sqlite # sqlite3mc-encrypted cache DB (keyed by K_intermediate) +/// account.vault.sqlite # sqlite3mc-encrypted vault DB (keyed by K_intermediate) +/// lock # account-scoped lock +/// groth16/ # cached Groth16 proving material +/// OPRFQuery.arks.zkey +/// OPRFNullifier.arks.zkey +/// OPRFQueryGraph.bin +/// OPRFNullifierGraph.bin +/// ``` #[derive(Debug, Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(uniffi::Object))] pub struct StoragePaths { diff --git a/walletkit-core/src/storage/traits.rs b/walletkit-core/src/storage/traits.rs index 838007197..91b6f94cf 100644 --- a/walletkit-core/src/storage/traits.rs +++ b/walletkit-core/src/storage/traits.rs @@ -1,18 +1,49 @@ //! Platform interfaces for credential storage. //! +//! This module defines the platform integration boundary for the storage engine. +//! The platform layer selects the storage root and wires together the keystore +//! and blob store. Core storage code is root-agnostic and consumes a +//! provider-supplied [`StoragePaths`]. +//! //! ## Key structure //! -//! - `K_device`: device-bound root key managed by `DeviceKeystore`. -//! - `account_keys.bin`: account key envelope stored via `AtomicBlobStore` and +//! - `K_device`: device-bound root key managed by [`DeviceKeystore`]. +//! - `account_keys.bin`: account key envelope stored via [`AtomicBlobStore`] and //! containing `DeviceKeystore::seal` of `K_intermediate` with associated data //! `worldid:account-key-envelope`. //! - `K_intermediate`: 32-byte per-account key unsealed at init and kept in //! memory for the lifetime of the storage handle. -//! - `SQLCipher` databases: `account.vault.sqlite` (authoritative) and +//! - sqlite3mc databases: `account.vault.sqlite` (authoritative) and //! `account.cache.sqlite` (non-authoritative) are opened with `K_intermediate`. //! - Derived keys: per relying-party session keys may be derived from //! `K_intermediate` and cached in `account.cache.sqlite` for performance. -//! cached in `account.cache.sqlite` for performance. +//! +//! ## Platform Bindings +//! +//! ### iOS (Swift) +//! +//! Default platform components: +//! - [`DeviceKeystore`]: Keychain / Secure Enclave-backed keystore +//! - [`AtomicBlobStore`]: app container filesystem (atomic replace) +//! +//! ### Android (Kotlin) +//! +//! Default platform components: +//! - [`DeviceKeystore`]: Android Keystore-backed +//! - [`AtomicBlobStore`]: app internal storage (atomic replace) +//! +//! ### Node.js +//! +//! Default platform components: +//! - [`DeviceKeystore`]: file-backed keystore stored under `/worldid/device_keystore.bin` +//! (development; production can use OS keystore) +//! - [`AtomicBlobStore`]: app internal storage (atomic replace) +//! +//! ### Browser (WASM) +//! +//! Default platform components: +//! - [`DeviceKeystore`]: `WebCrypto`-backed device keystore +//! - [`AtomicBlobStore`]: origin-private storage namespace use std::sync::Arc; @@ -20,6 +51,13 @@ use super::error::StorageResult; use super::paths::StoragePaths; /// Device keystore interface used to seal and open account keys. +/// +/// Represents the device-bound root key (`K_device`) provided by the platform keystore. +/// This key MUST be non-exportable when supported by the platform (Secure Enclave on iOS, +/// Android Keystore on Android, `WebCrypto` on browsers). +/// +/// `K_device` is used **only** to unwrap the per-account intermediate key (`K_intermediate`) +/// during initialization. It is never used directly for database encryption. #[cfg_attr(not(target_arch = "wasm32"), uniffi::export(with_foreign))] pub trait DeviceKeystore: Send + Sync { /// Seals plaintext under the device-bound key, authenticating `associated_data`. @@ -51,7 +89,11 @@ pub trait DeviceKeystore: Send + Sync { ) -> StorageResult>; } -/// Atomic blob store for small binary files (e.g., `account_keys.bin`). +/// Atomic blob store for small binary files. +/// +/// Used to persist the account key envelope (`account_keys.bin`) which contains +/// `K_intermediate` sealed under `K_device`. Writes MUST be atomic (write-then-rename +/// or equivalent) to avoid partial-write corruption. #[cfg_attr(not(target_arch = "wasm32"), uniffi::export(with_foreign))] pub trait AtomicBlobStore: Send + Sync { /// Reads the blob at `path`, if present. @@ -77,6 +119,10 @@ pub trait AtomicBlobStore: Send + Sync { } /// Provider responsible for platform-specific storage components and paths. +/// +/// The platform layer selects the storage root and wires together the keystore +/// and blob store. Core storage code is root-agnostic and consumes a +/// provider-supplied [`StoragePaths`]. #[cfg_attr(not(target_arch = "wasm32"), uniffi::export(with_foreign))] pub trait StorageProvider: Send + Sync { /// Returns the device keystore implementation. diff --git a/walletkit-core/src/storage/types.rs b/walletkit-core/src/storage/types.rs index 1ec80939b..38e5d7191 100644 --- a/walletkit-core/src/storage/types.rs +++ b/walletkit-core/src/storage/types.rs @@ -35,12 +35,19 @@ impl TryFrom for BlobKind { } /// Content identifier for stored blobs. +/// +/// Computed as `SHA256("worldid:blob" || blob_kind || plaintext_bytes)`. +/// Used to deduplicate blob storage in the vault `blob_objects` table. pub type ContentId = [u8; 32]; /// Request identifier for replay guard. pub type RequestId = [u8; 32]; /// Nullifier identifier used for replay safety. +/// +/// Within the retention window, a nullifier may be associated with at most +/// one `request_id`. Replay guard entries are intentionally short-lived to +/// avoid creating long-lived "interaction history" on device. pub type Nullifier = [u8; 32]; /// In-memory representation of stored credential metadata. diff --git a/walletkit-core/src/storage/vault/mod.rs b/walletkit-core/src/storage/vault/mod.rs index 4d8527f1a..38b388f24 100644 --- a/walletkit-core/src/storage/vault/mod.rs +++ b/walletkit-core/src/storage/vault/mod.rs @@ -1,4 +1,38 @@ //! Encrypted vault database for credential storage. +//! +//! The vault database (`account.vault.sqlite`) is the **authoritative** source +//! of truth for the account. It is encrypted via sqlite3mc (`SQLite3` Multiple +//! Ciphers, `ChaCha20-Poly1305` default) and integrity-protected, keyed by +//! `K_intermediate`. +//! +//! It stores: +//! +//! - Credentials and associated blobs +//! - Issuer subject blinding factors +//! - Core account state: +//! - Account leaf index +//! - Per-device state +//! +//! This implementation is device-local; future cross-device sync would require an +//! explicit key export/import mechanism or an external key provider. +//! +//! On native targets, sqlite3mc is compiled from the vendored amalgamation via `cc`. +//! On WASM targets, `sqlite-wasm-rs` provides sqlite3mc compiled to WebAssembly. +//! +//! ## Durability settings +//! +//! On open, implementations configure sqlite3mc/SQLite for crash consistency: +//! +//! - WAL journaling +//! - Durable sync level (`FULL`) +//! +//! Implementations MUST also perform a lightweight sqlite3mc integrity check +//! during `init()`. +//! +//! ## Corruption handling +//! +//! The vault database is authoritative. If the vault DB integrity check fails, +//! `init` returns an error. The vault is **never** auto-rebuilt. mod helpers; mod schema; @@ -17,6 +51,10 @@ use walletkit_db::{params, Connection, StepResult, Value}; use zeroize::Zeroizing; /// Encrypted vault database wrapper. +/// +/// Authoritative storage for credentials and associated blobs, issuer subject +/// blinding factors, and core account state (leaf index). Encrypted via +/// sqlite3mc (`ChaCha20-Poly1305` default) and keyed by `K_intermediate`. #[derive(Debug)] pub struct VaultDb { conn: Connection, diff --git a/walletkit-core/src/storage/vault/schema.rs b/walletkit-core/src/storage/vault/schema.rs index db7154888..ed9d9ac80 100644 --- a/walletkit-core/src/storage/vault/schema.rs +++ b/walletkit-core/src/storage/vault/schema.rs @@ -1,4 +1,38 @@ //! Vault database schema management. +//! +//! The vault schema defines three tables: +//! +//! ## `vault_meta` +//! +//! Stores the schema version and account leaf index. The leaf index is +//! nullable until first initialization. Once set it MUST NOT change. +//! +//! ## `credential_records` +//! +//! Each row represents a stored credential. Fields: +//! +//! - `credential_id` — auto-increment primary key (INTEGER). +//! - `issuer_schema_id` — the unique issuer+schema identifier (u64). +//! - `subject_blinding_factor` — 32-byte blinding factor (BLOB). +//! - `genesis_issued_at` — timestamp of first issuance (unix seconds). +//! - `expires_at` — expiration timestamp (unix seconds). +//! - `credential_blob_cid` — [`ContentId`](super::super::types::ContentId) referencing +//! the credential payload in `blob_objects`. +//! - `associated_data_cid` — optional [`ContentId`](super::super::types::ContentId) +//! referencing associated data in `blob_objects`. +//! +//! Indexed by `(issuer_schema_id, updated_at DESC)` for efficient lookups +//! and by `expires_at` for expiry queries. +//! +//! ## `blob_objects` +//! +//! Content-addressed blob storage. Each row stores a blob payload identified +//! by its [`ContentId`](super::super::types::ContentId). +//! +//! - `content_id` — 32 bytes: `SHA256("worldid:blob" || blob_kind || plaintext_bytes)`. +//! - `blob_kind` — 1 = [`CredentialBlob`](super::super::types::BlobKind::CredentialBlob), +//! 2 = [`AssociatedData`](super::super::types::BlobKind::AssociatedData). +//! - `bytes` — the raw blob payload. use crate::storage::error::StorageResult; use walletkit_db::Connection;