Skip to main content

walletkit_core/storage/
keys.rs

1//! Key management for credential storage.
2//!
3//! [`StorageKeys`] opens (or creates on first use) the account key envelope via
4//! `walletkit-db` and holds the resulting `K_intermediate` in memory for the lifetime
5//! of the storage handle; both databases are opened with it. The `K_device` →
6//! `K_intermediate` hierarchy, envelope sealing, and encryption are described in the
7//! `walletkit-db` README.
8
9use secrecy::SecretBox;
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12use super::{
13    error::StorageResult,
14    traits::{AtomicBlobStore, DeviceKeystore},
15    ACCOUNT_KEYS_FILENAME, ACCOUNT_KEY_ENVELOPE_AD,
16};
17use walletkit_db::Lock;
18
19/// In-memory account keys derived from the account key envelope.
20///
21/// Keys are held in memory for the lifetime of the storage handle.
22#[derive(Zeroize, ZeroizeOnDrop)]
23#[allow(clippy::struct_field_names)]
24pub struct StorageKeys {
25    intermediate_key: SecretBox<[u8; 32]>,
26}
27
28impl StorageKeys {
29    /// Initializes storage keys by opening or creating the account key envelope.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the envelope cannot be read, decrypted, or parsed,
34    /// or if persistence to the blob store fails.
35    pub fn init(
36        keystore: &dyn DeviceKeystore,
37        blob_store: &dyn AtomicBlobStore,
38        lock: &Lock,
39        now: u64,
40    ) -> StorageResult<Self> {
41        let intermediate_key = walletkit_db::init_or_open_envelope_key(
42            &Ks(keystore),
43            &Bs(blob_store),
44            lock,
45            ACCOUNT_KEYS_FILENAME,
46            ACCOUNT_KEY_ENVELOPE_AD,
47            now,
48        )?;
49        Ok(Self { intermediate_key })
50    }
51
52    /// Returns a reference to the intermediate key's [`SecretBox`].
53    #[must_use]
54    pub const fn intermediate_key(&self) -> &SecretBox<[u8; 32]> {
55        &self.intermediate_key
56    }
57}
58
59// Trait-object bridge from walletkit-core's uniffi-annotated traits onto
60// walletkit-db's plain-Rust trait surface. Required because Rust's orphan
61// rule prevents a blanket impl across crates. `Keystore::seal` borrows its
62// plaintext (see walletkit-db/src/traits.rs); `Ks::seal` is the single
63// point where the secret is copied into an owned `Vec<u8>`, because
64// `DeviceKeystore` is a uniffi callback interface and those only support
65// pass-by-value parameters (no `&[u8]`). That copy — and any further copy
66// the foreign (Swift/Kotlin/etc.) implementation makes on its own side — is
67// outside Rust's control; this is an accepted uniffi limitation, not a bug.
68
69struct Ks<'a>(&'a dyn DeviceKeystore);
70impl walletkit_db::Keystore for Ks<'_> {
71    fn seal(&self, aad: &[u8], pt: &[u8]) -> walletkit_db::StoreResult<Vec<u8>> {
72        self.0
73            .seal(aad.to_vec(), pt.to_vec())
74            .map_err(|e| walletkit_db::StoreError::Keystore(e.to_string()))
75    }
76    fn open_sealed(
77        &self,
78        aad: Vec<u8>,
79        ct: Vec<u8>,
80    ) -> walletkit_db::StoreResult<Vec<u8>> {
81        self.0
82            .open_sealed(aad, ct)
83            .map_err(|e| walletkit_db::StoreError::Keystore(e.to_string()))
84    }
85}
86
87struct Bs<'a>(&'a dyn AtomicBlobStore);
88impl walletkit_db::AtomicBlobStore for Bs<'_> {
89    fn read(&self, path: String) -> walletkit_db::StoreResult<Option<Vec<u8>>> {
90        self.0
91            .read(path)
92            .map_err(|e| walletkit_db::StoreError::BlobStore(e.to_string()))
93    }
94    fn write_atomic(
95        &self,
96        path: String,
97        bytes: Vec<u8>,
98    ) -> walletkit_db::StoreResult<()> {
99        self.0
100            .write_atomic(path, bytes)
101            .map_err(|e| walletkit_db::StoreError::BlobStore(e.to_string()))
102    }
103    fn delete(&self, path: String) -> walletkit_db::StoreResult<()> {
104        self.0
105            .delete(path)
106            .map_err(|e| walletkit_db::StoreError::BlobStore(e.to_string()))
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::storage::error::StorageError;
114    use crate::storage::tests_utils::{InMemoryBlobStore, InMemoryKeystore};
115    use secrecy::ExposeSecret;
116    use uuid::Uuid;
117    use walletkit_db::Lock;
118
119    fn temp_lock_path() -> std::path::PathBuf {
120        let mut path = std::env::temp_dir();
121        path.push(format!("walletkit-keys-lock-{}.lock", Uuid::new_v4()));
122        path
123    }
124
125    #[test]
126    fn test_storage_keys_round_trip() {
127        let keystore = InMemoryKeystore::new();
128        let blob_store = InMemoryBlobStore::new();
129        let lock_path = temp_lock_path();
130        let lock = Lock::open(&lock_path).expect("open lock");
131        let keys_first =
132            StorageKeys::init(&keystore, &blob_store, &lock, 100).expect("init");
133        let keys_second =
134            StorageKeys::init(&keystore, &blob_store, &lock, 200).expect("init");
135
136        assert_eq!(
137            keys_first.intermediate_key.expose_secret(),
138            keys_second.intermediate_key.expose_secret()
139        );
140        let _ = std::fs::remove_file(lock_path);
141    }
142
143    #[test]
144    fn test_storage_keys_keystore_mismatch_fails() {
145        let keystore = InMemoryKeystore::new();
146        let blob_store = InMemoryBlobStore::new();
147        let lock_path = temp_lock_path();
148        let lock = Lock::open(&lock_path).expect("open lock");
149        StorageKeys::init(&keystore, &blob_store, &lock, 123).expect("init");
150
151        let other_keystore = InMemoryKeystore::new();
152        match StorageKeys::init(&other_keystore, &blob_store, &lock, 456) {
153            Err(
154                StorageError::Crypto(_)
155                | StorageError::InvalidEnvelope(_)
156                | StorageError::Keystore(_),
157            ) => {}
158            Err(err) => panic!("unexpected error: {err}"),
159            Ok(_) => panic!("expected error"),
160        }
161        let _ = std::fs::remove_file(lock_path);
162    }
163
164    #[test]
165    fn test_storage_keys_tampered_envelope_fails() {
166        let keystore = InMemoryKeystore::new();
167        let blob_store = InMemoryBlobStore::new();
168        let lock_path = temp_lock_path();
169        let lock = Lock::open(&lock_path).expect("open lock");
170        StorageKeys::init(&keystore, &blob_store, &lock, 123).expect("init");
171
172        let mut bytes = blob_store
173            .read(ACCOUNT_KEYS_FILENAME.to_string())
174            .expect("read")
175            .expect("present");
176        bytes[0] ^= 0xFF;
177        blob_store
178            .write_atomic(ACCOUNT_KEYS_FILENAME.to_string(), bytes)
179            .expect("write");
180
181        match StorageKeys::init(&keystore, &blob_store, &lock, 456) {
182            Err(
183                StorageError::Serialization(_)
184                | StorageError::Crypto(_)
185                | StorageError::UnsupportedEnvelopeVersion(_),
186            ) => {}
187            Err(err) => panic!("unexpected error: {err}"),
188            Ok(_) => panic!("expected error"),
189        }
190        let _ = std::fs::remove_file(lock_path);
191    }
192}