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, Zeroizing};
11
12use super::{
13    error::{StorageError, 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    /// Wraps an already resolved 32-byte database key, for example one derived from a
53    /// passkey PRF in the browser. See [`super::CredentialStore::with_keys`].
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if the key is not exactly 32 bytes.
58    pub fn from_bytes(database_key: Vec<u8>) -> StorageResult<Self> {
59        let database_key = Zeroizing::new(database_key);
60        if database_key.len() != 32 {
61            return Err(StorageError::InvalidInput(
62                "expected a 32-byte database key".to_string(),
63            ));
64        }
65        // Copy straight into the boxed key; `init_with` would leave a stack copy.
66        Ok(Self {
67            intermediate_key: SecretBox::init_with_mut(|key: &mut [u8; 32]| {
68                key.copy_from_slice(&database_key);
69            }),
70        })
71    }
72
73    /// Returns a reference to the intermediate key's [`SecretBox`].
74    #[must_use]
75    pub const fn intermediate_key(&self) -> &SecretBox<[u8; 32]> {
76        &self.intermediate_key
77    }
78}
79
80// Trait-object bridge from walletkit-core's uniffi-annotated traits onto
81// walletkit-db's plain-Rust trait surface. Required because Rust's orphan
82// rule prevents a blanket impl across crates. `Keystore::seal` borrows its
83// plaintext (see walletkit-db/src/traits.rs); `Ks::seal` is the single
84// point where the secret is copied into an owned `Vec<u8>`, because
85// `DeviceKeystore` is a uniffi callback interface and those only support
86// pass-by-value parameters (no `&[u8]`). That copy — and any further copy
87// the foreign (Swift/Kotlin/etc.) implementation makes on its own side — is
88// outside Rust's control; this is an accepted uniffi limitation, not a bug.
89
90struct Ks<'a>(&'a dyn DeviceKeystore);
91impl walletkit_db::Keystore for Ks<'_> {
92    fn seal(&self, aad: &[u8], pt: &[u8]) -> walletkit_db::StoreResult<Vec<u8>> {
93        self.0
94            .seal(aad.to_vec(), pt.to_vec())
95            .map_err(|e| walletkit_db::StoreError::Keystore(e.to_string()))
96    }
97    fn open_sealed(
98        &self,
99        aad: Vec<u8>,
100        ct: Vec<u8>,
101    ) -> walletkit_db::StoreResult<Vec<u8>> {
102        self.0
103            .open_sealed(aad, ct)
104            .map_err(|e| walletkit_db::StoreError::Keystore(e.to_string()))
105    }
106}
107
108struct Bs<'a>(&'a dyn AtomicBlobStore);
109impl walletkit_db::AtomicBlobStore for Bs<'_> {
110    fn read(&self, path: String) -> walletkit_db::StoreResult<Option<Vec<u8>>> {
111        self.0
112            .read(path)
113            .map_err(|e| walletkit_db::StoreError::BlobStore(e.to_string()))
114    }
115    fn write_atomic(
116        &self,
117        path: String,
118        bytes: Vec<u8>,
119    ) -> walletkit_db::StoreResult<()> {
120        self.0
121            .write_atomic(path, bytes)
122            .map_err(|e| walletkit_db::StoreError::BlobStore(e.to_string()))
123    }
124    fn delete(&self, path: String) -> walletkit_db::StoreResult<()> {
125        self.0
126            .delete(path)
127            .map_err(|e| walletkit_db::StoreError::BlobStore(e.to_string()))
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::storage::error::StorageError;
135    use crate::storage::tests_utils::{InMemoryBlobStore, InMemoryKeystore};
136    use secrecy::ExposeSecret;
137    use uuid::Uuid;
138    use walletkit_db::Lock;
139
140    fn temp_lock_path() -> std::path::PathBuf {
141        let mut path = std::env::temp_dir();
142        path.push(format!("walletkit-keys-lock-{}.lock", Uuid::new_v4()));
143        path
144    }
145
146    #[test]
147    fn test_storage_keys_round_trip() {
148        let keystore = InMemoryKeystore::new();
149        let blob_store = InMemoryBlobStore::new();
150        let lock_path = temp_lock_path();
151        let lock = Lock::open(&lock_path).expect("open lock");
152        let keys_first =
153            StorageKeys::init(&keystore, &blob_store, &lock, 100).expect("init");
154        let keys_second =
155            StorageKeys::init(&keystore, &blob_store, &lock, 200).expect("init");
156
157        assert_eq!(
158            keys_first.intermediate_key.expose_secret(),
159            keys_second.intermediate_key.expose_secret()
160        );
161        let _ = std::fs::remove_file(lock_path);
162    }
163
164    #[test]
165    fn test_storage_keys_keystore_mismatch_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 other_keystore = InMemoryKeystore::new();
173        match StorageKeys::init(&other_keystore, &blob_store, &lock, 456) {
174            Err(
175                StorageError::Crypto(_)
176                | StorageError::InvalidEnvelope(_)
177                | StorageError::Keystore(_),
178            ) => {}
179            Err(err) => panic!("unexpected error: {err}"),
180            Ok(_) => panic!("expected error"),
181        }
182        let _ = std::fs::remove_file(lock_path);
183    }
184
185    #[test]
186    fn test_storage_keys_tampered_envelope_fails() {
187        let keystore = InMemoryKeystore::new();
188        let blob_store = InMemoryBlobStore::new();
189        let lock_path = temp_lock_path();
190        let lock = Lock::open(&lock_path).expect("open lock");
191        StorageKeys::init(&keystore, &blob_store, &lock, 123).expect("init");
192
193        let mut bytes = blob_store
194            .read(ACCOUNT_KEYS_FILENAME.to_string())
195            .expect("read")
196            .expect("present");
197        bytes[0] ^= 0xFF;
198        blob_store
199            .write_atomic(ACCOUNT_KEYS_FILENAME.to_string(), bytes)
200            .expect("write");
201
202        match StorageKeys::init(&keystore, &blob_store, &lock, 456) {
203            Err(
204                StorageError::Serialization(_)
205                | StorageError::Crypto(_)
206                | StorageError::UnsupportedEnvelopeVersion(_),
207            ) => {}
208            Err(err) => panic!("unexpected error: {err}"),
209            Ok(_) => panic!("expected error"),
210        }
211        let _ = std::fs::remove_file(lock_path);
212    }
213}