Skip to main content

hashiverse_lib/client/key_locker/
disk_key_locker.rs

1//! # File-backed key locker
2//!
3//! Persists each account's encrypted `Keys` to `{data_dir}/key_locker/{public_key_hex}.key`
4//! using [`crate::tools::keys::Keys::to_persistence`] (passphrase-protected ChaCha20Poly1305
5//! over an Argon2-derived key). On load, the stored passphrase unlocks the file back into an
6//! in-memory [`crate::client::key_locker::key_locker::KeyLocker`].
7//!
8//! A `TempDirHandle` fallback is used when no persistent data directory is configured,
9//! so headless tools and tests still get a functional locker with correctly-typed paths.
10//! Used by the native server binary and desktop client.
11
12use crate::client::key_locker::key_locker::{KeyLocker, KeyLockerManager, GUEST_CLIENT_ID};
13use crate::tools::client_id::ClientId;
14use crate::tools::keys::Keys;
15use crate::tools::signing;
16use crate::tools::types::Signature;
17use crate::tools::tools::TempDirHandle;
18use anyhow::anyhow;
19use std::collections::HashMap;
20use std::path::PathBuf;
21use std::sync::Arc;
22use parking_lot::RwLock;
23
24/// A key locker that holds the signing key in memory, with persistence to disk.
25pub struct DiskKeyLocker {
26    keys: Keys,
27    client_id: ClientId,
28}
29
30#[async_trait::async_trait]
31impl KeyLocker for DiskKeyLocker {
32    fn client_id(&self) -> &ClientId {
33        &self.client_id
34    }
35
36    async fn sign(&self, data: &[u8]) -> anyhow::Result<Signature> {
37        Ok(signing::sign(&self.keys.signature_key, data))
38    }
39}
40
41/// File-backed key locker manager. Keys are stored as encrypted files in
42/// `{data_dir}/key_locker/{public_key_hex}.key`, encrypted via `Keys::to_persistence()`.
43///
44/// The `passphrase` is used to encrypt/decrypt key files at rest. Use an empty
45/// string for bots that don't need key encryption.
46pub struct DiskKeyLockerManager {
47    key_locker_dir: PathBuf,
48    passphrase: String,
49    loaded_keys: Arc<RwLock<HashMap<String, Arc<DiskKeyLocker>>>>,
50    /// Holds the temp dir handle to prevent cleanup when created via `new()`.
51    _temp_dir_handle: Option<TempDirHandle>,
52}
53
54impl DiskKeyLockerManager {
55    pub fn with_data_dir(data_dir: PathBuf, passphrase: String) -> anyhow::Result<Arc<Self>> {
56        let key_locker_dir = data_dir.join("key_locker");
57        std::fs::create_dir_all(&key_locker_dir)?;
58        let manager = Arc::new(Self {
59            key_locker_dir,
60            passphrase,
61            loaded_keys: Arc::new(RwLock::new(HashMap::new())),
62            _temp_dir_handle: None,
63        });
64        Ok(manager)
65    }
66
67    fn key_file_path(&self, key_public_hex: &str) -> PathBuf {
68        self.key_locker_dir.join(format!("{}.key", key_public_hex))
69    }
70}
71
72impl KeyLockerManager<DiskKeyLocker> for DiskKeyLockerManager {
73    /// Warning: stores keys in a temporary folder that will be cleaned up when this manager is dropped.
74    /// Use `with_data_dir()` for persistent storage.
75    async fn new() -> anyhow::Result<Arc<Self>> {
76        let (temp_dir_handle, temp_dir_path) = crate::tools::tools::get_temp_dir()?;
77        let key_locker_dir = PathBuf::from(&temp_dir_path).join("key_locker");
78        std::fs::create_dir_all(&key_locker_dir)?;
79        Ok(Arc::new(Self {
80            key_locker_dir,
81            passphrase: String::new(),
82            loaded_keys: Arc::new(RwLock::new(HashMap::new())),
83            _temp_dir_handle: Some(temp_dir_handle),
84        }))
85    }
86
87    async fn list(&self) -> anyhow::Result<Vec<String>> {
88        let mut key_ids = Vec::new();
89        for entry in std::fs::read_dir(&self.key_locker_dir)? {
90            let entry = entry?;
91            let file_name = entry.file_name().to_string_lossy().to_string();
92            if let Some(key_public_hex) = file_name.strip_suffix(".key") {
93                if key_public_hex != GUEST_CLIENT_ID {
94                    key_ids.push(key_public_hex.to_string());
95                }
96            }
97        }
98        Ok(key_ids)
99    }
100
101    async fn create(&self, key_phrase: String) -> anyhow::Result<Arc<DiskKeyLocker>> {
102        let keys = Keys::from_phrase(&key_phrase)?;
103        let client_id = ClientId::new(keys.verification_key_bytes, keys.pq_commitment_bytes)?;
104        let key_public_hex = client_id.id_hex();
105
106        // Persist the key to disk
107        let persistence_data = keys.to_persistence(&self.passphrase)?;
108        let key_file_path = self.key_file_path(&key_public_hex);
109        std::fs::write(&key_file_path, persistence_data)?;
110
111        let native_key_locker = Arc::new(DiskKeyLocker { keys, client_id });
112
113        let mut loaded_keys = self.loaded_keys.write();
114        loaded_keys.insert(key_public_hex, native_key_locker.clone());
115
116        Ok(native_key_locker)
117    }
118
119    async fn switch(&self, key_public: String) -> anyhow::Result<Arc<DiskKeyLocker>> {
120        // Check if already loaded in memory
121        {
122            let loaded_keys = self.loaded_keys.read();
123            if let Some(native_key_locker) = loaded_keys.get(&key_public) {
124                return Ok(native_key_locker.clone());
125            }
126        }
127
128        // Load from disk
129        let key_file_path = self.key_file_path(&key_public);
130        let persistence_data = std::fs::read_to_string(&key_file_path)
131            .map_err(|_| anyhow!("Key file not found for {}", key_public))?;
132        let keys = Keys::from_persistence(&self.passphrase, &persistence_data)?;
133        let client_id = ClientId::new(keys.verification_key_bytes, keys.pq_commitment_bytes)?;
134
135        let native_key_locker = Arc::new(DiskKeyLocker { keys, client_id });
136
137        let mut loaded_keys = self.loaded_keys.write();
138        loaded_keys.insert(key_public, native_key_locker.clone());
139
140        Ok(native_key_locker)
141    }
142
143    async fn delete(&self, key_public: String) -> anyhow::Result<()> {
144        // Remove from memory
145        {
146            let mut loaded_keys = self.loaded_keys.write();
147            loaded_keys.remove(&key_public);
148        }
149
150        // Remove from disk
151        let key_file_path = self.key_file_path(&key_public);
152        if key_file_path.exists() {
153            std::fs::remove_file(&key_file_path)?;
154        }
155
156        Ok(())
157    }
158
159    async fn reset(&self) -> anyhow::Result<()> {
160        // Clear memory
161        {
162            let mut loaded_keys = self.loaded_keys.write();
163            loaded_keys.clear();
164        }
165
166        // Remove all .key files from the data directory
167        for entry in std::fs::read_dir(&self.key_locker_dir)? {
168            let entry = entry?;
169            let file_name = entry.file_name().to_string_lossy().to_string();
170            if file_name.ends_with(".key") {
171                std::fs::remove_file(entry.path())?;
172            }
173        }
174
175        Ok(())
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use crate::client::key_locker::disk_key_locker::{DiskKeyLocker, DiskKeyLockerManager};
182    use crate::tools::tools::get_temp_dir;
183    use std::sync::Arc;
184    use crate::client::key_locker::key_locker;
185
186    #[tokio::test]
187    async fn add_test() {
188        key_locker::tests::add_test::<DiskKeyLocker, DiskKeyLockerManager>().await;
189    }
190    #[tokio::test]
191    async fn sign_test() {
192        key_locker::tests::sign_test::<DiskKeyLocker, DiskKeyLockerManager>().await;
193    }
194    #[tokio::test]
195    async fn guest_client_id_excluded_from_list_test() {
196        key_locker::tests::guest_client_id_excluded_from_list_test::<DiskKeyLocker, DiskKeyLockerManager>().await;
197    }
198
199    #[tokio::test]
200    async fn persistence_roundtrip_test() {
201        use crate::client::key_locker::key_locker::KeyLockerManager;
202
203        let (_temp_dir, temp_dir_path) = get_temp_dir().unwrap();
204        let data_dir = std::path::PathBuf::from(temp_dir_path);
205
206        // Create a key with one manager instance
207        let manager_1 = DiskKeyLockerManager::with_data_dir(data_dir.clone(), "test_passphrase".to_string()).unwrap();
208        manager_1.reset().await.unwrap();
209        let key_locker: Arc<DiskKeyLocker> = manager_1.create("my_key_phrase".to_string()).await.unwrap();
210
211        use crate::client::key_locker::key_locker::KeyLocker;
212        let original_client_id = key_locker.client_id().clone();
213        let key_public_hex = original_client_id.id.to_hex_str();
214
215        // Create a fresh manager instance (simulating process restart) and switch to the stored key
216        let manager_2 = DiskKeyLockerManager::with_data_dir(data_dir, "test_passphrase".to_string()).unwrap();
217        let restored_key_locker = manager_2.switch(key_public_hex).await.unwrap();
218
219        assert_eq!(&original_client_id, restored_key_locker.client_id());
220    }
221}