use super::{
super::account::Crypto,
disk::{self, DiskDirectory, KeyFileManager},
KeyDirectory, SetKeyError, VaultKey, VaultKeyDirectory,
};
use crate::{crypto::Keccak256, json, Error, SafeAccount};
use parking_lot::Mutex;
use std::{
fs, io,
path::{Path, PathBuf},
};
pub const VAULT_FILE_NAME: &str = "vault.json";
pub const VAULT_TEMP_FILE_NAME: &str = "vault_temp.json";
pub type VaultDiskDirectory = DiskDirectory<VaultKeyFileManager>;
pub struct VaultKeyFileManager {
name: String,
key: VaultKey,
meta: Mutex<String>,
}
impl VaultDiskDirectory {
pub fn create<P>(
root: P, name: &str, key: VaultKey,
) -> Result<Self, Error>
where P: AsRef<Path> {
let vault_dir_path = make_vault_dir_path(root, name, true)?;
if vault_dir_path.exists() {
return Err(Error::CreationFailed);
}
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); return Err(err);
}
Ok(DiskDirectory::new(
vault_dir_path,
VaultKeyFileManager::new(name, key, vault_meta),
))
}
pub fn at<P>(root: P, name: &str, key: VaultKey) -> Result<Self, Error>
where P: AsRef<Path> {
let vault_dir_path = make_vault_dir_path(root, name, true)?;
if !vault_dir_path.is_dir() {
return Err(Error::CreationFailed);
}
let meta = read_vault_file(&vault_dir_path, Some(&key))?;
Ok(DiskDirectory::new(
vault_dir_path,
VaultKeyFileManager::new(name, key, &meta),
))
}
pub fn meta_at<P>(root: P, name: &str) -> Result<String, Error>
where P: AsRef<Path> {
let vault_dir_path = make_vault_dir_path(root, name, true)?;
if !vault_dir_path.is_dir() {
return Err(Error::VaultNotFound);
}
read_vault_file(&vault_dir_path, None)
}
fn create_temp_vault(
&self, key: VaultKey,
) -> Result<VaultDiskDirectory, Error> {
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); 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, true)?;
}
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) -> &dyn 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)
.map_err(SetKeyError::NonFatalOld)?;
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();
temp_vault
.set_meta(&self.meta())
.map_err(SetKeyError::NonFatalOld)?;
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| {
let _ = temp_vault.delete();
SetKeyError::NonFatalOld(err)
})?;
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(SetKeyError::NonFatalNew)
}
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,
meta: Mutex::new(meta.to_owned()),
}
}
}
impl KeyFileManager for VaultKeyFileManager {
fn read<T>(
&self, filename: Option<String>, reader: T,
) -> Result<SafeAccount, Error>
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,
)?;
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<T>(
&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)))
}
}
fn make_vault_dir_path<P>(
root: P, name: &str, check_name: bool,
) -> Result<PathBuf, Error>
where P: AsRef<Path> {
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)
}
fn check_vault_name(name: &str) -> bool {
!name.is_empty()
&& name.chars().all(|c| {
c.is_alphanumeric() || c.is_whitespace() || c == '-' || c == '_'
})
}
fn create_vault_file<P>(
vault_dir_path: P, key: &VaultKey, meta: &str,
) -> Result<(), Error>
where P: AsRef<Path> {
let password_hash = key.password.as_bytes().keccak256();
let crypto =
Crypto::with_plain(&password_hash, &key.password, key.iterations)?;
let vault_file_path = vault_dir_path.as_ref().join(VAULT_FILE_NAME);
let temp_vault_file_name = disk::find_unique_filename_using_random_suffix(
vault_dir_path.as_ref(),
&VAULT_TEMP_FILE_NAME,
)?;
let temp_vault_file_path =
vault_dir_path.as_ref().join(&temp_vault_file_name);
let mut vault_file =
disk::create_new_file_with_permissions_to_owner(&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(())
}
fn read_vault_file<P>(
vault_dir_path: P, key: Option<&VaultKey>,
) -> Result<String, Error>
where P: AsRef<Path> {
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();
if let Some(key) = key {
let password_bytes = vault_file_crypto.decrypt(&key.password)?;
let password_hash = key.password.as_bytes().keccak256();
if password_hash != password_bytes.as_slice() {
return Err(Error::InvalidPassword);
}
}
Ok(vault_file_meta)
}
#[cfg(test)]
mod test {
use super::{
check_vault_name, create_vault_file, make_vault_dir_path,
read_vault_file, VaultDiskDirectory, VaultKey, VAULT_FILE_NAME,
};
use std::{fs, io::Write, path::PathBuf};
use tempdir::TempDir;
#[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"));
assert!(check_vault_name("vault中文名字"));
}
#[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() {
use std::path::Path;
assert_eq!(
&make_vault_dir_path("/home/user/parity", "vault", true).unwrap(),
&Path::new("/home/user/parity/vault")
);
assert_eq!(
&make_vault_dir_path("/home/user/parity", "*bad-name*", false)
.unwrap(),
&Path::new("/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() {
let temp_path = TempDir::new("").unwrap();
let key = VaultKey::new(&"password".into(), 1024);
let mut vault_dir: PathBuf = temp_path.path().into();
vault_dir.push("vault");
fs::create_dir_all(&vault_dir).unwrap();
let result = create_vault_file(&vault_dir, &key, "{}");
assert!(result.is_ok());
let mut vault_file_path = vault_dir;
vault_file_path.push(VAULT_FILE_NAME);
assert!(vault_file_path.exists() && vault_file_path.is_file());
}
#[test]
fn read_vault_file_succeeds() {
let temp_path = TempDir::new("").unwrap();
let key = VaultKey::new(&"password".into(), 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.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();
}
let result = read_vault_file(&dir, Some(&key));
assert!(result.is_ok());
}
#[test]
fn read_vault_file_fails() {
let temp_path = TempDir::new("").unwrap();
let key = VaultKey::new(&"password1".into(), 1024);
let dir: PathBuf = temp_path.path().into();
let mut vault_file_path: PathBuf = dir.clone();
vault_file_path.push(VAULT_FILE_NAME);
let result = read_vault_file(&dir, Some(&key));
assert!(result.is_err());
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();
}
let result = read_vault_file(&dir, Some(&key));
assert!(result.is_err());
}
#[test]
fn vault_directory_can_be_created() {
let temp_path = TempDir::new("").unwrap();
let key = VaultKey::new(&"password".into(), 1024);
let dir: PathBuf = temp_path.path().into();
let vault = VaultDiskDirectory::create(&dir, "vault", key.clone());
assert!(vault.is_ok());
let vault = VaultDiskDirectory::at(&dir, "vault", key);
assert!(vault.is_ok());
}
#[test]
fn vault_directory_cannot_be_created_if_already_exists() {
let temp_path = TempDir::new("").unwrap();
let key = VaultKey::new(&"password".into(), 1024);
let dir: PathBuf = temp_path.path().into();
let mut vault_dir = dir.clone();
vault_dir.push("vault");
fs::create_dir_all(&vault_dir).unwrap();
let vault = VaultDiskDirectory::create(&dir, "vault", key);
assert!(vault.is_err());
}
#[test]
fn vault_directory_cannot_be_opened_if_not_exists() {
let temp_path = TempDir::new("").unwrap();
let key = VaultKey::new(&"password".into(), 1024);
let dir: PathBuf = temp_path.path().into();
let vault = VaultDiskDirectory::at(&dir, "vault", key);
assert!(vault.is_err());
}
}