Vaults RPCs (#4366)
* vaults RPCs * vault.password != vault_account.password * moved vault RPCs to parityAccounts NS * parity_listVaults + parity_listOpenedVaults
This commit is contained in:
committed by
Gav Wood
parent
e257e4e3bd
commit
2f340a547a
@@ -23,6 +23,7 @@ parking_lot = "0.3"
|
||||
ethcrypto = { path = "../ethcrypto" }
|
||||
ethcore-util = { path = "../util" }
|
||||
smallvec = "0.3.1"
|
||||
ethcore-devtools = { path = "../devtools" }
|
||||
|
||||
[build-dependencies]
|
||||
serde_codegen = { version = "0.8", optional = true }
|
||||
|
||||
@@ -21,7 +21,7 @@ use time;
|
||||
use {json, SafeAccount, Error};
|
||||
use json::Uuid;
|
||||
use super::{KeyDirectory, VaultKeyDirectory, VaultKeyDirectoryProvider, VaultKey};
|
||||
use super::vault::VaultDiskDirectory;
|
||||
use super::vault::{VAULT_FILE_NAME, VaultDiskDirectory};
|
||||
|
||||
const IGNORED_FILES: &'static [&'static str] = &[
|
||||
"thumbs.db",
|
||||
@@ -193,7 +193,7 @@ impl<T> KeyDirectory for DiskDirectory<T> where T: KeyFileManager {
|
||||
// and find entry with given address
|
||||
let to_remove = self.files()?
|
||||
.into_iter()
|
||||
.find(|&(_, ref acc)| acc == account);
|
||||
.find(|&(_, ref acc)| acc.id == account.id && acc.address == account.address);
|
||||
|
||||
// remove it
|
||||
match to_remove {
|
||||
@@ -219,6 +219,21 @@ impl<T> VaultKeyDirectoryProvider for DiskDirectory<T> where T: KeyFileManager {
|
||||
let vault_dir = VaultDiskDirectory::at(&self.path, name, key)?;
|
||||
Ok(Box::new(vault_dir))
|
||||
}
|
||||
|
||||
fn list_vaults(&self) -> Result<Vec<String>, 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())
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyFileManager for DiskKeyFileManager {
|
||||
@@ -240,6 +255,7 @@ mod test {
|
||||
use dir::{KeyDirectory, VaultKey};
|
||||
use account::SafeAccount;
|
||||
use ethkey::{Random, Generator};
|
||||
use devtools::RandomTempPath;
|
||||
|
||||
#[test]
|
||||
fn should_create_new_account() {
|
||||
@@ -295,4 +311,20 @@ mod test {
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_list_vaults() {
|
||||
// given
|
||||
let temp_path = RandomTempPath::new();
|
||||
let directory = RootDiskDirectory::create(&temp_path).unwrap();
|
||||
let vault_provider = directory.as_vault_provider().unwrap();
|
||||
vault_provider.create("vault1", VaultKey::new("password1", 1)).unwrap();
|
||||
vault_provider.create("vault2", VaultKey::new("password2", 1)).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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ pub trait VaultKeyDirectoryProvider {
|
||||
fn create(&self, name: &str, key: VaultKey) -> Result<Box<VaultKeyDirectory>, Error>;
|
||||
/// Open existing vault with given key
|
||||
fn open(&self, name: &str, key: VaultKey) -> Result<Box<VaultKeyDirectory>, Error>;
|
||||
/// List all vaults
|
||||
fn list_vaults(&self) -> Result<Vec<String>, Error>;
|
||||
}
|
||||
|
||||
/// Vault directory
|
||||
@@ -78,8 +80,10 @@ pub trait VaultKeyDirectory: KeyDirectory {
|
||||
fn as_key_directory(&self) -> &KeyDirectory;
|
||||
/// Vault name
|
||||
fn name(&self) -> &str;
|
||||
/// Get vault key
|
||||
fn key(&self) -> VaultKey;
|
||||
/// Set new key for vault
|
||||
fn set_key(&self, old_key: VaultKey, key: VaultKey) -> Result<(), SetKeyError>;
|
||||
fn set_key(&self, key: VaultKey) -> Result<(), SetKeyError>;
|
||||
}
|
||||
|
||||
pub use self::disk::RootDiskDirectory;
|
||||
|
||||
@@ -22,13 +22,15 @@ use super::super::account::Crypto;
|
||||
use super::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError};
|
||||
use super::disk::{DiskDirectory, KeyFileManager};
|
||||
|
||||
const VAULT_FILE_NAME: &'static str = "vault.json";
|
||||
/// Name of vault metadata file
|
||||
pub const VAULT_FILE_NAME: &'static str = "vault.json";
|
||||
|
||||
/// Vault directory implementation
|
||||
pub type VaultDiskDirectory = DiskDirectory<VaultKeyFileManager>;
|
||||
|
||||
/// Vault key file manager
|
||||
pub struct VaultKeyFileManager {
|
||||
name: String,
|
||||
key: VaultKey,
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ impl VaultDiskDirectory {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(key)))
|
||||
Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(name, key)))
|
||||
}
|
||||
|
||||
/// Open existing vault directory with given key
|
||||
@@ -62,7 +64,7 @@ impl VaultDiskDirectory {
|
||||
// check that passed key matches vault file
|
||||
check_vault_file(&vault_dir_path, &key)?;
|
||||
|
||||
Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(key)))
|
||||
Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(name, key)))
|
||||
}
|
||||
|
||||
fn create_temp_vault(&self, key: VaultKey) -> Result<VaultDiskDirectory, Error> {
|
||||
@@ -84,12 +86,10 @@ impl VaultDiskDirectory {
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_to_vault(&self, vault: &VaultDiskDirectory, vault_key: &VaultKey) -> Result<(), Error> {
|
||||
let password = &self.key_manager().key.password;
|
||||
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");
|
||||
let new_account = account.change_password(password, &vault_key.password, vault_key.iterations)?;
|
||||
vault.insert_with_filename(new_account, filename)?;
|
||||
vault.insert_with_filename(account, filename)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -107,19 +107,14 @@ impl VaultKeyDirectory for VaultDiskDirectory {
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.path()
|
||||
.expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed")
|
||||
.file_name()
|
||||
.expect("last component of path is checked in make_vault_dir_path; it contains no fs-specific characters; file_name only returns None if last component is fs-specific; qed")
|
||||
.to_str()
|
||||
.expect("last component of path is checked in make_vault_dir_path; it contains only valid unicode characters; to_str fails when file_name is not valid unicode; qed")
|
||||
&self.key_manager().name
|
||||
}
|
||||
|
||||
fn set_key(&self, key: VaultKey, new_key: VaultKey) -> Result<(), SetKeyError> {
|
||||
if self.key_manager().key != key {
|
||||
return Err(SetKeyError::NonFatalOld(Error::InvalidPassword));
|
||||
}
|
||||
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();
|
||||
@@ -127,7 +122,7 @@ impl VaultKeyDirectory for VaultDiskDirectory {
|
||||
source_path.push("next");
|
||||
target_path.push("next");
|
||||
|
||||
let temp_accounts = self.copy_to_vault(&temp_vault, &new_key)
|
||||
let temp_accounts = self.copy_to_vault(&temp_vault)
|
||||
.and_then(|_| temp_vault.load())
|
||||
.map_err(|err| {
|
||||
// ignore error, as we already processing error
|
||||
@@ -153,8 +148,9 @@ impl VaultKeyDirectory for VaultDiskDirectory {
|
||||
}
|
||||
|
||||
impl VaultKeyFileManager {
|
||||
pub fn new(key: VaultKey) -> Self {
|
||||
pub fn new(name: &str, key: VaultKey) -> Self {
|
||||
VaultKeyFileManager {
|
||||
name: name.into(),
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
@@ -163,20 +159,16 @@ impl VaultKeyFileManager {
|
||||
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 safe_account = SafeAccount::from_vault_file(&self.key.password, vault_file, filename.clone())?;
|
||||
if !safe_account.check_password(&self.key.password) {
|
||||
warn!("Invalid vault key file: {:?}", filename);
|
||||
return Err(Error::InvalidPassword);
|
||||
}
|
||||
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<T>(&self, account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write {
|
||||
// all accounts share the same password
|
||||
if !account.check_password(&self.key.password) {
|
||||
return Err(Error::InvalidPassword);
|
||||
}
|
||||
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)))
|
||||
@@ -243,10 +235,12 @@ fn check_vault_file<P>(vault_dir_path: P, key: &VaultKey) -> Result<(), Error> w
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{env, fs};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use dir::VaultKey;
|
||||
use super::{VAULT_FILE_NAME, check_vault_name, make_vault_dir_path, create_vault_file, check_vault_file, VaultDiskDirectory};
|
||||
use devtools::RandomTempPath;
|
||||
|
||||
#[test]
|
||||
fn check_vault_name_succeeds() {
|
||||
@@ -282,10 +276,9 @@ mod test {
|
||||
#[test]
|
||||
fn create_vault_file_succeeds() {
|
||||
// given
|
||||
let temp_path = RandomTempPath::new();
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("create_vault_file_succeeds");
|
||||
let mut vault_dir = dir.clone();
|
||||
let mut vault_dir: PathBuf = temp_path.as_path().into();
|
||||
vault_dir.push("vault");
|
||||
fs::create_dir_all(&vault_dir).unwrap();
|
||||
|
||||
@@ -297,20 +290,16 @@ mod test {
|
||||
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());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_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 mut dir = env::temp_dir();
|
||||
dir.push("check_vault_file_succeeds");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let mut vault_file_path = dir.clone();
|
||||
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();
|
||||
@@ -322,20 +311,16 @@ mod test {
|
||||
|
||||
// then
|
||||
assert!(result.is_ok());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_vault_file_fails() {
|
||||
// given
|
||||
let temp_path = RandomTempPath::create_dir();
|
||||
let key = VaultKey::new("password1", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("check_vault_file_fails");
|
||||
let mut vault_file_path = dir.clone();
|
||||
let dir: PathBuf = temp_path.as_path().into();
|
||||
let mut vault_file_path: PathBuf = dir.clone();
|
||||
vault_file_path.push(VAULT_FILE_NAME);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
// when
|
||||
let result = check_vault_file(&dir, &key);
|
||||
@@ -355,17 +340,14 @@ mod test {
|
||||
|
||||
// then
|
||||
assert!(result.is_err());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_directory_can_be_created() {
|
||||
// given
|
||||
let temp_path = RandomTempPath::new();
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("vault_directory_can_be_created");
|
||||
let dir: PathBuf = temp_path.as_path().into();
|
||||
|
||||
// when
|
||||
let vault = VaultDiskDirectory::create(&dir, "vault", key.clone());
|
||||
@@ -378,17 +360,14 @@ mod test {
|
||||
|
||||
// then
|
||||
assert!(vault.is_ok());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_directory_cannot_be_created_if_already_exists() {
|
||||
// given
|
||||
let temp_path = RandomTempPath::new();
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("vault_directory_cannot_be_created_if_already_exists");
|
||||
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();
|
||||
@@ -398,25 +377,19 @@ mod test {
|
||||
|
||||
// then
|
||||
assert!(vault.is_err());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_directory_cannot_be_opened_if_not_exists() {
|
||||
// given
|
||||
let temp_path = RandomTempPath::create_dir();
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("vault_directory_cannot_be_opened_if_not_exists");
|
||||
let dir: PathBuf = temp_path.as_path().into();
|
||||
|
||||
// when
|
||||
let vault = VaultDiskDirectory::at(&dir, "vault", key);
|
||||
|
||||
// then
|
||||
assert!(vault.is_err());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ impl SimpleSecretStore for EthStore {
|
||||
self.store.insert_account(vault, secret, password)
|
||||
}
|
||||
|
||||
fn account_ref(&self, address: &Address) -> Result<StoreAccountRef, Error> {
|
||||
self.store.account_ref(address)
|
||||
}
|
||||
|
||||
fn accounts(&self) -> Result<Vec<StoreAccountRef>, Error> {
|
||||
self.store.accounts()
|
||||
}
|
||||
@@ -88,8 +92,20 @@ impl SimpleSecretStore for EthStore {
|
||||
self.store.close_vault(name)
|
||||
}
|
||||
|
||||
fn change_vault_password(&self, name: &str, password: &str, new_password: &str) -> Result<(), Error> {
|
||||
self.store.change_vault_password(name, password, new_password)
|
||||
fn list_vaults(&self) -> Result<Vec<String>, Error> {
|
||||
self.store.list_vaults()
|
||||
}
|
||||
|
||||
fn list_opened_vaults(&self) -> Result<Vec<String>, Error> {
|
||||
self.store.list_opened_vaults()
|
||||
}
|
||||
|
||||
fn change_vault_password(&self, name: &str, new_password: &str) -> Result<(), Error> {
|
||||
self.store.change_vault_password(name, new_password)
|
||||
}
|
||||
|
||||
fn change_account_vault(&self, vault: SecretVaultRef, account: StoreAccountRef) -> Result<StoreAccountRef, Error> {
|
||||
self.store.change_account_vault(vault, account)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,12 +137,6 @@ impl SecretStore for EthStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_account(&self, new_store: &SimpleSecretStore, new_vault: SecretVaultRef, account: &StoreAccountRef, password: &str, new_password: &str) -> Result<(), Error> {
|
||||
self.copy_account(new_store, new_vault, account, password, new_password)?;
|
||||
self.remove_account(account, password)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn public(&self, account: &StoreAccountRef, password: &str) -> Result<Public, Error> {
|
||||
let account = self.get(account)?;
|
||||
account.public(password)
|
||||
@@ -296,6 +306,32 @@ impl EthMultiStore {
|
||||
|
||||
}
|
||||
|
||||
fn remove_safe_account(&self, account_ref: &StoreAccountRef, account: &SafeAccount) -> Result<(), Error> {
|
||||
// Remove from dir
|
||||
match account_ref.vault {
|
||||
SecretVaultRef::Root => self.dir.remove(&account)?,
|
||||
SecretVaultRef::Vault(ref vault_name) => self.vaults.lock().get(vault_name).ok_or(Error::VaultNotFound)?.remove(&account)?,
|
||||
};
|
||||
|
||||
// Remove from cache
|
||||
let mut cache = self.cache.write();
|
||||
let is_empty = {
|
||||
if let Some(accounts) = cache.get_mut(account_ref) {
|
||||
if let Some(position) = accounts.iter().position(|acc| acc == account) {
|
||||
accounts.remove(position);
|
||||
}
|
||||
accounts.is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if is_empty {
|
||||
cache.remove(account_ref);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleSecretStore for EthMultiStore {
|
||||
@@ -306,6 +342,14 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
self.import(vault, account)
|
||||
}
|
||||
|
||||
fn account_ref(&self, address: &Address) -> Result<StoreAccountRef, Error> {
|
||||
self.reload_accounts()?;
|
||||
self.cache.read().keys()
|
||||
.find(|r| &r.address == address)
|
||||
.cloned()
|
||||
.ok_or(Error::InvalidAccount)
|
||||
}
|
||||
|
||||
fn accounts(&self) -> Result<Vec<StoreAccountRef>, Error> {
|
||||
self.reload_accounts()?;
|
||||
Ok(self.cache.read().keys().cloned().collect())
|
||||
@@ -320,50 +364,20 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove from dir
|
||||
match account_ref.vault {
|
||||
SecretVaultRef::Root => self.dir.remove(&account)?,
|
||||
SecretVaultRef::Vault(ref vault_name) => self.vaults.lock().get(vault_name).ok_or(Error::VaultNotFound)?.remove(&account)?,
|
||||
};
|
||||
|
||||
// Remove from cache
|
||||
let mut cache = self.cache.write();
|
||||
let is_empty = {
|
||||
if let Some(accounts) = cache.get_mut(account_ref) {
|
||||
if let Some(position) = accounts.iter().position(|acc| acc == &account) {
|
||||
accounts.remove(position);
|
||||
}
|
||||
accounts.is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if is_empty {
|
||||
cache.remove(account_ref);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
return self.remove_safe_account(account_ref, &account);
|
||||
}
|
||||
Err(Error::InvalidPassword)
|
||||
}
|
||||
|
||||
fn change_password(&self, account_ref: &StoreAccountRef, old_password: &str, new_password: &str) -> Result<(), Error> {
|
||||
match account_ref.vault {
|
||||
SecretVaultRef::Root => {
|
||||
let accounts = self.get(account_ref)?;
|
||||
let accounts = self.get(account_ref)?;
|
||||
|
||||
for account in accounts {
|
||||
// Change password
|
||||
let new_account = account.change_password(old_password, new_password, self.iterations)?;
|
||||
self.update(account_ref, account, new_account)?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
SecretVaultRef::Vault(ref vault_name) => {
|
||||
self.change_vault_password(vault_name, old_password, new_password)
|
||||
},
|
||||
for account in accounts {
|
||||
// Change password
|
||||
let new_account = account.change_password(old_password, new_password, self.iterations)?;
|
||||
self.update(account_ref, account, new_account)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result<Signature, Error> {
|
||||
@@ -435,10 +449,20 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn change_vault_password(&self, name: &str, password: &str, new_password: &str) -> Result<(), Error> {
|
||||
fn list_vaults(&self) -> Result<Vec<String>, Error> {
|
||||
let vault_provider = self.dir.as_vault_provider().ok_or(Error::VaultsAreNotSupported)?;
|
||||
let vault = vault_provider.open(name, VaultKey::new(password, self.iterations))?;
|
||||
match vault.set_key(VaultKey::new(password, self.iterations), VaultKey::new(new_password, self.iterations)) {
|
||||
vault_provider.list_vaults()
|
||||
}
|
||||
|
||||
fn list_opened_vaults(&self) -> Result<Vec<String>, Error> {
|
||||
Ok(self.vaults.lock().keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn change_vault_password(&self, name: &str, new_password: &str) -> Result<(), Error> {
|
||||
let old_key = self.vaults.lock().get(name).map(|v| v.key()).ok_or(Error::VaultNotFound)?;
|
||||
let vault_provider = self.dir.as_vault_provider().ok_or(Error::VaultsAreNotSupported)?;
|
||||
let vault = vault_provider.open(name, old_key)?;
|
||||
match vault.set_key(VaultKey::new(new_password, self.iterations)) {
|
||||
Ok(_) => {
|
||||
self.close_vault(name)
|
||||
.and_then(|_| self.open_vault(name, new_password))
|
||||
@@ -446,7 +470,7 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
Err(SetKeyError::Fatal(err)) => {
|
||||
let _ = self.close_vault(name);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
Err(SetKeyError::NonFatalNew(err)) => {
|
||||
let _ = self.close_vault(name)
|
||||
.and_then(|_| self.open_vault(name, new_password));
|
||||
@@ -455,17 +479,28 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
Err(SetKeyError::NonFatalOld(err)) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn change_account_vault(&self, vault: SecretVaultRef, account_ref: StoreAccountRef) -> Result<StoreAccountRef, Error> {
|
||||
if account_ref.vault == vault {
|
||||
return Ok(account_ref);
|
||||
}
|
||||
|
||||
let account = self.get(&account_ref)?.into_iter().nth(0).ok_or(Error::InvalidAccount)?;
|
||||
let new_account_ref = self.import(vault, account.clone())?;
|
||||
self.remove_safe_account(&account_ref, &account)?;
|
||||
self.reload_accounts()?;
|
||||
Ok(new_account_ref)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::{env, fs};
|
||||
use std::path::PathBuf;
|
||||
use dir::{KeyDirectory, MemoryDirectory, RootDiskDirectory};
|
||||
use ethkey::{Random, Generator, KeyPair};
|
||||
use secret_store::{SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef};
|
||||
use super::{EthStore, EthMultiStore};
|
||||
use devtools::RandomTempPath;
|
||||
|
||||
fn keypair() -> KeyPair {
|
||||
Random.generate().unwrap()
|
||||
@@ -481,26 +516,17 @@ mod tests {
|
||||
|
||||
struct RootDiskDirectoryGuard {
|
||||
pub key_dir: Option<Box<KeyDirectory>>,
|
||||
path: Option<PathBuf>,
|
||||
_path: RandomTempPath,
|
||||
}
|
||||
|
||||
impl RootDiskDirectoryGuard {
|
||||
pub fn new(test_name: &str) -> Self {
|
||||
let mut path = env::temp_dir();
|
||||
path.push(test_name);
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
pub fn new() -> Self {
|
||||
let temp_path = RandomTempPath::new();
|
||||
let disk_dir = Box::new(RootDiskDirectory::create(temp_path.as_path()).unwrap());
|
||||
|
||||
RootDiskDirectoryGuard {
|
||||
key_dir: Some(Box::new(RootDiskDirectory::create(&path).unwrap())),
|
||||
path: Some(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RootDiskDirectoryGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(path) = self.path.take() {
|
||||
let _ = fs::remove_dir_all(path);
|
||||
key_dir: Some(disk_dir),
|
||||
_path: temp_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,7 +632,7 @@ mod tests {
|
||||
#[test]
|
||||
fn should_create_and_open_vaults() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_create_and_open_vaults");
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let name2 = "vault2"; let password2 = "password2";
|
||||
@@ -660,7 +686,7 @@ mod tests {
|
||||
#[test]
|
||||
fn should_move_vault_acounts() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_move_vault_acounts");
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let name2 = "vault2"; let password2 = "password2";
|
||||
@@ -677,72 +703,42 @@ mod tests {
|
||||
let account3 = store.insert_account(SecretVaultRef::Root, keypair3.secret().clone(), password3).unwrap();
|
||||
|
||||
// then
|
||||
store.move_account(&store, SecretVaultRef::Root, &account1, password1, password2).unwrap();
|
||||
store.move_account(&store, SecretVaultRef::Vault(name2.to_owned()), &account2, password1, password2).unwrap();
|
||||
store.move_account(&store, SecretVaultRef::Vault(name2.to_owned()), &account3, password3, password2).unwrap();
|
||||
let account1 = store.change_account_vault(SecretVaultRef::Root, account1.clone()).unwrap();
|
||||
let account2 = store.change_account_vault(SecretVaultRef::Vault(name2.to_owned()), account2.clone()).unwrap();
|
||||
let account3 = store.change_account_vault(SecretVaultRef::Vault(name2.to_owned()), account3).unwrap();
|
||||
let accounts = store.accounts().unwrap();
|
||||
assert_eq!(accounts.len(), 3);
|
||||
assert!(accounts.iter().any(|a| a == &StoreAccountRef::root(account1.address.clone())));
|
||||
assert!(accounts.iter().any(|a| a == &StoreAccountRef::vault(name2, account2.address.clone())));
|
||||
assert!(accounts.iter().any(|a| a == &StoreAccountRef::vault(name2, account3.address.clone())));
|
||||
|
||||
// and then
|
||||
assert_eq!(store.meta(&StoreAccountRef::root(account1.address)).unwrap(), r#"{}"#);
|
||||
assert_eq!(store.meta(&StoreAccountRef::vault("vault2", account2.address)).unwrap(), r#"{"vault":"vault2"}"#);
|
||||
assert_eq!(store.meta(&StoreAccountRef::vault("vault2", account3.address)).unwrap(), r#"{"vault":"vault2"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_remove_account_when_moving_to_self() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_not_remove_account_when_moving_to_self");
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let password1 = "password1";
|
||||
let keypair1 = keypair();
|
||||
|
||||
// when
|
||||
let account1 = store.insert_account(SecretVaultRef::Root, keypair1.secret().clone(), password1).unwrap();
|
||||
store.move_account(&store, SecretVaultRef::Root, &account1, password1, password1).unwrap();
|
||||
store.change_account_vault(SecretVaultRef::Root, account1).unwrap();
|
||||
|
||||
// then
|
||||
let accounts = store.accounts().unwrap();
|
||||
assert_eq!(accounts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_move_account_when_vault_password_incorrect() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_not_move_account_when_vault_password_incorrect");
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let name2 = "vault2"; let password2 = "password2";
|
||||
let keypair1 = keypair();
|
||||
|
||||
// when
|
||||
store.create_vault(name1, password1).unwrap();
|
||||
store.create_vault(name2, password2).unwrap();
|
||||
let account1 = store.insert_account(SecretVaultRef::Vault(name1.to_owned()), keypair1.secret().clone(), password1).unwrap();
|
||||
|
||||
// then
|
||||
store.move_account(&store, SecretVaultRef::Root, &account1, password2, password1).unwrap_err();
|
||||
store.move_account(&store, SecretVaultRef::Vault(name2.to_owned()), &account1, password1, password1).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_insert_account_when_vault_password_incorrect() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_not_insert_account_when_vault_password_incorrect");
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let password2 = "password2";
|
||||
let keypair1 = keypair();
|
||||
|
||||
// when
|
||||
store.create_vault(name1, password1).unwrap();
|
||||
|
||||
// then
|
||||
store.insert_account(SecretVaultRef::Vault(name1.to_owned()), keypair1.secret().clone(), password2).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_remove_account_from_vault() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_remove_account_from_vault");
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let keypair1 = keypair();
|
||||
@@ -760,7 +756,7 @@ mod tests {
|
||||
#[test]
|
||||
fn should_not_remove_account_from_vault_when_password_is_incorrect() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_not_remove_account_from_vault_when_password_is_incorrect");
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let password2 = "password2";
|
||||
@@ -779,7 +775,7 @@ mod tests {
|
||||
#[test]
|
||||
fn should_change_vault_password() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_change_vault_password");
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name = "vault"; let password = "password";
|
||||
let keypair = keypair();
|
||||
@@ -791,9 +787,7 @@ mod tests {
|
||||
// then
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
let new_password = "new_password";
|
||||
store.change_vault_password(name, "bad_password", new_password).unwrap_err();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
store.change_vault_password(name, password, new_password).unwrap();
|
||||
store.change_vault_password(name, new_password).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
|
||||
// and when
|
||||
@@ -803,4 +797,46 @@ mod tests {
|
||||
store.open_vault(name, new_password).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_have_different_passwords_for_vault_secret_and_meta() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name = "vault"; let password = "password";
|
||||
let secret_password = "sec_password";
|
||||
let keypair = keypair();
|
||||
|
||||
// when
|
||||
store.create_vault(name, password).unwrap();
|
||||
let account_ref = store.insert_account(SecretVaultRef::Vault(name.to_owned()), keypair.secret().clone(), secret_password).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
let new_secret_password = "new_sec_password";
|
||||
store.change_password(&account_ref, secret_password, new_secret_password).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_list_opened_vaults() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let name2 = "vault2"; let password2 = "password2";
|
||||
let name3 = "vault3"; let password3 = "password3";
|
||||
|
||||
// when
|
||||
store.create_vault(name1, password1).unwrap();
|
||||
store.create_vault(name2, password2).unwrap();
|
||||
store.create_vault(name3, password3).unwrap();
|
||||
store.close_vault(name2).unwrap();
|
||||
|
||||
// then
|
||||
let opened_vaults = store.list_opened_vaults().unwrap();
|
||||
assert_eq!(opened_vaults.len(), 2);
|
||||
assert!(opened_vaults.iter().any(|v| &*v == name1));
|
||||
assert!(opened_vaults.iter().any(|v| &*v == name3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ pub use self::kdf::{Kdf, KdfSer, Prf, Pbkdf2, Scrypt, KdfSerParams};
|
||||
pub use self::key_file::KeyFile;
|
||||
pub use self::presale::{PresaleWallet, Encseed};
|
||||
pub use self::vault_file::VaultFile;
|
||||
pub use self::vault_key_file::{VaultKeyFile, VaultKeyMeta};
|
||||
pub use self::vault_key_file::{VaultKeyFile, VaultKeyMeta, insert_vault_name_to_json_meta, remove_vault_name_from_json_meta};
|
||||
pub use self::version::Version;
|
||||
|
||||
|
||||
@@ -18,8 +18,13 @@ use std::io::{Read, Write};
|
||||
use serde::{Deserialize, Deserializer, Error};
|
||||
use serde::de::{Visitor, MapVisitor};
|
||||
use serde_json;
|
||||
use serde_json::value::Value;
|
||||
use serde_json::error;
|
||||
use super::{Uuid, Version, Crypto, H160};
|
||||
|
||||
/// Meta key name for vault field
|
||||
const VAULT_NAME_META_KEY: &'static str = "vault";
|
||||
|
||||
/// Key file as stored in vaults
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct VaultKeyFile {
|
||||
@@ -27,9 +32,9 @@ pub struct VaultKeyFile {
|
||||
pub id: Uuid,
|
||||
/// Key version
|
||||
pub version: Version,
|
||||
/// Encrypted secret
|
||||
/// Secret, encrypted with account password
|
||||
pub crypto: Crypto,
|
||||
/// Encrypted serialized `VaultKeyMeta`
|
||||
/// Serialized `VaultKeyMeta`, encrypted with vault password
|
||||
pub metacrypto: Crypto,
|
||||
}
|
||||
|
||||
@@ -44,6 +49,38 @@ pub struct VaultKeyMeta {
|
||||
pub meta: Option<String>,
|
||||
}
|
||||
|
||||
/// Insert vault name to the JSON meta field
|
||||
pub fn insert_vault_name_to_json_meta(meta: &str, vault_name: &str) -> Result<String, error::Error> {
|
||||
let mut meta = if meta.is_empty() {
|
||||
Value::Object(serde_json::Map::new())
|
||||
} else {
|
||||
serde_json::from_str(meta)?
|
||||
};
|
||||
|
||||
if let Some(meta_obj) = meta.as_object_mut() {
|
||||
meta_obj.insert(VAULT_NAME_META_KEY.to_owned(), Value::String(vault_name.to_owned()));
|
||||
serde_json::to_string(meta_obj)
|
||||
} else {
|
||||
Err(error::Error::custom("Meta is expected to be a serialized JSON object"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove vault name from the JSON meta field
|
||||
pub fn remove_vault_name_from_json_meta(meta: &str) -> Result<String, error::Error> {
|
||||
let mut meta = if meta.is_empty() {
|
||||
Value::Object(serde_json::Map::new())
|
||||
} else {
|
||||
serde_json::from_str(meta)?
|
||||
};
|
||||
|
||||
if let Some(meta_obj) = meta.as_object_mut() {
|
||||
meta_obj.remove(VAULT_NAME_META_KEY);
|
||||
serde_json::to_string(meta_obj)
|
||||
} else {
|
||||
Err(error::Error::custom("Meta is expected to be a serialized JSON object"))
|
||||
}
|
||||
}
|
||||
|
||||
enum VaultKeyFileField {
|
||||
Id,
|
||||
Version,
|
||||
@@ -244,7 +281,8 @@ impl VaultKeyMeta {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_json;
|
||||
use json::{VaultKeyFile, Version, Crypto, Cipher, Aes128Ctr, Kdf, Pbkdf2, Prf};
|
||||
use json::{VaultKeyFile, Version, Crypto, Cipher, Aes128Ctr, Kdf, Pbkdf2, Prf,
|
||||
insert_vault_name_to_json_meta, remove_vault_name_from_json_meta};
|
||||
|
||||
#[test]
|
||||
fn to_and_from_json() {
|
||||
@@ -284,4 +322,28 @@ mod test {
|
||||
|
||||
assert_eq!(file, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_name_inserted_to_json_meta() {
|
||||
assert_eq!(insert_vault_name_to_json_meta(r#""#, "MyVault").unwrap(), r#"{"vault":"MyVault"}"#);
|
||||
assert_eq!(insert_vault_name_to_json_meta(r#"{"tags":["kalabala"]}"#, "MyVault").unwrap(), r#"{"tags":["kalabala"],"vault":"MyVault"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_name_not_inserted_to_json_meta() {
|
||||
assert!(insert_vault_name_to_json_meta(r#"///3533"#, "MyVault").is_err());
|
||||
assert!(insert_vault_name_to_json_meta(r#""string""#, "MyVault").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_name_removed_from_json_meta() {
|
||||
assert_eq!(remove_vault_name_from_json_meta(r#"{"vault":"MyVault"}"#).unwrap(), r#"{}"#);
|
||||
assert_eq!(remove_vault_name_from_json_meta(r#"{"tags":["kalabala"],"vault":"MyVault"}"#).unwrap(), r#"{"tags":["kalabala"]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_name_not_removed_from_json_meta() {
|
||||
assert!(remove_vault_name_from_json_meta(r#"///3533"#).is_err());
|
||||
assert!(remove_vault_name_from_json_meta(r#""string""#).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ extern crate rustc_serialize;
|
||||
extern crate crypto as rcrypto;
|
||||
extern crate tiny_keccak;
|
||||
extern crate parking_lot;
|
||||
extern crate ethcore_devtools as devtools;
|
||||
|
||||
// reexport it nicely
|
||||
extern crate ethkey as _ethkey;
|
||||
|
||||
@@ -47,6 +47,9 @@ pub trait SimpleSecretStore: Send + Sync {
|
||||
fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>;
|
||||
|
||||
fn accounts(&self) -> Result<Vec<StoreAccountRef>, Error>;
|
||||
/// Get reference to some account with given address.
|
||||
/// This method could be removed if we will guarantee that there is max(1) account for given address.
|
||||
fn account_ref(&self, address: &Address) -> Result<StoreAccountRef, Error>;
|
||||
|
||||
/// Create new vault with given password
|
||||
fn create_vault(&self, name: &str, password: &str) -> Result<(), Error>;
|
||||
@@ -54,15 +57,20 @@ pub trait SimpleSecretStore: Send + Sync {
|
||||
fn open_vault(&self, name: &str, password: &str) -> Result<(), Error>;
|
||||
/// Close vault
|
||||
fn close_vault(&self, name: &str) -> Result<(), Error>;
|
||||
/// List all vaults
|
||||
fn list_vaults(&self) -> Result<Vec<String>, Error>;
|
||||
/// List all currently opened vaults
|
||||
fn list_opened_vaults(&self) -> Result<Vec<String>, Error>;
|
||||
/// Change vault password
|
||||
fn change_vault_password(&self, name: &str, password: &str, new_password: &str) -> Result<(), Error>;
|
||||
fn change_vault_password(&self, name: &str, new_password: &str) -> Result<(), Error>;
|
||||
/// Cnage account' vault
|
||||
fn change_account_vault(&self, vault: SecretVaultRef, account: StoreAccountRef) -> Result<StoreAccountRef, Error>;
|
||||
}
|
||||
|
||||
pub trait SecretStore: SimpleSecretStore {
|
||||
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error>;
|
||||
fn import_wallet(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error>;
|
||||
fn copy_account(&self, new_store: &SimpleSecretStore, new_vault: SecretVaultRef, account: &StoreAccountRef, password: &str, new_password: &str) -> Result<(), Error>;
|
||||
fn move_account(&self, new_store: &SimpleSecretStore, new_vault: SecretVaultRef, account: &StoreAccountRef, password: &str, new_password: &str) -> Result<(), Error>;
|
||||
fn test_password(&self, account: &StoreAccountRef, password: &str) -> Result<bool, Error>;
|
||||
|
||||
fn public(&self, account: &StoreAccountRef, password: &str) -> Result<Public, Error>;
|
||||
|
||||
Reference in New Issue
Block a user