Skip to main content

walletkit_core/storage/cache/
mod.rs

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