Skip to main content

walletkit_core/storage/cache/
mod.rs

1//! Encrypted cache database for credential storage.
2
3use std::path::Path;
4
5use crate::storage::error::StorageResult;
6use secrecy::SecretBox;
7use walletkit_db::Vault;
8
9mod maintenance;
10mod merkle;
11mod nullifiers;
12mod schema;
13mod session;
14mod util;
15
16/// Encrypted cache database wrapper.
17///
18/// Stores non-authoritative, regenerable data (proof cache, session keys,
19/// replay guard). Wraps [`walletkit_db::Vault`].
20///
21/// Unlike the credential vault, cache corruption is recoverable: open
22/// failures or integrity failures trigger a wipe-and-rebuild rather than
23/// a fatal error.
24#[derive(Debug)]
25pub struct CacheDb {
26    vault: Vault,
27}
28
29impl CacheDb {
30    /// Opens or rebuilds the encrypted cache database at `path`.
31    ///
32    /// If the database is corrupted or unreadable, the file is deleted
33    /// and a fresh empty cache is created.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the database cannot be opened or rebuilt.
38    pub fn new(
39        path: &Path,
40        k_intermediate: &SecretBox<[u8; 32]>,
41    ) -> StorageResult<Self> {
42        let vault = maintenance::open_or_rebuild(path, k_intermediate)?;
43        Ok(Self { vault })
44    }
45
46    /// Fetches a cached Merkle proof if it remains valid beyond `valid_until`.
47    ///
48    /// Returns `None` when missing or expired so callers can refetch from the
49    /// indexer without relying on stale proofs.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the query fails.
54    pub fn merkle_cache_get(&self, valid_until: u64) -> StorageResult<Option<Vec<u8>>> {
55        merkle::get(self.vault.connection(), valid_until)
56    }
57
58    /// Inserts a cached Merkle proof with a TTL. Existing entries for the
59    /// same key are replaced.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the insert fails.
64    pub fn merkle_cache_put(
65        &self,
66        proof_bytes: &[u8],
67        now: u64,
68        ttl_seconds: u64,
69    ) -> StorageResult<()> {
70        merkle::put(self.vault.connection(), proof_bytes, now, ttl_seconds)
71    }
72
73    /// Fetches a cached `session_id_r_seed` for the given `oprf_seed`.
74    ///
75    /// Returns `None` when missing or expired.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the query fails.
80    pub fn session_seed_get(
81        &self,
82        oprf_seed: [u8; 32],
83        now: u64,
84    ) -> StorageResult<Option<[u8; 32]>> {
85        session::get(self.vault.connection(), oprf_seed, now)
86    }
87
88    /// Stores a `session_id_r_seed` keyed by `oprf_seed` with a TTL.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the insert fails.
93    pub fn session_seed_put(
94        &self,
95        oprf_seed: [u8; 32],
96        session_id_r_seed: [u8; 32],
97        now: u64,
98        ttl_seconds: u64,
99    ) -> StorageResult<()> {
100        session::put(
101            self.vault.connection(),
102            oprf_seed,
103            session_id_r_seed,
104            now,
105            ttl_seconds,
106        )
107    }
108
109    /// Checks whether a replay guard entry exists for the given nullifier.
110    ///
111    /// # Returns
112    ///
113    /// - `true` if a replay guard entry exists (nullifier replay).
114    /// - `false` otherwise.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the query to the cache unexpectedly fails.
119    pub fn is_nullifier_replay(
120        &self,
121        nullifier: [u8; 32],
122        now: u64,
123    ) -> StorageResult<bool> {
124        nullifiers::is_nullifier_replay(self.vault.connection(), nullifier, now)
125    }
126
127    /// After a proof has been successfully generated, creates a replay guard
128    /// entry locally to avoid future replays of the same nullifier.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the query to the cache unexpectedly fails.
133    pub fn replay_guard_set(&self, nullifier: [u8; 32], now: u64) -> StorageResult<()> {
134        nullifiers::replay_guard_set(self.vault.connection(), nullifier, now)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use secrecy::SecretBox;
142    use std::fs;
143    use std::path::PathBuf;
144    use uuid::Uuid;
145
146    fn temp_cache_path() -> PathBuf {
147        let mut path = std::env::temp_dir();
148        path.push(format!("walletkit-cache-{}.sqlite", Uuid::new_v4()));
149        path
150    }
151
152    fn cleanup_cache_files(path: &Path) {
153        let _ = fs::remove_file(path);
154        let _ = fs::remove_file(path.with_extension("sqlite-wal"));
155        let _ = fs::remove_file(path.with_extension("sqlite-shm"));
156    }
157
158    fn temp_lock_path() -> PathBuf {
159        let mut path = std::env::temp_dir();
160        path.push(format!("walletkit-cache-lock-{}.lock", Uuid::new_v4()));
161        path
162    }
163
164    fn cleanup_lock_file(path: &Path) {
165        let _ = fs::remove_file(path);
166    }
167
168    #[test]
169    fn test_cache_create_and_open() {
170        let path = temp_cache_path();
171        let key = SecretBox::init_with(|| [0x11u8; 32]);
172        let lock_path = temp_lock_path();
173        let db = CacheDb::new(&path, &key).expect("create cache");
174        drop(db);
175        CacheDb::new(&path, &key).expect("open cache");
176        cleanup_cache_files(&path);
177        cleanup_lock_file(&lock_path);
178    }
179
180    #[test]
181    fn test_cache_rebuild_on_corruption() {
182        let path = temp_cache_path();
183        let key = SecretBox::init_with(|| [0x22u8; 32]);
184        let lock_path = temp_lock_path();
185        let db = CacheDb::new(&path, &key).expect("create cache");
186        let oprf_seed = [0x01u8; 32];
187        let r_seed = [0x02u8; 32];
188        let now = 1_000;
189        db.session_seed_put(oprf_seed, r_seed, now, 1000)
190            .expect("put session seed");
191        drop(db);
192
193        fs::write(&path, b"corrupt").expect("corrupt cache file");
194
195        let db = CacheDb::new(&path, &key).expect("rebuild cache");
196        let value = db
197            .session_seed_get(oprf_seed, now)
198            .expect("get session seed");
199        assert!(value.is_none());
200        cleanup_cache_files(&path);
201        cleanup_lock_file(&lock_path);
202    }
203
204    #[test]
205    fn test_merkle_cache_ttl() {
206        let path = temp_cache_path();
207        let key = SecretBox::init_with(|| [0x33u8; 32]);
208        let lock_path = temp_lock_path();
209        let db = CacheDb::new(&path, &key).expect("create cache");
210        db.merkle_cache_put(&[1, 2, 3], 100, 10)
211            .expect("put merkle proof");
212        let hit = db.merkle_cache_get(105).expect("get merkle proof");
213        assert!(hit.is_some());
214        let miss = db.merkle_cache_get(111).expect("get merkle proof");
215        assert!(miss.is_none());
216        cleanup_cache_files(&path);
217        cleanup_lock_file(&lock_path);
218    }
219
220    #[test]
221    fn test_session_seed_cache_ttl() {
222        let path = temp_cache_path();
223        let key = SecretBox::init_with(|| [0x44u8; 32]);
224        let lock_path = temp_lock_path();
225        let db = CacheDb::new(&path, &key).expect("create cache");
226        let oprf_seed = [0x55u8; 32];
227        let r_seed = [0x66u8; 32];
228        let now = 100;
229        db.session_seed_put(oprf_seed, r_seed, now, 10)
230            .expect("put session seed");
231        let hit = db.session_seed_get(oprf_seed, now).expect("get");
232        assert_eq!(hit, Some(r_seed));
233        let miss = db.session_seed_get(oprf_seed, now + 11).expect("get");
234        assert!(miss.is_none());
235        cleanup_cache_files(&path);
236        cleanup_lock_file(&lock_path);
237    }
238}