// Copyright 2015-2020 Parity Technologies (UK) Ltd. // This file is part of OpenEthereum. // OpenEthereum is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // OpenEthereum is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with OpenEthereum. If not, see . use super::{ vault::{VaultDiskDirectory, VAULT_FILE_NAME}, KeyDirectory, VaultKey, VaultKeyDirectory, VaultKeyDirectoryProvider, }; use ethkey::Password; use json::{self, Uuid}; use std::{ collections::HashMap, fs, io, io::Write, path::{Path, PathBuf}, }; use time; use Error; use SafeAccount; const IGNORED_FILES: &'static [&'static str] = &[ "thumbs.db", "address_book.json", "dapps_policy.json", "dapps_accounts.json", "dapps_history.json", "vault.json", ]; /// Find a unique filename that does not exist using four-letter random suffix. pub fn find_unique_filename_using_random_suffix( parent_path: &Path, original_filename: &str, ) -> io::Result { let mut path = parent_path.join(original_filename); let mut deduped_filename = original_filename.to_string(); if path.exists() { const MAX_RETRIES: usize = 500; let mut retries = 0; while path.exists() { if retries >= MAX_RETRIES { return Err(io::Error::new( io::ErrorKind::Other, "Exceeded maximum retries when deduplicating filename.", )); } let suffix = ::random::random_string(4); deduped_filename = format!("{}-{}", original_filename, suffix); path.set_file_name(&deduped_filename); retries += 1; } } Ok(deduped_filename) } /// Create a new file and restrict permissions to owner only. It errors if the file already exists. #[cfg(unix)] pub fn create_new_file_with_permissions_to_owner(file_path: &Path) -> io::Result { use std::os::unix::fs::OpenOptionsExt; fs::OpenOptions::new() .write(true) .create_new(true) .mode((libc::S_IWUSR | libc::S_IRUSR) as u32) .open(file_path) } /// Create a new file and restrict permissions to owner only. It errors if the file already exists. #[cfg(not(unix))] pub fn create_new_file_with_permissions_to_owner(file_path: &Path) -> io::Result { fs::OpenOptions::new() .write(true) .create_new(true) .open(file_path) } /// Create a new file and restrict permissions to owner only. It replaces the existing file if it already exists. #[cfg(unix)] pub fn replace_file_with_permissions_to_owner(file_path: &Path) -> io::Result { use std::os::unix::fs::PermissionsExt; let file = fs::File::create(file_path)?; let mut permissions = file.metadata()?.permissions(); permissions.set_mode((libc::S_IWUSR | libc::S_IRUSR) as u32); file.set_permissions(permissions)?; Ok(file) } /// Create a new file and restrict permissions to owner only. It replaces the existing file if it already exists. #[cfg(not(unix))] pub fn replace_file_with_permissions_to_owner(file_path: &Path) -> io::Result { fs::File::create(file_path) } /// Root keys directory implementation pub type RootDiskDirectory = DiskDirectory; /// Disk directory key file manager pub trait KeyFileManager: Send + Sync { /// Read `SafeAccount` from given key file stream fn read(&self, filename: Option, reader: T) -> Result where T: io::Read; /// Write `SafeAccount` to given key file stream fn write(&self, account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write; } /// Disk-based keys directory implementation pub struct DiskDirectory where T: KeyFileManager, { path: PathBuf, key_manager: T, } /// Keys file manager for root keys directory #[derive(Default)] pub struct DiskKeyFileManager { password: Option, } impl RootDiskDirectory { pub fn create

(path: P) -> Result where P: AsRef, { fs::create_dir_all(&path)?; Ok(Self::at(path)) } /// allows to read keyfiles with given password (needed for keyfiles w/o address) pub fn with_password(&self, password: Option) -> Self { DiskDirectory::new(&self.path, DiskKeyFileManager { password }) } pub fn at

(path: P) -> Self where P: AsRef, { DiskDirectory::new(path, DiskKeyFileManager::default()) } } impl DiskDirectory where T: KeyFileManager, { /// Create new disk directory instance pub fn new

(path: P, key_manager: T) -> Self where P: AsRef, { DiskDirectory { path: path.as_ref().to_path_buf(), key_manager: key_manager, } } fn files(&self) -> Result, Error> { Ok(fs::read_dir(&self.path)? .flat_map(Result::ok) .filter(|entry| { let metadata = entry.metadata().ok(); let file_name = entry.file_name(); let name = file_name.to_string_lossy(); // filter directories metadata.map_or(false, |m| !m.is_dir()) && // hidden files !name.starts_with(".") && // other ignored files !IGNORED_FILES.contains(&&*name) }) .map(|entry| entry.path()) .collect::>()) } pub fn files_hash(&self) -> Result { use std::{collections::hash_map::DefaultHasher, hash::Hasher}; let mut hasher = DefaultHasher::new(); let files = self.files()?; for file in files { hasher.write(file.to_str().unwrap_or("").as_bytes()) } Ok(hasher.finish()) } fn last_modification_date(&self) -> Result { use std::time::{Duration, UNIX_EPOCH}; let duration = fs::metadata(&self.path)? .modified()? .duration_since(UNIX_EPOCH) .unwrap_or(Duration::default()); let timestamp = duration.as_secs() ^ (duration.subsec_nanos() as u64); Ok(timestamp) } /// all accounts found in keys directory fn files_content(&self) -> Result, Error> { // it's not done using one iterator cause // there is an issue with rustc and it takes tooo much time to compile let paths = self.files()?; Ok(paths .into_iter() .filter_map(|path| { let filename = Some( path.file_name() .and_then(|n| n.to_str()) .expect("Keys have valid UTF8 names only.") .to_owned(), ); fs::File::open(path.clone()) .map_err(Into::into) .and_then(|file| self.key_manager.read(filename, file)) .map_err(|err| { warn!("Invalid key file: {:?} ({})", path, err); err }) .map(|account| (path, account)) .ok() }) .collect()) } /// insert account with given filename. if the filename is a duplicate of any stored account and dedup is set to /// true, a random suffix is appended to the filename. pub fn insert_with_filename( &self, account: SafeAccount, mut filename: String, dedup: bool, ) -> Result { if dedup { filename = find_unique_filename_using_random_suffix(&self.path, &filename)?; } // path to keyfile let keyfile_path = self.path.join(filename.as_str()); // update account filename let original_account = account.clone(); let mut account = account; account.filename = Some(filename); { // save the file let mut file = if dedup { create_new_file_with_permissions_to_owner(&keyfile_path)? } else { replace_file_with_permissions_to_owner(&keyfile_path)? }; // write key content self.key_manager .write(original_account, &mut file) .map_err(|e| Error::Custom(format!("{:?}", e)))?; file.flush()?; file.sync_all()?; } Ok(account) } /// Get key file manager referece pub fn key_manager(&self) -> &T { &self.key_manager } } impl KeyDirectory for DiskDirectory where T: KeyFileManager, { fn load(&self) -> Result, Error> { let accounts = self .files_content()? .into_iter() .map(|(_, account)| account) .collect(); Ok(accounts) } fn update(&self, account: SafeAccount) -> Result { // Disk store handles updates correctly iff filename is the same let filename = account_filename(&account); self.insert_with_filename(account, filename, false) } fn insert(&self, account: SafeAccount) -> Result { let filename = account_filename(&account); self.insert_with_filename(account, filename, true) } fn remove(&self, account: &SafeAccount) -> Result<(), Error> { // enumerate all entries in keystore // and find entry with given address let to_remove = self .files_content()? .into_iter() .find(|&(_, ref acc)| acc.id == account.id && acc.address == account.address); // remove it match to_remove { None => Err(Error::InvalidAccount), Some((path, _)) => fs::remove_file(path).map_err(From::from), } } fn path(&self) -> Option<&PathBuf> { Some(&self.path) } fn as_vault_provider(&self) -> Option<&dyn VaultKeyDirectoryProvider> { Some(self) } fn unique_repr(&self) -> Result { self.last_modification_date() } } impl VaultKeyDirectoryProvider for DiskDirectory where T: KeyFileManager, { fn create(&self, name: &str, key: VaultKey) -> Result, Error> { let vault_dir = VaultDiskDirectory::create(&self.path, name, key)?; Ok(Box::new(vault_dir)) } fn open(&self, name: &str, key: VaultKey) -> Result, Error> { let vault_dir = VaultDiskDirectory::at(&self.path, name, key)?; Ok(Box::new(vault_dir)) } fn list_vaults(&self) -> Result, Error> { Ok(fs::read_dir(&self.path)? .filter_map(|e| e.ok().map(|e| e.path())) .filter_map(|path| { let mut vault_file_path = path.clone(); vault_file_path.push(VAULT_FILE_NAME); if vault_file_path.is_file() { path.file_name() .and_then(|f| f.to_str()) .map(|f| f.to_owned()) } else { None } }) .collect()) } fn vault_meta(&self, name: &str) -> Result { VaultDiskDirectory::meta_at(&self.path, name) } } impl KeyFileManager for DiskKeyFileManager { fn read(&self, filename: Option, reader: T) -> Result where T: io::Read, { let key_file = json::KeyFile::load(reader).map_err(|e| Error::Custom(format!("{:?}", e)))?; SafeAccount::from_file(key_file, filename, &self.password) } fn write(&self, mut account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write, { // when account is moved back to root directory from vault // => remove vault field from meta account.meta = json::remove_vault_name_from_json_meta(&account.meta) .map_err(|err| Error::Custom(format!("{:?}", err)))?; let key_file: json::KeyFile = account.into(); key_file .write(writer) .map_err(|e| Error::Custom(format!("{:?}", e))) } } fn account_filename(account: &SafeAccount) -> String { // build file path account.filename.clone().unwrap_or_else(|| { let timestamp = time::strftime("%Y-%m-%dT%H-%M-%S", &time::now_utc()) .expect("Time-format string is valid."); format!("UTC--{}Z--{}", timestamp, Uuid::from(account.id)) }) } #[cfg(test)] mod test { extern crate tempdir; use self::tempdir::TempDir; use super::{KeyDirectory, RootDiskDirectory, VaultKey}; use account::SafeAccount; use ethkey::{Generator, Random}; use std::{env, fs, num::NonZeroU32}; lazy_static! { static ref ITERATIONS: NonZeroU32 = NonZeroU32::new(1024).expect("1024 > 0; qed"); } #[test] fn should_create_new_account() { // given let mut dir = env::temp_dir(); dir.push("ethstore_should_create_new_account"); let keypair = Random.generate().unwrap(); let password = "hello world".into(); let directory = RootDiskDirectory::create(dir.clone()).unwrap(); // when let account = SafeAccount::create( &keypair, [0u8; 16], &password, *ITERATIONS, "Test".to_owned(), "{}".to_owned(), ); let res = directory.insert(account.unwrap()); // then assert!(res.is_ok(), "Should save account succesfuly."); assert!( res.unwrap().filename.is_some(), "Filename has been assigned." ); // cleanup let _ = fs::remove_dir_all(dir); } #[test] fn should_handle_duplicate_filenames() { // given let mut dir = env::temp_dir(); dir.push("ethstore_should_handle_duplicate_filenames"); let keypair = Random.generate().unwrap(); let password = "hello world".into(); let directory = RootDiskDirectory::create(dir.clone()).unwrap(); // when let account = SafeAccount::create( &keypair, [0u8; 16], &password, *ITERATIONS, "Test".to_owned(), "{}".to_owned(), ) .unwrap(); let filename = "test".to_string(); let dedup = true; directory .insert_with_filename(account.clone(), "foo".to_string(), dedup) .unwrap(); let file1 = directory .insert_with_filename(account.clone(), filename.clone(), dedup) .unwrap() .filename .unwrap(); let file2 = directory .insert_with_filename(account.clone(), filename.clone(), dedup) .unwrap() .filename .unwrap(); let file3 = directory .insert_with_filename(account.clone(), filename.clone(), dedup) .unwrap() .filename .unwrap(); // then // the first file should have the original names assert_eq!(file1, filename); // the following duplicate files should have a suffix appended assert!(file2 != file3); assert_eq!(file2.len(), filename.len() + 5); assert_eq!(file3.len(), filename.len() + 5); // cleanup let _ = fs::remove_dir_all(dir); } #[test] fn should_manage_vaults() { // given let mut dir = env::temp_dir(); dir.push("should_create_new_vault"); let directory = RootDiskDirectory::create(dir.clone()).unwrap(); let vault_name = "vault"; let password = "password".into(); // then assert!(directory.as_vault_provider().is_some()); // and when let before_root_items_count = fs::read_dir(&dir).unwrap().count(); let vault = directory .as_vault_provider() .unwrap() .create(vault_name, VaultKey::new(&password, *ITERATIONS)); // then assert!(vault.is_ok()); let after_root_items_count = fs::read_dir(&dir).unwrap().count(); assert!(after_root_items_count > before_root_items_count); // and when let vault = directory .as_vault_provider() .unwrap() .open(vault_name, VaultKey::new(&password, *ITERATIONS)); // then assert!(vault.is_ok()); let after_root_items_count2 = fs::read_dir(&dir).unwrap().count(); assert!(after_root_items_count == after_root_items_count2); // cleanup let _ = fs::remove_dir_all(dir); } #[test] fn should_list_vaults() { // given let temp_path = TempDir::new("").unwrap(); let directory = RootDiskDirectory::create(&temp_path).unwrap(); let vault_provider = directory.as_vault_provider().unwrap(); let iter = NonZeroU32::new(1).expect("1 > 0; qed"); vault_provider .create("vault1", VaultKey::new(&"password1".into(), iter)) .unwrap(); vault_provider .create("vault2", VaultKey::new(&"password2".into(), iter)) .unwrap(); // then let vaults = vault_provider.list_vaults().unwrap(); assert_eq!(vaults.len(), 2); assert!(vaults.iter().any(|v| &*v == "vault1")); assert!(vaults.iter().any(|v| &*v == "vault2")); } #[test] fn hash_of_files() { let temp_path = TempDir::new("").unwrap(); let directory = RootDiskDirectory::create(&temp_path).unwrap(); let hash = directory .files_hash() .expect("Files hash should be calculated ok"); assert_eq!(hash, 15130871412783076140); let keypair = Random.generate().unwrap(); let password = "test pass".into(); let account = SafeAccount::create( &keypair, [0u8; 16], &password, *ITERATIONS, "Test".to_owned(), "{}".to_owned(), ); directory .insert(account.unwrap()) .expect("Account should be inserted ok"); let new_hash = directory .files_hash() .expect("New files hash should be calculated ok"); assert!( new_hash != hash, "hash of the file list should change once directory content changed" ); } }