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