walletkit_core/storage/
keys.rs1use rand::{rngs::OsRng, RngCore};
4use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
5
6use super::{
7 envelope::AccountKeyEnvelope,
8 error::{StorageError, StorageResult},
9 lock::StorageLockGuard,
10 traits::{AtomicBlobStore, DeviceKeystore},
11 ACCOUNT_KEYS_FILENAME, ACCOUNT_KEY_ENVELOPE_AD,
12};
13
14#[derive(Zeroize, ZeroizeOnDrop)]
18#[allow(clippy::struct_field_names)]
19pub struct StorageKeys {
20 intermediate_key: [u8; 32],
21}
22
23impl StorageKeys {
24 pub fn init(
31 keystore: &dyn DeviceKeystore,
32 blob_store: &dyn AtomicBlobStore,
33 _lock: &StorageLockGuard,
34 now: u64,
35 ) -> StorageResult<Self> {
36 if let Some(bytes) = blob_store.read(ACCOUNT_KEYS_FILENAME.to_string())? {
37 let envelope = AccountKeyEnvelope::deserialize(&bytes)?;
38 let wrapped_k_intermediate = envelope.wrapped_k_intermediate.clone();
39 let k_intermediate_bytes = Zeroizing::new(keystore.open_sealed(
40 ACCOUNT_KEY_ENVELOPE_AD.to_vec(),
41 wrapped_k_intermediate,
42 )?);
43 let k_intermediate =
44 parse_key_32(k_intermediate_bytes.as_slice(), "K_intermediate")?;
45 Ok(Self {
46 intermediate_key: k_intermediate,
47 })
48 } else {
49 let k_intermediate = random_key();
50 let wrapped_k_intermediate = keystore
51 .seal(ACCOUNT_KEY_ENVELOPE_AD.to_vec(), k_intermediate.to_vec())?;
52 let envelope = AccountKeyEnvelope::new(wrapped_k_intermediate, now);
53 let bytes = envelope.serialize()?;
54 blob_store.write_atomic(ACCOUNT_KEYS_FILENAME.to_string(), bytes)?;
55 Ok(Self {
56 intermediate_key: k_intermediate,
57 })
58 }
59 }
60
61 #[must_use]
63 pub const fn intermediate_key(&self) -> [u8; 32] {
64 self.intermediate_key
65 }
66}
67
68fn random_key() -> [u8; 32] {
69 let mut key = [0u8; 32];
70 OsRng.fill_bytes(&mut key);
71 key
72}
73
74fn parse_key_32(bytes: &[u8], label: &str) -> StorageResult<[u8; 32]> {
75 if bytes.len() != 32 {
76 return Err(StorageError::InvalidEnvelope(format!(
77 "{label} length mismatch: expected 32, got {}",
78 bytes.len()
79 )));
80 }
81 let mut out = [0u8; 32];
82 out.copy_from_slice(bytes);
83 Ok(out)
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::storage::lock::StorageLock;
90 use crate::storage::tests_utils::{InMemoryBlobStore, InMemoryKeystore};
91 use uuid::Uuid;
92
93 fn temp_lock_path() -> std::path::PathBuf {
94 let mut path = std::env::temp_dir();
95 path.push(format!("walletkit-keys-lock-{}.lock", Uuid::new_v4()));
96 path
97 }
98
99 #[test]
100 fn test_storage_keys_round_trip() {
101 let keystore = InMemoryKeystore::new();
102 let blob_store = InMemoryBlobStore::new();
103 let lock_path = temp_lock_path();
104 let lock = StorageLock::open(&lock_path).expect("open lock");
105 let guard = lock.lock().expect("lock");
106 let keys_first =
107 StorageKeys::init(&keystore, &blob_store, &guard, 100).expect("init");
108 let keys_second =
109 StorageKeys::init(&keystore, &blob_store, &guard, 200).expect("init");
110
111 assert_eq!(keys_first.intermediate_key, keys_second.intermediate_key);
112 let _ = std::fs::remove_file(lock_path);
113 }
114
115 #[test]
116 fn test_storage_keys_keystore_mismatch_fails() {
117 let keystore = InMemoryKeystore::new();
118 let blob_store = InMemoryBlobStore::new();
119 let lock_path = temp_lock_path();
120 let lock = StorageLock::open(&lock_path).expect("open lock");
121 let guard = lock.lock().expect("lock");
122 StorageKeys::init(&keystore, &blob_store, &guard, 123).expect("init");
123
124 let other_keystore = InMemoryKeystore::new();
125 match StorageKeys::init(&other_keystore, &blob_store, &guard, 456) {
126 Err(
127 StorageError::Crypto(_)
128 | StorageError::InvalidEnvelope(_)
129 | StorageError::Keystore(_),
130 ) => {}
131 Err(err) => panic!("unexpected error: {err}"),
132 Ok(_) => panic!("expected error"),
133 }
134 let _ = std::fs::remove_file(lock_path);
135 }
136
137 #[test]
138 fn test_storage_keys_tampered_envelope_fails() {
139 let keystore = InMemoryKeystore::new();
140 let blob_store = InMemoryBlobStore::new();
141 let lock_path = temp_lock_path();
142 let lock = StorageLock::open(&lock_path).expect("open lock");
143 let guard = lock.lock().expect("lock");
144 StorageKeys::init(&keystore, &blob_store, &guard, 123).expect("init");
145
146 let mut bytes = blob_store
147 .read(ACCOUNT_KEYS_FILENAME.to_string())
148 .expect("read")
149 .expect("present");
150 bytes[0] ^= 0xFF;
151 blob_store
152 .write_atomic(ACCOUNT_KEYS_FILENAME.to_string(), bytes)
153 .expect("write");
154
155 match StorageKeys::init(&keystore, &blob_store, &guard, 456) {
156 Err(
157 StorageError::Serialization(_)
158 | StorageError::Crypto(_)
159 | StorageError::UnsupportedEnvelopeVersion(_),
160 ) => {}
161 Err(err) => panic!("unexpected error: {err}"),
162 Ok(_) => panic!("expected error"),
163 }
164 let _ = std::fs::remove_file(lock_path);
165 }
166}