// Copyright 2015, 2016, 2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity 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. // Parity 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 Parity. If not, see . use std::{fs, io}; use std::path::{PathBuf, Path}; use parking_lot::Mutex; use {json, SafeAccount, Error}; use util::sha3::Hashable; use super::super::account::Crypto; use super::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError}; use super::disk::{DiskDirectory, KeyFileManager}; /// Name of vault metadata file pub const VAULT_FILE_NAME: &'static str = "vault.json"; /// Name of temporary vault metadata file pub const VAULT_TEMP_FILE_NAME: &'static str = "vault_temp.json"; /// Vault directory implementation pub type VaultDiskDirectory = DiskDirectory; /// Vault key file manager pub struct VaultKeyFileManager { name: String, key: VaultKey, meta: Mutex, } impl VaultDiskDirectory { /// Create new vault directory with given key pub fn create

(root: P, name: &str, key: VaultKey) -> Result where P: AsRef { // check that vault directory does not exists let vault_dir_path = make_vault_dir_path(root, name, true)?; if vault_dir_path.exists() { return Err(Error::CreationFailed); } // create vault && vault file let vault_meta = "{}"; fs::create_dir_all(&vault_dir_path)?; if let Err(err) = create_vault_file(&vault_dir_path, &key, vault_meta) { let _ = fs::remove_dir_all(&vault_dir_path); // can't do anything with this return Err(err); } Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(name, key, vault_meta))) } /// Open existing vault directory with given key pub fn at

(root: P, name: &str, key: VaultKey) -> Result where P: AsRef { // check that vault directory exists let vault_dir_path = make_vault_dir_path(root, name, true)?; if !vault_dir_path.is_dir() { return Err(Error::CreationFailed); } // check that passed key matches vault file let meta = read_vault_file(&vault_dir_path, &key)?; Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(name, key, &meta))) } fn create_temp_vault(&self, key: VaultKey) -> Result { let original_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed"); let mut path: PathBuf = original_path.clone(); let name = self.name(); path.push(name); // to jump to the next level let mut index = 0; loop { let name = format!("{}_temp_{}", name, index); path.set_file_name(&name); if !path.exists() { return VaultDiskDirectory::create(original_path, &name, key); } index += 1; } } fn copy_to_vault(&self, vault: &VaultDiskDirectory) -> Result<(), Error> { for account in self.load()? { let filename = account.filename.clone().expect("self is instance of DiskDirectory; DiskDirectory fills filename in load; qed"); vault.insert_with_filename(account, filename)?; } Ok(()) } fn delete(&self) -> Result<(), Error> { let path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed"); fs::remove_dir_all(path).map_err(Into::into) } } impl VaultKeyDirectory for VaultDiskDirectory { fn as_key_directory(&self) -> &KeyDirectory { self } fn name(&self) -> &str { &self.key_manager().name } fn key(&self) -> VaultKey { self.key_manager().key.clone() } fn set_key(&self, new_key: VaultKey) -> Result<(), SetKeyError> { let temp_vault = VaultDiskDirectory::create_temp_vault(self, new_key.clone()).map_err(|err| SetKeyError::NonFatalOld(err))?; let mut source_path = temp_vault.path().expect("temp_vault is instance of DiskDirectory; DiskDirectory always returns path; qed").clone(); let mut target_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed").clone(); // jump to next fs level source_path.push("next"); target_path.push("next"); let temp_accounts = self.copy_to_vault(&temp_vault) .and_then(|_| temp_vault.load()) .map_err(|err| { // ignore error, as we already processing error let _ = temp_vault.delete(); SetKeyError::NonFatalOld(err) })?; // we can't just delete temp vault until all files moved, because // original vault content has already been partially replaced // => when error or crash happens here, we can't do anything for temp_account in temp_accounts { let filename = temp_account.filename.expect("self is instance of DiskDirectory; DiskDirectory fills filename in load; qed"); source_path.set_file_name(&filename); target_path.set_file_name(&filename); fs::rename(&source_path, &target_path).map_err(|err| SetKeyError::Fatal(err.into()))?; } source_path.set_file_name(VAULT_FILE_NAME); target_path.set_file_name(VAULT_FILE_NAME); fs::rename(source_path, target_path).map_err(|err| SetKeyError::Fatal(err.into()))?; temp_vault.delete().map_err(|err| SetKeyError::NonFatalNew(err)) } fn meta(&self) -> String { self.key_manager().meta.lock().clone() } fn set_meta(&self, meta: &str) -> Result<(), Error> { let key_manager = self.key_manager(); let vault_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed"); create_vault_file(vault_path, &key_manager.key, meta)?; *key_manager.meta.lock() = meta.to_owned(); Ok(()) } } impl VaultKeyFileManager { pub fn new(name: &str, key: VaultKey, meta: &str) -> Self { VaultKeyFileManager { name: name.into(), key: key, meta: Mutex::new(meta.to_owned()), } } } impl KeyFileManager for VaultKeyFileManager { fn read(&self, filename: Option, reader: T) -> Result where T: io::Read { let vault_file = json::VaultKeyFile::load(reader).map_err(|e| Error::Custom(format!("{:?}", e)))?; let mut safe_account = SafeAccount::from_vault_file(&self.key.password, vault_file, filename.clone())?; safe_account.meta = json::insert_vault_name_to_json_meta(&safe_account.meta, &self.name) .map_err(|err| Error::Custom(format!("{:?}", err)))?; Ok(safe_account) } fn write(&self, mut account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write { account.meta = json::remove_vault_name_from_json_meta(&account.meta) .map_err(|err| Error::Custom(format!("{:?}", err)))?; let vault_file: json::VaultKeyFile = account.into_vault_file(self.key.iterations, &self.key.password)?; vault_file.write(writer).map_err(|e| Error::Custom(format!("{:?}", e))) } } /// Makes path to vault directory, checking that vault name is appropriate fn make_vault_dir_path

(root: P, name: &str, check_name: bool) -> Result where P: AsRef { // check vault name if check_name && !check_vault_name(name) { return Err(Error::InvalidVaultName); } let mut vault_dir_path: PathBuf = root.as_ref().into(); vault_dir_path.push(name); Ok(vault_dir_path) } /// Every vault must have unique name => we rely on filesystem to check this /// => vault name must not contain any fs-special characters to avoid directory traversal /// => we only allow alphanumeric + separator characters in vault name. fn check_vault_name(name: &str) -> bool { !name.is_empty() && name.chars() .all(|c| c.is_alphanumeric() || c.is_whitespace() || c == '-' || c == '_') } /// Vault can be empty, but still must be pluggable => we store vault password in separate file fn create_vault_file

(vault_dir_path: P, key: &VaultKey, meta: &str) -> Result<(), Error> where P: AsRef { let password_hash = key.password.sha3(); let crypto = Crypto::with_plain(&password_hash, &key.password, key.iterations); let mut vault_file_path: PathBuf = vault_dir_path.as_ref().into(); vault_file_path.push(VAULT_FILE_NAME); let mut temp_vault_file_path: PathBuf = vault_dir_path.as_ref().into(); temp_vault_file_path.push(VAULT_TEMP_FILE_NAME); // this method is used to rewrite existing vault file // => write to temporary file first, then rename temporary file to vault file let mut vault_file = fs::File::create(&temp_vault_file_path)?; let vault_file_contents = json::VaultFile { crypto: crypto.into(), meta: Some(meta.to_owned()), }; vault_file_contents.write(&mut vault_file).map_err(|e| Error::Custom(format!("{:?}", e)))?; drop(vault_file); fs::rename(&temp_vault_file_path, &vault_file_path)?; Ok(()) } /// When vault is opened => we must check that password matches && read metadata fn read_vault_file

(vault_dir_path: P, key: &VaultKey) -> Result where P: AsRef { let mut vault_file_path: PathBuf = vault_dir_path.as_ref().into(); vault_file_path.push(VAULT_FILE_NAME); let vault_file = fs::File::open(vault_file_path)?; let vault_file_contents = json::VaultFile::load(vault_file).map_err(|e| Error::Custom(format!("{:?}", e)))?; let vault_file_meta = vault_file_contents.meta.unwrap_or("{}".to_owned()); let vault_file_crypto: Crypto = vault_file_contents.crypto.into(); let password_bytes = vault_file_crypto.decrypt(&key.password)?; let password_hash = key.password.sha3(); if &*password_hash != password_bytes.as_slice() { return Err(Error::InvalidPassword); } Ok(vault_file_meta) } #[cfg(test)] mod test { use std::fs; use std::io::Write; use std::path::PathBuf; use dir::{VaultKey, VaultKeyDirectory}; use super::{VAULT_FILE_NAME, check_vault_name, make_vault_dir_path, create_vault_file, read_vault_file, VaultDiskDirectory}; use devtools::RandomTempPath; #[test] fn check_vault_name_succeeds() { assert!(check_vault_name("vault")); assert!(check_vault_name("vault with spaces")); assert!(check_vault_name("vault with tabs")); assert!(check_vault_name("vault_with_underscores")); assert!(check_vault_name("vault-with-dashes")); assert!(check_vault_name("vault-with-digits-123")); } #[test] fn check_vault_name_fails() { assert!(!check_vault_name("")); assert!(!check_vault_name(".")); assert!(!check_vault_name("*")); assert!(!check_vault_name("../.bash_history")); assert!(!check_vault_name("/etc/passwd")); assert!(!check_vault_name("c:\\windows")); } #[test] fn make_vault_dir_path_succeeds() { assert_eq!(make_vault_dir_path("/home/user/parity", "vault", true).unwrap().to_str().unwrap(), "/home/user/parity/vault"); assert_eq!(make_vault_dir_path("/home/user/parity", "*bad-name*", false).unwrap().to_str().unwrap(), "/home/user/parity/*bad-name*"); } #[test] fn make_vault_dir_path_fails() { assert!(make_vault_dir_path("/home/user/parity", "*bad-name*", true).is_err()); } #[test] fn create_vault_file_succeeds() { // given let temp_path = RandomTempPath::new(); let key = VaultKey::new("password", 1024); let mut vault_dir: PathBuf = temp_path.as_path().into(); vault_dir.push("vault"); fs::create_dir_all(&vault_dir).unwrap(); // when let result = create_vault_file(&vault_dir, &key, "{}"); // then assert!(result.is_ok()); let mut vault_file_path = vault_dir.clone(); vault_file_path.push(VAULT_FILE_NAME); assert!(vault_file_path.exists() && vault_file_path.is_file()); } #[test] fn read_vault_file_succeeds() { // given let temp_path = RandomTempPath::create_dir(); let key = VaultKey::new("password", 1024); let vault_file_contents = r#"{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"758696c8dc6378ab9b25bb42790da2f5"},"ciphertext":"54eb50683717d41caaeb12ea969f2c159daada5907383f26f327606a37dc7168","kdf":"pbkdf2","kdfparams":{"c":1024,"dklen":32,"prf":"hmac-sha256","salt":"3c320fa566a1a7963ac8df68a19548d27c8f40bf92ef87c84594dcd5bbc402b6"},"mac":"9e5c2314c2a0781962db85611417c614bd6756666b6b1e93840f5b6ed895f003"}}"#; let dir: PathBuf = temp_path.as_path().into(); let mut vault_file_path: PathBuf = dir.clone(); vault_file_path.push(VAULT_FILE_NAME); { let mut vault_file = fs::File::create(vault_file_path).unwrap(); vault_file.write_all(vault_file_contents.as_bytes()).unwrap(); } // when let result = read_vault_file(&dir, &key); // then assert!(result.is_ok()); } #[test] fn read_vault_file_fails() { // given let temp_path = RandomTempPath::create_dir(); let key = VaultKey::new("password1", 1024); let dir: PathBuf = temp_path.as_path().into(); let mut vault_file_path: PathBuf = dir.clone(); vault_file_path.push(VAULT_FILE_NAME); // when let result = read_vault_file(&dir, &key); // then assert!(result.is_err()); // and when given let vault_file_contents = r#"{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"0155e3690be19fbfbecabcd440aa284b"},"ciphertext":"4d6938a1f49b7782","kdf":"pbkdf2","kdfparams":{"c":1024,"dklen":32,"prf":"hmac-sha256","salt":"b6a9338a7ccd39288a86dba73bfecd9101b4f3db9c9830e7c76afdbd4f6872e5"},"mac":"16381463ea11c6eb2239a9f339c2e780516d29d234ce30ac5f166f9080b5a262"}}"#; { let mut vault_file = fs::File::create(vault_file_path).unwrap(); vault_file.write_all(vault_file_contents.as_bytes()).unwrap(); } // when let result = read_vault_file(&dir, &key); // then assert!(result.is_err()); } #[test] fn vault_directory_can_be_created() { // given let temp_path = RandomTempPath::new(); let key = VaultKey::new("password", 1024); let dir: PathBuf = temp_path.as_path().into(); // when let vault = VaultDiskDirectory::create(&dir, "vault", key.clone()); // then assert!(vault.is_ok()); // and when let vault = VaultDiskDirectory::at(&dir, "vault", key); // then assert!(vault.is_ok()); } #[test] fn vault_directory_cannot_be_created_if_already_exists() { // given let temp_path = RandomTempPath::new(); let key = VaultKey::new("password", 1024); let dir: PathBuf = temp_path.as_path().into(); let mut vault_dir = dir.clone(); vault_dir.push("vault"); fs::create_dir_all(&vault_dir).unwrap(); // when let vault = VaultDiskDirectory::create(&dir, "vault", key); // then assert!(vault.is_err()); } #[test] fn vault_directory_cannot_be_opened_if_not_exists() { // given let temp_path = RandomTempPath::create_dir(); let key = VaultKey::new("password", 1024); let dir: PathBuf = temp_path.as_path().into(); // when let vault = VaultDiskDirectory::at(&dir, "vault", key); // then assert!(vault.is_err()); } #[test] fn vault_directory_can_preserve_meta() { // given let temp_path = RandomTempPath::new(); let key = VaultKey::new("password", 1024); let dir: PathBuf = temp_path.as_path().into(); let vault = VaultDiskDirectory::create(&dir, "vault", key.clone()).unwrap(); // then assert_eq!(vault.meta(), "{}".to_owned()); assert!(vault.set_meta("Hello, world!!!").is_ok()); assert_eq!(vault.meta(), "Hello, world!!!".to_owned()); // and when let vault = VaultDiskDirectory::at(&dir, "vault", key.clone()).unwrap(); assert_eq!(vault.meta(), "Hello, world!!!".to_owned()); } }