walletkit_core/storage/
keys.rs1use 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#[derive(Zeroize, ZeroizeOnDrop)]
23#[allow(clippy::struct_field_names)]
24pub struct StorageKeys {
25 intermediate_key: SecretBox<[u8; 32]>,
26}
27
28impl StorageKeys {
29 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 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 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 #[must_use]
75 pub const fn intermediate_key(&self) -> &SecretBox<[u8; 32]> {
76 &self.intermediate_key
77 }
78}
79
80struct 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}