cfxstore/accounts_dir/
disk.rs

1// Copyright 2015-2019 Parity Technologies (UK) Ltd.
2// This file is part of Parity Ethereum.
3
4// Parity Ethereum is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Parity Ethereum is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Parity Ethereum.  If not, see <http://www.gnu.org/licenses/>.
16
17use super::{
18    vault::{VaultDiskDirectory, VAULT_FILE_NAME},
19    KeyDirectory, VaultKey, VaultKeyDirectory, VaultKeyDirectoryProvider,
20};
21use crate::{
22    json::{self, Uuid},
23    Error, SafeAccount,
24};
25use cfxkey::Password;
26use log::warn;
27use std::{
28    collections::HashMap,
29    fs,
30    io::{self, Write},
31    path::{Path, PathBuf},
32};
33use time;
34
35const IGNORED_FILES: &[&str] = &[
36    "thumbs.db",
37    "address_book.json",
38    "dapps_policy.json",
39    "dapps_accounts.json",
40    "dapps_history.json",
41    "vault.json",
42];
43
44/// Find a unique filename that does not exist using four-letter random suffix.
45pub fn find_unique_filename_using_random_suffix(
46    parent_path: &Path, original_filename: &str,
47) -> io::Result<String> {
48    let mut path = parent_path.join(original_filename);
49    let mut deduped_filename = original_filename.to_string();
50
51    if path.exists() {
52        const MAX_RETRIES: usize = 500;
53        let mut retries = 0;
54
55        while path.exists() {
56            if retries >= MAX_RETRIES {
57                return Err(io::Error::other(
58                    "Exceeded maximum retries when deduplicating filename.",
59                ));
60            }
61
62            let suffix = crate::random::random_string(4);
63            deduped_filename = format!("{}-{}", original_filename, suffix);
64            path.set_file_name(&deduped_filename);
65            retries += 1;
66        }
67    }
68
69    Ok(deduped_filename)
70}
71
72/// Create a new file and restrict permissions to owner only. It errors if the
73/// file already exists.
74#[cfg(unix)]
75pub fn create_new_file_with_permissions_to_owner(
76    file_path: &Path,
77) -> io::Result<fs::File> {
78    use std::os::unix::fs::OpenOptionsExt;
79
80    fs::OpenOptions::new()
81        .write(true)
82        .create_new(true)
83        .mode((libc::S_IWUSR | libc::S_IRUSR) as u32)
84        .open(file_path)
85}
86
87/// Create a new file and restrict permissions to owner only. It errors if the
88/// file already exists.
89#[cfg(not(unix))]
90pub fn create_new_file_with_permissions_to_owner(
91    file_path: &Path,
92) -> io::Result<fs::File> {
93    fs::OpenOptions::new()
94        .write(true)
95        .create_new(true)
96        .open(file_path)
97}
98
99/// Create a new file and restrict permissions to owner only. It replaces the
100/// existing file if it already exists.
101#[cfg(unix)]
102pub fn replace_file_with_permissions_to_owner(
103    file_path: &Path,
104) -> io::Result<fs::File> {
105    use std::os::unix::fs::PermissionsExt;
106
107    let file = fs::File::create(file_path)?;
108    let mut permissions = file.metadata()?.permissions();
109    permissions.set_mode((libc::S_IWUSR | libc::S_IRUSR) as u32);
110    file.set_permissions(permissions)?;
111
112    Ok(file)
113}
114
115/// Create a new file and restrict permissions to owner only. It replaces the
116/// existing file if it already exists.
117#[cfg(not(unix))]
118pub fn replace_file_with_permissions_to_owner(
119    file_path: &Path,
120) -> io::Result<fs::File> {
121    fs::File::create(file_path)
122}
123
124/// Root keys directory implementation
125pub type RootDiskDirectory = DiskDirectory<DiskKeyFileManager>;
126
127/// Disk directory key file manager
128pub trait KeyFileManager: Send + Sync {
129    /// Read `SafeAccount` from given key file stream
130    fn read<T>(
131        &self, filename: Option<String>, reader: T,
132    ) -> Result<SafeAccount, Error>
133    where T: io::Read;
134
135    /// Write `SafeAccount` to given key file stream
136    fn write<T>(
137        &self, account: SafeAccount, writer: &mut T,
138    ) -> Result<(), Error>
139    where T: io::Write;
140}
141
142/// Disk-based keys directory implementation
143pub struct DiskDirectory<T>
144where T: KeyFileManager
145{
146    path: PathBuf,
147    key_manager: T,
148}
149
150/// Keys file manager for root keys directory
151#[derive(Default)]
152pub struct DiskKeyFileManager {
153    password: Option<Password>,
154}
155
156impl RootDiskDirectory {
157    pub fn create<P>(path: P) -> Result<Self, Error>
158    where P: AsRef<Path> {
159        fs::create_dir_all(&path)?;
160        Ok(Self::at(path))
161    }
162
163    /// allows to read keyfiles with given password (needed for keyfiles w/o
164    /// address)
165    pub fn with_password(&self, password: Option<Password>) -> Self {
166        DiskDirectory::new(&self.path, DiskKeyFileManager { password })
167    }
168
169    pub fn at<P>(path: P) -> Self
170    where P: AsRef<Path> {
171        DiskDirectory::new(path, DiskKeyFileManager::default())
172    }
173}
174
175impl<T> DiskDirectory<T>
176where T: KeyFileManager
177{
178    /// Create new disk directory instance
179    pub fn new<P>(path: P, key_manager: T) -> Self
180    where P: AsRef<Path> {
181        DiskDirectory {
182            path: path.as_ref().to_path_buf(),
183            key_manager,
184        }
185    }
186
187    fn files(&self) -> Result<Vec<PathBuf>, Error> {
188        Ok(fs::read_dir(&self.path)?
189            .flat_map(Result::ok)
190            .filter(|entry| {
191                let metadata = entry.metadata().ok();
192                let file_name = entry.file_name();
193                let name = file_name.to_string_lossy();
194                // filter directories
195                metadata.is_some_and(|m| !m.is_dir()) &&
196					// hidden files
197					!name.starts_with('.') &&
198					// other ignored files
199					!IGNORED_FILES.contains(&&*name)
200            })
201            .map(|entry| entry.path())
202            .collect::<Vec<PathBuf>>())
203    }
204
205    pub fn files_hash(&self) -> Result<u64, Error> {
206        use std::{collections::hash_map::DefaultHasher, hash::Hasher};
207
208        let mut hasher = DefaultHasher::new();
209        let files = self.files()?;
210        for file in files {
211            hasher.write(file.to_str().unwrap_or("").as_bytes())
212        }
213
214        Ok(hasher.finish())
215    }
216
217    fn last_modification_date(&self) -> Result<u64, Error> {
218        use std::time::UNIX_EPOCH;
219        let duration = fs::metadata(&self.path)?
220            .modified()?
221            .duration_since(UNIX_EPOCH)
222            .unwrap_or_default();
223        let timestamp = duration.as_secs() ^ (duration.subsec_nanos() as u64);
224        Ok(timestamp)
225    }
226
227    /// all accounts found in keys directory
228    fn files_content(&self) -> Result<HashMap<PathBuf, SafeAccount>, Error> {
229        // it's not done using one iterator cause
230        // there is an issue with rustc and it takes tooo much time to compile
231        let paths = self.files()?;
232        Ok(paths
233            .into_iter()
234            .filter_map(|path| {
235                let filename = Some(
236                    path.file_name()
237                        .and_then(|n| n.to_str())
238                        .expect("Keys have valid UTF8 names only.")
239                        .to_owned(),
240                );
241                fs::File::open(path.clone())
242                    .map_err(Into::into)
243                    .and_then(|file| self.key_manager.read(filename, file))
244                    .map_err(|err| {
245                        warn!("Invalid key file: {:?} ({})", path, err);
246                        err
247                    })
248                    .map(|account| (path, account))
249                    .ok()
250            })
251            .collect())
252    }
253
254    /// insert account with given filename. if the filename is a duplicate of
255    /// any stored account and dedup is set to true, a random suffix is
256    /// appended to the filename.
257    pub fn insert_with_filename(
258        &self, account: SafeAccount, mut filename: String, dedup: bool,
259    ) -> Result<SafeAccount, Error> {
260        if dedup {
261            filename = find_unique_filename_using_random_suffix(
262                &self.path, &filename,
263            )?;
264        }
265
266        // path to keyfile
267        let keyfile_path = self.path.join(filename.as_str());
268
269        // update account filename
270        let original_account = account.clone();
271        let mut account = account;
272        account.filename = Some(filename);
273
274        {
275            // save the file
276            let mut file = if dedup {
277                create_new_file_with_permissions_to_owner(&keyfile_path)?
278            } else {
279                replace_file_with_permissions_to_owner(&keyfile_path)?
280            };
281
282            // write key content
283            self.key_manager
284                .write(original_account, &mut file)
285                .map_err(|e| Error::Custom(format!("{:?}", e)))?;
286
287            file.flush()?;
288            file.sync_all()?;
289        }
290
291        Ok(account)
292    }
293
294    /// Get key file manager referece
295    pub fn key_manager(&self) -> &T { &self.key_manager }
296}
297
298impl<T> KeyDirectory for DiskDirectory<T>
299where T: KeyFileManager
300{
301    fn load(&self) -> Result<Vec<SafeAccount>, Error> {
302        let accounts = self.files_content()?.into_values().collect();
303        Ok(accounts)
304    }
305
306    fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
307        // Disk store handles updates correctly iff filename is the same
308        let filename = account_filename(&account);
309        self.insert_with_filename(account, filename, false)
310    }
311
312    fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
313        let filename = account_filename(&account);
314        self.insert_with_filename(account, filename, true)
315    }
316
317    fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
318        // enumerate all entries in keystore
319        // and find entry with given address
320        let to_remove = self.files_content()?.into_iter().find(|(_, acc)| {
321            acc.id == account.id && acc.address == account.address
322        });
323
324        // remove it
325        match to_remove {
326            None => Err(Error::InvalidAccount),
327            Some((path, _)) => fs::remove_file(path).map_err(From::from),
328        }
329    }
330
331    fn path(&self) -> Option<&PathBuf> { Some(&self.path) }
332
333    fn as_vault_provider(&self) -> Option<&dyn VaultKeyDirectoryProvider> {
334        Some(self)
335    }
336
337    fn unique_repr(&self) -> Result<u64, Error> {
338        self.last_modification_date()
339    }
340}
341
342impl<T> VaultKeyDirectoryProvider for DiskDirectory<T>
343where T: KeyFileManager
344{
345    fn create(
346        &self, name: &str, key: VaultKey,
347    ) -> Result<Box<dyn VaultKeyDirectory>, Error> {
348        let vault_dir = VaultDiskDirectory::create(&self.path, name, key)?;
349        Ok(Box::new(vault_dir))
350    }
351
352    fn open(
353        &self, name: &str, key: VaultKey,
354    ) -> Result<Box<dyn VaultKeyDirectory>, Error> {
355        let vault_dir = VaultDiskDirectory::at(&self.path, name, key)?;
356        Ok(Box::new(vault_dir))
357    }
358
359    fn list_vaults(&self) -> Result<Vec<String>, Error> {
360        Ok(fs::read_dir(&self.path)?
361            .filter_map(|e| e.ok().map(|e| e.path()))
362            .filter_map(|path| {
363                let mut vault_file_path = path.clone();
364                vault_file_path.push(VAULT_FILE_NAME);
365                if vault_file_path.is_file() {
366                    path.file_name()
367                        .and_then(|f| f.to_str())
368                        .map(|f| f.to_owned())
369                } else {
370                    None
371                }
372            })
373            .collect())
374    }
375
376    fn vault_meta(&self, name: &str) -> Result<String, Error> {
377        VaultDiskDirectory::meta_at(&self.path, name)
378    }
379}
380
381impl KeyFileManager for DiskKeyFileManager {
382    fn read<T>(
383        &self, filename: Option<String>, reader: T,
384    ) -> Result<SafeAccount, Error>
385    where T: io::Read {
386        let key_file = json::KeyFile::load(reader)
387            .map_err(|e| Error::Custom(format!("{:?}", e)))?;
388        SafeAccount::from_file(key_file, filename, &self.password)
389    }
390
391    fn write<T>(
392        &self, mut account: SafeAccount, writer: &mut T,
393    ) -> Result<(), Error>
394    where T: io::Write {
395        // when account is moved back to root directory from vault
396        // => remove vault field from meta
397        account.meta = json::remove_vault_name_from_json_meta(&account.meta)
398            .map_err(|err| Error::Custom(format!("{:?}", err)))?;
399
400        let key_file: json::KeyFile = account.into();
401        key_file
402            .write(writer)
403            .map_err(|e| Error::Custom(format!("{:?}", e)))
404    }
405}
406
407fn account_filename(account: &SafeAccount) -> String {
408    // build file path
409    account.filename.clone().unwrap_or_else(|| {
410        let time_format = time::macros::format_description!(
411            "[year]-[month]-[day]T[hour]-[minute]-[second]"
412        );
413        let timestamp = time::OffsetDateTime::now_utc()
414            .format(&time_format)
415            .expect("Time-format string is valid.");
416        format!("UTC--{}Z--{}", timestamp, Uuid::from(account.id))
417    })
418}
419
420#[cfg(test)]
421mod test {
422    use super::{KeyDirectory, RootDiskDirectory, VaultKey};
423    use crate::account::SafeAccount;
424    use cfxkey::{Generator, Random};
425    use std::{env, fs};
426    use tempfile::tempdir;
427
428    #[test]
429    fn should_create_new_account() {
430        // given
431        let mut dir = env::temp_dir();
432        dir.push("cfxstore_should_create_new_account");
433        let keypair = Random.generate().unwrap();
434        let password = "hello world".into();
435        let directory = RootDiskDirectory::create(dir.clone()).unwrap();
436
437        // when
438        let account = SafeAccount::create(
439            &keypair,
440            [0u8; 16],
441            &password,
442            1024,
443            "Test".to_owned(),
444            "{}".to_owned(),
445        );
446        let res = directory.insert(account.unwrap());
447
448        // then
449        assert!(res.is_ok(), "Should save account succesfuly.");
450        assert!(
451            res.unwrap().filename.is_some(),
452            "Filename has been assigned."
453        );
454
455        // cleanup
456        let _ = fs::remove_dir_all(dir);
457    }
458
459    #[test]
460    fn should_handle_duplicate_filenames() {
461        // given
462        let mut dir = env::temp_dir();
463        dir.push("cfxstore_should_handle_duplicate_filenames");
464        let keypair = Random.generate().unwrap();
465        let password = "hello world".into();
466        let directory = RootDiskDirectory::create(dir.clone()).unwrap();
467
468        // when
469        let account = SafeAccount::create(
470            &keypair,
471            [0u8; 16],
472            &password,
473            1024,
474            "Test".to_owned(),
475            "{}".to_owned(),
476        )
477        .unwrap();
478        let filename = "test".to_string();
479        let dedup = true;
480
481        directory
482            .insert_with_filename(account.clone(), "foo".to_string(), dedup)
483            .unwrap();
484        let file1 = directory
485            .insert_with_filename(account.clone(), filename.clone(), dedup)
486            .unwrap()
487            .filename
488            .unwrap();
489        let file2 = directory
490            .insert_with_filename(account.clone(), filename.clone(), dedup)
491            .unwrap()
492            .filename
493            .unwrap();
494        let file3 = directory
495            .insert_with_filename(account, filename.clone(), dedup)
496            .unwrap()
497            .filename
498            .unwrap();
499
500        // then
501        // the first file should have the original names
502        assert_eq!(file1, filename);
503
504        // the following duplicate files should have a suffix appended
505        assert!(file2 != file3);
506        assert_eq!(file2.len(), filename.len() + 5);
507        assert_eq!(file3.len(), filename.len() + 5);
508
509        // cleanup
510        let _ = fs::remove_dir_all(dir);
511    }
512
513    #[test]
514    fn should_manage_vaults() {
515        // given
516        let mut dir = env::temp_dir();
517        dir.push("should_create_new_vault");
518        let directory = RootDiskDirectory::create(dir.clone()).unwrap();
519        let vault_name = "vault";
520        let password = "password".into();
521
522        // then
523        assert!(directory.as_vault_provider().is_some());
524
525        // and when
526        let before_root_items_count = fs::read_dir(&dir).unwrap().count();
527        let vault = directory
528            .as_vault_provider()
529            .unwrap()
530            .create(vault_name, VaultKey::new(&password, 1024));
531
532        // then
533        assert!(vault.is_ok());
534        let after_root_items_count = fs::read_dir(&dir).unwrap().count();
535        assert!(after_root_items_count > before_root_items_count);
536
537        // and when
538        let vault = directory
539            .as_vault_provider()
540            .unwrap()
541            .open(vault_name, VaultKey::new(&password, 1024));
542
543        // then
544        assert!(vault.is_ok());
545        let after_root_items_count2 = fs::read_dir(&dir).unwrap().count();
546        assert!(after_root_items_count == after_root_items_count2);
547
548        // cleanup
549        let _ = fs::remove_dir_all(dir);
550    }
551
552    #[test]
553    fn should_list_vaults() {
554        // given
555        let temp_path = tempdir().unwrap();
556        let directory = RootDiskDirectory::create(&temp_path).unwrap();
557        let vault_provider = directory.as_vault_provider().unwrap();
558        vault_provider
559            .create("vault1", VaultKey::new(&"password1".into(), 1))
560            .unwrap();
561        vault_provider
562            .create("vault2", VaultKey::new(&"password2".into(), 1))
563            .unwrap();
564
565        // then
566        let vaults = vault_provider.list_vaults().unwrap();
567        assert_eq!(vaults.len(), 2);
568        assert!(vaults.iter().any(|v| &*v == "vault1"));
569        assert!(vaults.iter().any(|v| &*v == "vault2"));
570    }
571
572    #[test]
573    fn hash_of_files() {
574        let temp_path = tempdir().unwrap();
575        let directory = RootDiskDirectory::create(&temp_path).unwrap();
576
577        let hash = directory
578            .files_hash()
579            .expect("Files hash should be calculated ok");
580        assert_eq!(hash, 15_130_871_412_783_076_140);
581
582        let keypair = Random.generate().unwrap();
583        let password = "test pass".into();
584        let account = SafeAccount::create(
585            &keypair,
586            [0u8; 16],
587            &password,
588            1024,
589            "Test".to_owned(),
590            "{}".to_owned(),
591        );
592        directory
593            .insert(account.unwrap())
594            .expect("Account should be inserted ok");
595
596        let new_hash = directory
597            .files_hash()
598            .expect("New files hash should be calculated ok");
599
600        assert!(new_hash != hash, "hash of the file list should change once directory content changed");
601    }
602}