Skip to main content

walletkit_core/storage/
mod.rs

1//! # Credential Store
2//!
3//! On-device, consistent, encrypted storage for World ID credentials.
4//!
5//! The storage layer handles structured storage of all credentials and their
6//! associated data (only storage, the semantics of the associated data is the
7//! Issuer's responsibility). In addition the storage layer handles encryption
8//! and clean up after expiration.
9//!
10//! ## Components
11//!
12//! [`crate::storage::CredentialStore`] is the facade exposed to hosts (via `UniFFI`).
13//! It owns the account key envelope and two databases:
14//!
15//! 1. **Vault database (`account.vault.sqlite`)** — authoritative storage for
16//!    credentials, associated data blobs, issuer subject blinding factors, and the account
17//!    leaf index. Corruption is a hard failure. See [`crate::storage::CredentialVault`].
18//! 2. **Cache database (`account.cache.sqlite`)** — non-authoritative, regenerable
19//!    entries: Merkle inclusion proof cache, per-account session seed, and nullifier
20//!    replay guards. Subject to TTL pruning and can be rebuilt at any time without
21//!    correctness loss. See [`crate::storage::CacheDb`].
22//!
23//! The encrypted-storage primitives beneath these — the sealed key envelope, the
24//! `K_device` → `K_intermediate` key hierarchy, sqlite3mc encryption, the
25//! cross-process lock, content-addressed blobs, and the threat model are owned by
26//! the [`walletkit-db`](https://docs.rs/crate/walletkit-db/latest) crate.
27//!
28//! ## Keys
29//!
30//! Both databases are opened with the single `K_intermediate` managed by
31//! `walletkit-db`.
32//!
33//! ## On-disk layout
34//!
35//! The vault, cache, and lock live under `<root>/worldid/` — see
36//! [`crate::storage::StoragePaths`]. The account key envelope (`account_keys.bin`) is
37//! written separately through the host's [`crate::storage::AtomicBlobStore`] and its
38//! location is host-determined (not necessarily under `worldid/`); backup and
39//! deletion must include it.
40//!
41//! ## Security and privacy properties
42//!
43//! Encryption, the sealed-envelope threat model, and integrity checks are covered by
44//! the `walletkit-db` README.
45
46pub mod cache;
47pub mod credential_storage;
48pub mod credential_vault;
49pub mod error;
50pub mod keys;
51pub mod paths;
52pub mod traits;
53pub mod types;
54
55pub use cache::CacheDb;
56pub use credential_storage::CredentialStore;
57pub use credential_vault::CredentialVault;
58pub use error::{StorageError, StorageResult};
59pub use keys::StorageKeys;
60pub use paths::StoragePaths;
61pub use traits::{
62    ActivityChangedListener, AtomicBlobStore, DeviceKeystore, StorageProvider,
63    VaultChangedListener,
64};
65pub use types::{
66    ActivityEntry, ActivityFailureReason, ActivityMetadata, ActivityOutcome,
67    ActivityQuery, BlobKind, ContentId, CredentialRecord, Nullifier, ProtocolVersion,
68    ReplayGuardKind, ReplayGuardResult, RequestId,
69};
70pub use walletkit_db::{Lock as StorageLock, LockGuard as StorageLockGuard};
71
72/// Deletes a closed `SQLite` database and its journal sidecars.
73///
74/// Best effort - logs failed operations but does not return an error.
75pub(crate) fn delete_database_files(path: &std::path::Path) {
76    if let Err(err) = try_delete_database_files(path) {
77        tracing::error!("{err}");
78    }
79}
80
81/// Deletes a database and its journal files, attempting every file and reporting
82/// all failures.
83pub(crate) fn try_delete_database_files(path: &std::path::Path) -> Result<(), String> {
84    let errors: Vec<String> = [
85        path.to_path_buf(),
86        path.with_extension("sqlite-journal"),
87        path.with_extension("sqlite-wal"),
88        path.with_extension("sqlite-shm"),
89    ]
90    .iter()
91    .filter_map(|path| {
92        delete_database_file(path).err().map(|err| {
93            format!("Failed to delete database file {}: {err}", path.display())
94        })
95    })
96    .collect();
97    if errors.is_empty() {
98        Ok(())
99    } else {
100        Err(errors.join("; "))
101    }
102}
103
104#[cfg(target_arch = "wasm32")]
105fn delete_database_file(path: &std::path::Path) -> Result<(), String> {
106    walletkit_sqlite::opfs::delete_file(path).map_err(|err| err.to_string())
107}
108
109#[cfg(not(target_arch = "wasm32"))]
110fn delete_database_file(path: &std::path::Path) -> Result<(), String> {
111    match std::fs::remove_file(path) {
112        Ok(()) => Ok(()),
113        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
114        Err(err) => Err(err.to_string()),
115    }
116}
117
118/// Installs persistent encrypted browser storage in the current Web Worker.
119///
120/// This must be awaited once before initializing a [`CredentialStore`] on
121/// WASM. The function fails when called outside a supported dedicated worker
122/// or when another browsing context owns the same OPFS SAH pool.
123///
124/// # Errors
125///
126/// Returns [`StorageError::PersistentStorage`] when OPFS setup fails.
127#[cfg(target_arch = "wasm32")]
128#[uniffi::export]
129pub async fn initialize_persistent_storage() -> StorageResult<()> {
130    walletkit_sqlite::opfs::install()
131        .await
132        .map_err(|err| StorageError::PersistentStorage(err.to_string()))
133}
134
135pub(crate) const ACCOUNT_KEYS_FILENAME: &str = "account_keys.bin";
136pub(crate) const ACCOUNT_KEY_ENVELOPE_AD: &[u8] = b"worldid:account-key-envelope";
137
138#[cfg(test)]
139pub(crate) mod tests_utils;