Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions walletkit-core/src/storage/cache/mod.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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,
Expand Down
21 changes: 19 additions & 2 deletions walletkit-core/src/storage/cache/nullifiers.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
15 changes: 15 additions & 0 deletions walletkit-core/src/storage/cache/schema.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
47 changes: 45 additions & 2 deletions walletkit-core/src/storage/credential_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CredentialStoreInner>,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
///
Expand All @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions walletkit-core/src/storage/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
/// Timestamp of initial envelope creation (unix seconds).
pub(crate) created_at: u64,
/// Timestamp of last envelope update (unix seconds).
pub(crate) updated_at: u64,
}

Expand Down
14 changes: 14 additions & 0 deletions walletkit-core/src/storage/keys.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
Loading
Loading