dig_keystore/backend/memory.rs
1//! In-memory backend.
2//!
3//! Originally feature-gated behind `testing`. As of v0.1.2 it is compiled
4//! unconditionally because production adapters in other crates (notably
5//! `dig-l1-wallet`'s `encryption.rs`) use it as a scratch backend to reuse
6//! the full keystore file format without touching the filesystem. The
7//! `testing` module still re-exports it for discoverability in dependent
8//! crates' dev-dependencies.
9//!
10//! Stores blobs in a `parking_lot::Mutex<HashMap>`. Legitimate production
11//! uses: encrypt-to-bytes / decrypt-from-bytes helpers; unit tests; doc
12//! examples.
13
14use std::collections::HashMap;
15
16use parking_lot::Mutex;
17
18use crate::backend::{BackendKey, KeychainBackend};
19use crate::error::{KeystoreError, Result};
20
21/// A keychain backend that lives entirely in process memory.
22///
23/// Legitimate uses:
24/// - **Scratch backend** for bytes-in / bytes-out adapters (e.g.
25/// `dig-l1-wallet::keystore::encryption::encrypt_secret_key`).
26/// - **Tests and doc examples** where touching the filesystem is overhead.
27///
28/// Do **not** use this as the storage medium for a long-lived keystore —
29/// process exit drops all state.
30#[derive(Default)]
31pub struct MemoryBackend {
32 inner: Mutex<HashMap<BackendKey, Vec<u8>>>,
33}
34
35impl MemoryBackend {
36 /// Construct an empty backend.
37 pub fn new() -> Self {
38 Self::default()
39 }
40}
41
42impl KeychainBackend for MemoryBackend {
43 fn read(&self, key: &BackendKey) -> Result<Vec<u8>> {
44 self.inner.lock().get(key).cloned().ok_or_else(|| {
45 KeystoreError::from(std::io::Error::new(
46 std::io::ErrorKind::NotFound,
47 format!("key not found: {key}"),
48 ))
49 })
50 }
51
52 fn write(&self, key: &BackendKey, data: &[u8]) -> Result<()> {
53 self.inner.lock().insert(key.clone(), data.to_vec());
54 Ok(())
55 }
56
57 fn delete(&self, key: &BackendKey) -> Result<()> {
58 self.inner.lock().remove(key);
59 Ok(())
60 }
61
62 fn list(&self, prefix: &str) -> Result<Vec<BackendKey>> {
63 Ok(self
64 .inner
65 .lock()
66 .keys()
67 .filter(|k| k.as_str().starts_with(prefix))
68 .cloned()
69 .collect())
70 }
71
72 fn exists(&self, key: &BackendKey) -> Result<bool> {
73 Ok(self.inner.lock().contains_key(key))
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 /// **Proves:** `MemoryBackend` satisfies the [`KeychainBackend`]
82 /// contract end-to-end — write then read recovers the blob; `exists`
83 /// returns `true` for written keys and `false` for deleted ones.
84 ///
85 /// **Why it matters:** Dependent crates (`apps/validator`,
86 /// `dig-l1-wallet`) build their tests on `MemoryBackend`. If the
87 /// in-memory backend drifted from the `FileBackend` semantics (e.g.,
88 /// `exists` stayed `true` after delete, or `read` returned stale bytes
89 /// after overwrite), those tests would pass in CI and fail in
90 /// production.
91 ///
92 /// **Catches:** a regression in `delete` that leaks the key in the
93 /// internal `HashMap`, or an `exists` override that short-circuits
94 /// without consulting the map.
95 #[test]
96 fn roundtrip() {
97 let be = MemoryBackend::new();
98 let k = BackendKey::new("x");
99 be.write(&k, b"data").unwrap();
100 assert_eq!(be.read(&k).unwrap(), b"data");
101 assert!(be.exists(&k).unwrap());
102 be.delete(&k).unwrap();
103 assert!(!be.exists(&k).unwrap());
104 }
105
106 /// **Proves:** reading a key that was never written returns a
107 /// [`KeystoreError::Backend`] whose inner `io::Error` is
108 /// [`ErrorKind::NotFound`].
109 ///
110 /// **Why it matters:** The exact error *kind* is load-bearing — the default
111 /// [`KeychainBackend::exists`] and `Keystore::create`'s overwrite guard both
112 /// branch on `NotFound` specifically. If `MemoryBackend::read` reported a
113 /// missing key as some other error kind, callers that wrap it (e.g.
114 /// `dig-l1-wallet`'s scratch-backend decrypt path) would treat "absent" as a
115 /// hard failure.
116 ///
117 /// **Catches:** a regression that returns a generic/`Other` error, or that
118 /// returns `Ok(empty)` for a missing key.
119 #[test]
120 fn read_missing_key_is_not_found() {
121 let be = MemoryBackend::new();
122 let err = be.read(&BackendKey::new("absent")).unwrap_err();
123 match err {
124 KeystoreError::Backend(io) => {
125 assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
126 }
127 other => panic!("expected Backend(NotFound), got {other:?}"),
128 }
129 }
130
131 /// **Proves:** `write` to an existing key overwrites in place — a later
132 /// `read` sees the new bytes, never a concatenation or the stale value.
133 ///
134 /// **Why it matters:** Password rotation and KDF rotation re-`write` the
135 /// same backend key with fresh ciphertext. If `MemoryBackend` appended or
136 /// kept the old value, an `unlock` after rotation would decrypt stale
137 /// ciphertext with the new key and fail.
138 ///
139 /// **Catches:** a `write` that uses `entry().or_insert` (ignoring updates)
140 /// or otherwise fails to replace the prior blob.
141 #[test]
142 fn write_overwrites_in_place() {
143 let be = MemoryBackend::new();
144 let k = BackendKey::new("k");
145 be.write(&k, b"first").unwrap();
146 be.write(&k, b"second").unwrap();
147 assert_eq!(be.read(&k).unwrap(), b"second");
148 }
149
150 /// **Proves:** `list` returns exactly the keys whose name starts with the
151 /// given prefix, and an empty prefix lists everything.
152 ///
153 /// **Why it matters:** Callers enumerate keystores by prefix (e.g. listing
154 /// all `validator/` keys). A prefix filter that matched substrings anywhere,
155 /// or ignored the prefix entirely, would surface unrelated keys to the
156 /// operator.
157 ///
158 /// **Catches:** using `contains` instead of `starts_with`; returning all
159 /// keys regardless of prefix.
160 #[test]
161 fn list_filters_by_prefix() {
162 let be = MemoryBackend::new();
163 be.write(&BackendKey::new("validator/a"), b"1").unwrap();
164 be.write(&BackendKey::new("validator/b"), b"2").unwrap();
165 be.write(&BackendKey::new("wallet/c"), b"3").unwrap();
166
167 let mut matched: Vec<String> = be
168 .list("validator/")
169 .unwrap()
170 .into_iter()
171 .map(|k| k.as_str().to_string())
172 .collect();
173 matched.sort();
174 assert_eq!(matched, vec!["validator/a", "validator/b"]);
175
176 // An empty prefix matches every key.
177 assert_eq!(be.list("").unwrap().len(), 3);
178 // A non-matching prefix yields nothing.
179 assert!(be.list("none/").unwrap().is_empty());
180 }
181
182 /// **Proves:** `MemoryBackend::default()` produces an empty backend
183 /// equivalent to `new()`.
184 ///
185 /// **Why it matters:** Production adapters construct the scratch backend via
186 /// `MemoryBackend::default()` (it derives `Default`). An accidental
187 /// non-empty or mis-initialised `Default` would leak state between
188 /// independent encrypt/decrypt operations.
189 ///
190 /// **Catches:** a hand-written `Default` that pre-populates the map.
191 #[test]
192 fn default_is_empty() {
193 let be = MemoryBackend::default();
194 assert!(be.list("").unwrap().is_empty());
195 assert!(!be.exists(&BackendKey::new("anything")).unwrap());
196 }
197}