Skip to main content

dig_keystore/backend/
mod.rs

1//! Storage backend abstraction.
2//!
3//! A `KeychainBackend` is any byte-blob KV store. The shipped `FileBackend`
4//! persists to the local filesystem with atomic (tmp + rename) writes. Planned
5//! future backends: `OsKeyringBackend` (macOS Keychain / Windows Credential
6//! Store / Secret Service), and hardware-signer backends (`LedgerBackend`,
7//! `YubiHsmBackend`) that proxy `sign` to an external device.
8
9use crate::error::Result;
10
11#[cfg(feature = "file-backend")]
12mod file;
13mod memory;
14
15#[cfg(feature = "file-backend")]
16pub use file::FileBackend;
17/// In-memory backend — always available. Originally feature-gated, now
18/// unconditional because production adapters (e.g., `dig-l1-wallet`'s
19/// encrypt/decrypt-bytes helpers) wrap it in scratch backends to reuse the
20/// full keystore format without touching the filesystem.
21pub use memory::MemoryBackend;
22
23/// An opaque key identifying a single encrypted blob within a backend.
24///
25/// For `FileBackend`, the key maps to `<root>/<key>.dks`; for an OS-keyring
26/// backend it maps to a service / account pair; for a hardware signer it maps
27/// to a slot identifier.
28#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub struct BackendKey(pub String);
30
31impl BackendKey {
32    /// Construct from any string-like value.
33    pub fn new(name: impl Into<String>) -> Self {
34        Self(name.into())
35    }
36
37    /// Borrow as `&str`.
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41}
42
43impl<T: Into<String>> From<T> for BackendKey {
44    fn from(v: T) -> Self {
45        Self(v.into())
46    }
47}
48
49impl std::fmt::Display for BackendKey {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.write_str(&self.0)
52    }
53}
54
55/// Storage backend trait. Implementations must be `Send + Sync + 'static` so
56/// they can be held behind `Arc<dyn KeychainBackend>`.
57pub trait KeychainBackend: Send + Sync + 'static {
58    /// Read the full contents of the blob at `key`.
59    ///
60    /// Returns a backend I/O error if the blob does not exist.
61    fn read(&self, key: &BackendKey) -> Result<Vec<u8>>;
62
63    /// Write `data` to `key`. Implementations should be atomic — a reader
64    /// seeing the key after this call must see either the old bytes or the
65    /// new bytes in full, never a torn mix.
66    fn write(&self, key: &BackendKey, data: &[u8]) -> Result<()>;
67
68    /// Remove the blob at `key`. Implementations should best-effort overwrite
69    /// the storage before removing so residual disk sectors do not retain the
70    /// ciphertext.
71    fn delete(&self, key: &BackendKey) -> Result<()>;
72
73    /// List keys that start with `prefix`. Order is unspecified.
74    fn list(&self, prefix: &str) -> Result<Vec<BackendKey>>;
75
76    /// Whether a blob exists at `key`. Default impl delegates to `read`;
77    /// backends with cheaper existence checks should override.
78    fn exists(&self, key: &BackendKey) -> Result<bool> {
79        match self.read(key) {
80            Ok(_) => Ok(true),
81            Err(crate::error::KeystoreError::Backend(e))
82                if e.kind() == std::io::ErrorKind::NotFound =>
83            {
84                Ok(false)
85            }
86            Err(e) => Err(e),
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::error::KeystoreError;
95    use std::io;
96
97    /// A minimal backend that does **not** override `exists`, so it exercises
98    /// the [`KeychainBackend::exists`] *default* implementation. The shipped
99    /// backends (`FileBackend`, `MemoryBackend`) both override `exists` with a
100    /// cheaper check, leaving the default's three branches (present →
101    /// `Ok(true)`, `NotFound` → `Ok(false)`, other error → propagate) otherwise
102    /// unexercised. This stub is the only way to test that contract.
103    #[derive(Default)]
104    struct ProbeBackend {
105        /// When set, every `read` returns this io error kind instead of data.
106        fail_kind: Option<io::ErrorKind>,
107        present: bool,
108    }
109
110    impl KeychainBackend for ProbeBackend {
111        fn read(&self, _key: &BackendKey) -> Result<Vec<u8>> {
112            if let Some(kind) = self.fail_kind {
113                return Err(KeystoreError::from(io::Error::new(kind, "probe")));
114            }
115            if self.present {
116                Ok(vec![1, 2, 3])
117            } else {
118                Err(KeystoreError::from(io::Error::new(
119                    io::ErrorKind::NotFound,
120                    "absent",
121                )))
122            }
123        }
124        fn write(&self, _key: &BackendKey, _data: &[u8]) -> Result<()> {
125            Ok(())
126        }
127        fn delete(&self, _key: &BackendKey) -> Result<()> {
128            Ok(())
129        }
130        fn list(&self, _prefix: &str) -> Result<Vec<BackendKey>> {
131            Ok(vec![])
132        }
133        // Deliberately NO `exists` override.
134    }
135
136    /// **Proves:** `BackendKey` round-trips through every constructor + accessor
137    /// — `new`, the blanket `From<T: Into<String>>`, `as_str`, and the
138    /// `Display` impl all agree on the same underlying string.
139    ///
140    /// **Why it matters:** `BackendKey` is the address every backend keys off
141    /// (`FileBackend` maps it to `<root>/<key>.dks`). A `Display`/`as_str`
142    /// disagreement, or a `From` that mangled the input, would route reads and
143    /// writes to different paths — a silent data-loss bug.
144    ///
145    /// **Catches:** an `as_str` that returns a transformed copy; a `Display`
146    /// impl that adds quotes/prefixes; a `From` that drops or alters the value.
147    #[test]
148    fn backend_key_constructors_and_accessors_agree() {
149        let from_new = BackendKey::new("validator");
150        let from_into: BackendKey = "validator".into();
151        let from_string: BackendKey = BackendKey::from(String::from("validator"));
152
153        assert_eq!(from_new.as_str(), "validator");
154        assert_eq!(from_new, from_into);
155        assert_eq!(from_new, from_string);
156        assert_eq!(format!("{from_new}"), "validator");
157        // Eq / Hash derive sanity: distinct values are not equal.
158        assert_ne!(from_new, BackendKey::new("other"));
159    }
160
161    /// **Proves:** the default [`KeychainBackend::exists`] returns `Ok(true)`
162    /// when the blob reads back successfully.
163    ///
164    /// **Why it matters:** This is the happy-path branch of the default impl
165    /// that backends inherit unless they override it. `Keystore::create` calls
166    /// `exists` to refuse overwrites; if the default returned `false` for a
167    /// present key, `create` would clobber existing keys.
168    ///
169    /// **Catches:** an inverted truth value in the `Ok(_) => Ok(true)` arm.
170    #[test]
171    fn default_exists_true_when_present() {
172        let be = ProbeBackend {
173            present: true,
174            ..Default::default()
175        };
176        assert!(be.exists(&BackendKey::new("k")).unwrap());
177    }
178
179    /// **Proves:** the default `exists` maps a `NotFound` read error to
180    /// `Ok(false)` rather than propagating it.
181    ///
182    /// **Why it matters:** A missing key is the normal "not yet created" state,
183    /// not an error. `Keystore::create` relies on `exists(..) == Ok(false)` to
184    /// proceed with a first-time write. If `NotFound` propagated as `Err`,
185    /// creating any new keystore would fail outright.
186    ///
187    /// **Catches:** removing the `NotFound` guard so absence surfaces as an
188    /// error.
189    #[test]
190    fn default_exists_false_when_not_found() {
191        let be = ProbeBackend::default(); // present=false → NotFound
192        assert!(!be.exists(&BackendKey::new("k")).unwrap());
193    }
194
195    /// **Proves:** the default `exists` propagates non-`NotFound` I/O errors
196    /// (e.g. a permission error) instead of swallowing them as `false`.
197    ///
198    /// **Why it matters:** Treating a `PermissionDenied` as "does not exist"
199    /// would let `create` attempt to overwrite a file it merely cannot read —
200    /// masking a real environment problem behind a confusing later failure.
201    /// The default must distinguish "absent" from "inaccessible".
202    ///
203    /// **Catches:** a too-broad error arm that maps every error to `Ok(false)`.
204    #[test]
205    fn default_exists_propagates_other_errors() {
206        let be = ProbeBackend {
207            fail_kind: Some(io::ErrorKind::PermissionDenied),
208            ..Default::default()
209        };
210        let err = be.exists(&BackendKey::new("k")).unwrap_err();
211        assert!(matches!(err, KeystoreError::Backend(_)));
212    }
213}