Skip to main content

walletkit_core/storage/
keys.rs

1//! Key hierarchy management for credential storage.
2
3use 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/// In-memory account keys derived from the account key envelope.
15///
16/// Keys are held in memory for the lifetime of the storage handle.
17#[derive(Zeroize, ZeroizeOnDrop)]
18#[allow(clippy::struct_field_names)]
19pub struct StorageKeys {
20    intermediate_key: [u8; 32],
21}
22
23impl StorageKeys {
24    /// Initializes storage keys by opening or creating the account key envelope.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if the envelope cannot be read, decrypted, or parsed,
29    /// or if persistence to the blob store fails.
30    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    /// Returns the intermediate key. Treat this as sensitive material.
62    #[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}