emerald_vault/storage/
global_key.rs

1use std::convert::TryFrom;
2use std::fs;
3use std::path::{PathBuf};
4use protobuf::Message;
5use uuid::Uuid;
6use crate::error::VaultError;
7use crate::storage::vault::{safe_update, VaultStorage};
8use crate::structs::crypto::{Encrypted, GlobalKey};
9use crate::proto::crypto::{GlobalKey as proto_GlobalKey};
10use crate::structs::types::UsesOddKey;
11
12///
13/// Manage storage for a Global Key
14pub struct VaultGlobalKey {
15    pub(crate) vault: PathBuf,
16}
17
18pub(crate) const KEY_FILE: &str = "global.key";
19
20impl VaultGlobalKey {
21    fn get_path(&self) -> PathBuf {
22        self.vault.join(KEY_FILE)
23    }
24
25    ///
26    /// Check if Global Key is set for current Vault.
27    pub fn is_set(&self) -> bool {
28        self.get_path().is_file()
29    }
30
31    ///
32    /// Create new Global Key for the current vault. Can be created only once.
33    /// The key itself is randomly generated and encrypted with the provided password.
34    ///
35    /// * `password` - password to encrypt global key
36    pub fn create(&self, password: &str) -> Result<(), VaultError> {
37        if self.is_set() {
38            return Err(VaultError::FilesystemError("Global key already set".to_string()))
39        }
40        let global = GlobalKey::generate(password.as_bytes())?;
41        let encoded: Vec<u8> = proto_GlobalKey::try_from(&global).unwrap().write_to_bytes()?;
42        let file = self.get_path();
43        let write_result = fs::write(&file, encoded);
44        if write_result.is_err() {
45            // if we did actually wrote to the file, maybe partially, but still got an error then we
46            // need to do a clean up and remove the corrupted file
47            if file.exists() && fs::remove_file(&file).is_err() {
48                println!("Failed to remove {:?}", file)
49            }
50            return Err(VaultError::FilesystemError(
51                format!("Failed to write global key: {:?}", write_result.err().unwrap()))
52            );
53        }
54        Ok(())
55    }
56
57    ///
58    /// Get current global key if it's set
59    pub fn get(&self) -> Result<GlobalKey, VaultError> {
60        let file = self.get_path();
61        if file.exists() && file.is_file() {
62            let proto = fs::read(file)?;
63            let global = GlobalKey::try_from(proto.as_slice())?;
64            return Ok(global)
65        }
66        Err(VaultError::FilesystemError("Global key doesn't exist".to_string()))
67    }
68
69    ///
70    /// Try to a Global Key. Returns None if it's not set, or Error when there is an error to access it (ex. IO Error, etc)
71    pub fn get_if_exists(&self) -> Result<Option<GlobalKey>, VaultError> {
72        if !self.is_set() {
73            return Ok(None)
74        }
75        // may fail to read/decode and return just a Err(VaultError)
76        let global = self.get()?;
77        Ok(Some(global))
78    }
79
80    ///
81    /// Change password for the global key.
82    /// Note that it doesn't change the actual secret value which used to derive keys for dependent objects
83    pub fn change_password(self, current_password: &str, new_password: &str) -> Result<(), VaultError> {
84        if !self.is_set() {
85            return Err(VaultError::GlobalKeyRequired)
86        }
87
88        let mut g = self.get()?;
89        let key_value = g.key.decrypt(current_password.as_bytes(), None)?;
90        g.key = Encrypted::encrypt(key_value, new_password.as_bytes(), None)?;
91
92        let encoded: Vec<u8> = proto_GlobalKey::try_from(&g).unwrap().write_to_bytes()?;
93        // NOTE it doesn't make a copy of the old key in archive, because if the reason to change
94        // password is that the previous one is unsecure the copy make the change completely bogus
95        safe_update(self.get_path(), encoded, None)
96    }
97
98    ///
99    /// Verify that password can decrypt the Global Key.
100    /// May return Err if the Global Key is not set, or IO/conversion/etc errors while reading it
101    pub fn verify_password(self, password: &str) -> Result<bool, VaultError> {
102        if !self.is_set() {
103            return Err(VaultError::GlobalKeyRequired)
104        }
105
106        let key = self.get()?;
107        key.verify_password(password).map_err(VaultError::from)
108    }
109}
110
111#[derive(Clone, PartialEq, Eq, Debug)]
112pub enum LegacyEntryRef {
113    Seed(Uuid),
114    PrivateKey(Uuid)
115}
116
117impl VaultStorage {
118    ///
119    /// Get list of items in the Vault that use individual passwords. Such item may be a Seed or an individual Key
120    pub fn get_global_key_missing(&self) -> Result<Vec<LegacyEntryRef>, VaultError> {
121        let seeds: Vec<LegacyEntryRef> = self.seeds()
122            .list_entries()?
123            .iter()
124            .filter(|seed| seed.is_odd_key())
125            .map(|seed| LegacyEntryRef::Seed(seed.id))
126            .collect();
127        let keys: Vec<LegacyEntryRef> = self.keys()
128            .list_entries()?
129            .iter()
130            .filter(|key| key.is_odd_key())
131            .map(|key| LegacyEntryRef::PrivateKey(key.id))
132            .collect();
133
134        let mut result = Vec::with_capacity(seeds.len() + keys.len());
135        result.extend(seeds);
136        result.extend(keys);
137        Ok(result)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use std::str::FromStr;
144    use hdpath::StandardHDPath;
145    use tempdir::TempDir;
146    use crate::chains::Blockchain;
147    use crate::EthereumAddress;
148    use crate::storage::vault::VaultStorage;
149    use crate::structs::pk::PrivateKeyHolder;
150    use crate::structs::seed::{Seed, SeedSource, LedgerSource};
151    use crate::storage::global_key::LegacyEntryRef;
152
153    #[test]
154    fn create_when_unset() {
155        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
156        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
157
158        let global = vault.global_key();
159        assert!(!global.is_set());
160        global.create("test").expect("create global key");
161        assert!(global.is_set());
162    }
163
164    #[test]
165    fn cannot_get_when_unset() {
166        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
167        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
168
169        let global = vault.global_key();
170        assert!(!global.is_set());
171        let value_result = global.get();
172        assert!(value_result.is_err());
173    }
174
175    #[test]
176    fn cannot_create_when_set() {
177        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
178        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
179
180        let global = vault.global_key();
181        assert!(!global.is_set());
182        global.create("test-1").expect("create global key");
183        assert!(global.is_set());
184        let global_value_1 = global.get().unwrap();
185
186        let create2 = global.create("test-2");
187        assert!(create2.is_err());
188
189        let global_value_2 = global.get().unwrap();
190        assert_eq!(global_value_1, global_value_2);
191    }
192
193    #[test]
194    fn returns_when_exists() {
195        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
196        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
197
198        let global = vault.global_key();
199        global.create("test-1").unwrap();
200
201        let value = global.get_if_exists();
202
203        assert!(value.is_ok());
204        assert!(value.unwrap().is_some());
205    }
206
207    #[test]
208    fn verify_correct() {
209        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
210        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
211
212        let global = vault.global_key();
213        global.create("test-1").unwrap();
214
215        let value = global.verify_password("test-1");
216
217        assert!(value.is_ok());
218        assert!(value.unwrap());
219    }
220
221    #[test]
222    fn verify_wrong() {
223        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
224        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
225
226        let global = vault.global_key();
227        global.create("test-1").unwrap();
228
229        let value = global.verify_password("test-2");
230
231        assert!(value.is_ok());
232        assert!(!value.unwrap());
233    }
234
235    #[test]
236    fn none_when_doesnt_exist() {
237        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
238        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
239
240        let global = vault.global_key();
241
242        let value = global.get_if_exists();
243
244        assert!(value.is_ok());
245        assert!(value.unwrap().is_none());
246    }
247
248    #[test]
249    fn is_used_in_encryption() {
250        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
251        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
252
253        let global_store = vault.global_key();
254        global_store.create("test-1").unwrap();
255        let global = Some(global_store.get().unwrap());
256
257        let seed_id = vault.seeds().add(
258            Seed::test_generate(None, "test-1".as_bytes(), global.clone()).unwrap()
259        ).unwrap();
260
261        let seed_source = vault.seeds().get(seed_id).unwrap().source;
262
263        let get_no_global = seed_source.get_addresses::<EthereumAddress>(
264            Some("test-1".to_string()),
265            None,
266            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
267            Blockchain::Ethereum,
268        );
269
270        assert!(get_no_global.is_err());
271
272        let get_w_global = seed_source.get_addresses::<EthereumAddress>(
273            Some("test-1".to_string()),
274            global,
275            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
276            Blockchain::Ethereum,
277        );
278
279        println!("Result: {:?}", get_w_global);
280
281        assert!(get_w_global.is_ok());
282    }
283
284    #[test]
285    fn can_change_password() {
286        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
287        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
288
289        let global_store = vault.global_key();
290        global_store.create("test-1").unwrap();
291        let global = Some(global_store.get().unwrap());
292
293        let seed_id = vault.seeds().add(
294            Seed::test_generate(None, "test-1".as_bytes(), global.clone()).unwrap()
295        ).unwrap();
296
297        let changed = global_store.change_password("test-1", "test-2");
298        assert!(changed.is_ok());
299
300        // don't forget to read it from disk
301        let global = Some(vault.global_key().get().unwrap());
302
303        let seed_source = vault.seeds().get(seed_id).unwrap().source;
304
305        let get_old_password = seed_source.get_addresses::<EthereumAddress>(
306            Some("test-1".to_string()),
307            global.clone(),
308            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
309            Blockchain::Ethereum,
310        );
311        assert!(get_old_password.is_err());
312
313        let get_new_password = seed_source.get_addresses::<EthereumAddress>(
314            Some("test-2".to_string()),
315            global,
316            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
317            Blockchain::Ethereum,
318        );
319
320        println!("Result: {:?}", get_new_password);
321
322        assert!(get_new_password.is_ok());
323    }
324
325    #[test]
326    fn doesnt_change_wrong_password() {
327        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
328        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
329
330        let global_store = vault.global_key();
331        global_store.create("test-1").unwrap();
332        let global = Some(global_store.get().unwrap());
333
334        let seed_id = vault.seeds().add(
335            Seed::test_generate(None, "test-1".as_bytes(), global.clone()).unwrap()
336        ).unwrap();
337
338        let changed = global_store.change_password("test-1-wrong", "test-2");
339        assert!(changed.is_err());
340
341        // don't forget to read it from disk
342        let global = Some(vault.global_key().get().unwrap());
343
344        let seed_source = vault.seeds().get(seed_id).unwrap().source;
345
346        let get_old_password = seed_source.get_addresses::<EthereumAddress>(
347            Some("test-1".to_string()),
348            global.clone(),
349            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
350            Blockchain::Ethereum,
351        );
352        assert!(get_old_password.is_ok());
353
354        let get_new_password = seed_source.get_addresses::<EthereumAddress>(
355            Some("test-2".to_string()),
356            global,
357            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
358            Blockchain::Ethereum,
359        );
360
361        assert!(get_new_password.is_err());
362    }
363
364    #[test]
365    fn ignored_for_legacy_seed() {
366        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
367        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
368
369        let seed_id = vault.seeds().add(
370            Seed::test_generate(None, "test-1".as_bytes(), None).unwrap()
371        ).unwrap();
372
373        let global_store = vault.global_key();
374        global_store.create("test-1").unwrap();
375        let global = Some(global_store.get().unwrap());
376
377        let seed_source = vault.seeds().get(seed_id).unwrap().source;
378
379        // because seed was created without global key it can be decrypted with or without it
380
381        let get_no_global = seed_source.get_addresses::<EthereumAddress>(
382            Some("test-1".to_string()),
383            None,
384            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
385            Blockchain::Ethereum,
386        );
387
388        assert!(get_no_global.is_ok());
389
390        let get_w_global = seed_source.get_addresses::<EthereumAddress>(
391            Some("test-1".to_string()),
392            global,
393            &vec![StandardHDPath::from_str("m/44'/60'/0'/0/0").unwrap()],
394            Blockchain::Ethereum,
395        );
396
397        println!("Result: {:?}", get_w_global);
398
399        assert!(get_w_global.is_ok());
400    }
401
402    #[test]
403    fn reports_nokey_items() {
404        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
405        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
406
407        let seed_id_1 = vault.seeds().add(
408            Seed::test_generate(None, "test-1".as_bytes(), None).unwrap()
409        ).unwrap();
410
411        let key_id_1 = vault.keys().add(
412            PrivateKeyHolder::generate_ethereum_raw("test-2").unwrap()
413        ).unwrap();
414
415        println!("init: {:?}, {:?}", seed_id_1, key_id_1);
416
417        let global_store = vault.global_key();
418        global_store.create("test-g").unwrap();
419        let global = Some(global_store.get().unwrap());
420
421        let seed_id_2 = vault.seeds().add(
422            Seed::test_generate(None, "test-g".as_bytes(), global.clone()).unwrap()
423        ).unwrap();
424
425        let key_id_2 = vault.keys().add(
426            PrivateKeyHolder::create_ethereum_raw(hex::decode("15cc67bb2a7f75a682198264728b951c461bd4a92692ab3bb00f01e9dbe2fbe4").unwrap(), "test-g", global.clone()).unwrap()
427        ).unwrap();
428
429        let key_id_3 = vault.keys().add(
430            PrivateKeyHolder::generate_ethereum_raw("test-3").unwrap()
431        ).unwrap();
432
433        println!("post: {:?}, {:?}, {:?}", seed_id_2, key_id_2, key_id_3);
434
435        let unused = vault.get_global_key_missing().unwrap();
436
437        println!("{:?}", unused);
438
439        assert_eq!(unused.len(), 3);
440
441        assert!(unused.contains(&LegacyEntryRef::Seed(seed_id_1)));
442        assert!(unused.contains(&LegacyEntryRef::PrivateKey(key_id_1)));
443        assert!(unused.contains(&LegacyEntryRef::PrivateKey(key_id_3)));
444    }
445
446    #[test]
447    fn doesnt_report_ledger() {
448        let tmp_dir = TempDir::new("emerald-global-key-test").unwrap();
449        let vault = VaultStorage::create(tmp_dir.path()).unwrap();
450
451        let seed_id_1 = vault.seeds().add(
452            Seed::test_generate(None, "test-1".as_bytes(), None).unwrap()
453        ).unwrap();
454
455        let seed_id_2 = vault.seeds().add(
456            Seed {
457                source: SeedSource::Ledger(LedgerSource::default()),
458                ..Seed::default()
459            }
460        ).unwrap();
461
462        println!("init: {:?}, {:?}", seed_id_1, seed_id_2);
463
464        let global_store = vault.global_key();
465        global_store.create("test-g").unwrap();
466        let _global = Some(global_store.get().unwrap());
467
468        let unused = vault.get_global_key_missing().unwrap();
469
470        println!("{:?}", unused);
471
472        assert_eq!(unused.len(), 1);
473
474        assert!(unused.contains(&LegacyEntryRef::Seed(seed_id_1)));
475    }
476}