Initial commit for vaults (#4312)
* initial commit for vaults * fixed TODO * public docs * vault_file.json now contains enc(pwd hash) * removed doc
This commit is contained in:
parent
bf1e7ecfcb
commit
9ac4d83ca3
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -711,6 +711,7 @@ name = "ethstore"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ethcore-util 1.6.0",
|
||||
"ethcrypto 0.1.0",
|
||||
"ethkey 0.2.0",
|
||||
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -724,6 +725,7 @@ dependencies = [
|
||||
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"smallvec 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tiny-keccak 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
79
ethcore/src/account_provider/mod.rs
Normal file → Executable file
79
ethcore/src/account_provider/mod.rs
Normal file → Executable file
@ -24,7 +24,8 @@ use std::fmt;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Instant, Duration};
|
||||
use util::RwLock;
|
||||
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore, random_string};
|
||||
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore,
|
||||
random_string, SecretVaultRef, StoreAccountRef};
|
||||
use ethstore::dir::MemoryDirectory;
|
||||
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
||||
use ethjson::misc::AccountMeta;
|
||||
@ -96,7 +97,7 @@ type AccountToken = String;
|
||||
/// Account management.
|
||||
/// Responsible for unlocking accounts.
|
||||
pub struct AccountProvider {
|
||||
unlocked: RwLock<HashMap<Address, AccountData>>,
|
||||
unlocked: RwLock<HashMap<StoreAccountRef, AccountData>>,
|
||||
address_book: RwLock<AddressBook>,
|
||||
dapps_settings: RwLock<DappsSettingsStore>,
|
||||
/// Accounts on disk
|
||||
@ -138,27 +139,27 @@ impl AccountProvider {
|
||||
let acc = Random.generate().expect("secp context has generation capabilities; qed");
|
||||
let public = acc.public().clone();
|
||||
let secret = acc.secret().clone();
|
||||
let address = self.sstore.insert_account(secret, password)?;
|
||||
Ok((address, public))
|
||||
let account = self.sstore.insert_account(SecretVaultRef::Root, secret, password)?;
|
||||
Ok((account.address, public))
|
||||
}
|
||||
|
||||
/// Inserts new account into underlying store.
|
||||
/// Does not unlock account!
|
||||
pub fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
|
||||
let address = self.sstore.insert_account(secret, password)?;
|
||||
Ok(address)
|
||||
let account = self.sstore.insert_account(SecretVaultRef::Root, secret, password)?;
|
||||
Ok(account.address)
|
||||
}
|
||||
|
||||
/// Import a new presale wallet.
|
||||
pub fn import_presale(&self, presale_json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
let address = self.sstore.import_presale(presale_json, password)?;
|
||||
Ok(Address::from(address).into())
|
||||
let account = self.sstore.import_presale(SecretVaultRef::Root, presale_json, password)?;
|
||||
Ok(Address::from(account.address).into())
|
||||
}
|
||||
|
||||
/// Import a new presale wallet.
|
||||
pub fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
let address = self.sstore.import_wallet(json, password)?;
|
||||
Ok(Address::from(address).into())
|
||||
let account = self.sstore.import_wallet(SecretVaultRef::Root, json, password)?;
|
||||
Ok(Address::from(account.address).into())
|
||||
}
|
||||
|
||||
/// Checks whether an account with a given address is present.
|
||||
@ -169,7 +170,7 @@ impl AccountProvider {
|
||||
/// Returns addresses of all accounts.
|
||||
pub fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
let accounts = self.sstore.accounts()?;
|
||||
Ok(accounts)
|
||||
Ok(accounts.into_iter().map(|a| a.address).collect())
|
||||
}
|
||||
|
||||
/// Sets a whitelist of accounts exposed for unknown dapps.
|
||||
@ -247,13 +248,14 @@ impl AccountProvider {
|
||||
pub fn accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
|
||||
let r: HashMap<Address, AccountMeta> = self.sstore.accounts()?
|
||||
.into_iter()
|
||||
.map(|a| (a.clone(), self.account_meta(a).ok().unwrap_or_default()))
|
||||
.map(|a| (a.address.clone(), self.account_meta(a.address).ok().unwrap_or_default()))
|
||||
.collect();
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn account_meta(&self, account: Address) -> Result<AccountMeta, Error> {
|
||||
pub fn account_meta(&self, address: Address) -> Result<AccountMeta, Error> {
|
||||
let account = StoreAccountRef::root(address);
|
||||
Ok(AccountMeta {
|
||||
name: self.sstore.name(&account)?,
|
||||
meta: self.sstore.meta(&account)?,
|
||||
@ -262,38 +264,39 @@ impl AccountProvider {
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn set_account_name(&self, account: Address, name: String) -> Result<(), Error> {
|
||||
self.sstore.set_name(&account, name)?;
|
||||
pub fn set_account_name(&self, address: Address, name: String) -> Result<(), Error> {
|
||||
self.sstore.set_name(&StoreAccountRef::root(address), name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn set_account_meta(&self, account: Address, meta: String) -> Result<(), Error> {
|
||||
self.sstore.set_meta(&account, meta)?;
|
||||
pub fn set_account_meta(&self, address: Address, meta: String) -> Result<(), Error> {
|
||||
self.sstore.set_meta(&StoreAccountRef::root(address), meta)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the password for `account` is `password`. `false` if not.
|
||||
pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> {
|
||||
self.sstore.test_password(account, password)
|
||||
pub fn test_password(&self, address: &Address, password: &str) -> Result<bool, Error> {
|
||||
self.sstore.test_password(&StoreAccountRef::root(address.clone()), password)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Permanently removes an account.
|
||||
pub fn kill_account(&self, account: &Address, password: &str) -> Result<(), Error> {
|
||||
self.sstore.remove_account(account, &password)?;
|
||||
pub fn kill_account(&self, address: &Address, password: &str) -> Result<(), Error> {
|
||||
self.sstore.remove_account(&StoreAccountRef::root(address.clone()), &password)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Changes the password of `account` from `password` to `new_password`. Fails if incorrect `password` given.
|
||||
pub fn change_password(&self, account: &Address, password: String, new_password: String) -> Result<(), Error> {
|
||||
self.sstore.change_password(account, &password, &new_password).map_err(Error::SStore)
|
||||
pub fn change_password(&self, address: &Address, password: String, new_password: String) -> Result<(), Error> {
|
||||
self.sstore.change_password(&StoreAccountRef::root(address.clone()), &password, &new_password).map_err(Error::SStore)
|
||||
}
|
||||
|
||||
/// Helper method used for unlocking accounts.
|
||||
fn unlock_account(&self, account: Address, password: String, unlock: Unlock) -> Result<(), Error> {
|
||||
fn unlock_account(&self, address: Address, password: String, unlock: Unlock) -> Result<(), Error> {
|
||||
// verify password by signing dump message
|
||||
// result may be discarded
|
||||
let account = StoreAccountRef::root(address);
|
||||
let _ = self.sstore.sign(&account, &password, &Default::default())?;
|
||||
|
||||
// check if account is already unlocked pernamently, if it is, do nothing
|
||||
@ -313,7 +316,7 @@ impl AccountProvider {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn password(&self, account: &Address) -> Result<String, Error> {
|
||||
fn password(&self, account: &StoreAccountRef) -> Result<String, Error> {
|
||||
let mut unlocked = self.unlocked.write();
|
||||
let data = unlocked.get(account).ok_or(Error::NotUnlocked)?.clone();
|
||||
if let Unlock::Temp = data.unlock {
|
||||
@ -344,25 +347,28 @@ impl AccountProvider {
|
||||
}
|
||||
|
||||
/// Checks if given account is unlocked
|
||||
pub fn is_unlocked(&self, account: Address) -> bool {
|
||||
pub fn is_unlocked(&self, address: Address) -> bool {
|
||||
let unlocked = self.unlocked.read();
|
||||
let account = StoreAccountRef::root(address);
|
||||
unlocked.get(&account).is_some()
|
||||
}
|
||||
|
||||
/// Signs the message. If password is not provided the account must be unlocked.
|
||||
pub fn sign(&self, account: Address, password: Option<String>, message: Message) -> Result<Signature, Error> {
|
||||
pub fn sign(&self, address: Address, password: Option<String>, message: Message) -> Result<Signature, Error> {
|
||||
let account = StoreAccountRef::root(address);
|
||||
let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?;
|
||||
Ok(self.sstore.sign(&account, &password, &message)?)
|
||||
}
|
||||
|
||||
/// Signs given message with supplied token. Returns a token to use in next signing within this session.
|
||||
pub fn sign_with_token(&self, account: Address, token: AccountToken, message: Message) -> Result<(Signature, AccountToken), Error> {
|
||||
pub fn sign_with_token(&self, address: Address, token: AccountToken, message: Message) -> Result<(Signature, AccountToken), Error> {
|
||||
let account = StoreAccountRef::root(address);
|
||||
let is_std_password = self.sstore.test_password(&account, &token)?;
|
||||
|
||||
let new_token = random_string(16);
|
||||
let signature = if is_std_password {
|
||||
// Insert to transient store
|
||||
self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token)?;
|
||||
self.sstore.copy_account(&self.transient_sstore, SecretVaultRef::Root, &account, &token, &new_token)?;
|
||||
// sign
|
||||
self.sstore.sign(&account, &token, &message)?
|
||||
} else {
|
||||
@ -376,15 +382,16 @@ impl AccountProvider {
|
||||
}
|
||||
|
||||
/// Decrypts a message with given token. Returns a token to use in next operation for this account.
|
||||
pub fn decrypt_with_token(&self, account: Address, token: AccountToken, shared_mac: &[u8], message: &[u8])
|
||||
pub fn decrypt_with_token(&self, address: Address, token: AccountToken, shared_mac: &[u8], message: &[u8])
|
||||
-> Result<(Vec<u8>, AccountToken), Error>
|
||||
{
|
||||
let account = StoreAccountRef::root(address);
|
||||
let is_std_password = self.sstore.test_password(&account, &token)?;
|
||||
|
||||
let new_token = random_string(16);
|
||||
let message = if is_std_password {
|
||||
// Insert to transient store
|
||||
self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token)?;
|
||||
self.sstore.copy_account(&self.transient_sstore, SecretVaultRef::Root, &account, &token, &new_token)?;
|
||||
// decrypt
|
||||
self.sstore.decrypt(&account, &token, shared_mac, message)?
|
||||
} else {
|
||||
@ -398,7 +405,8 @@ impl AccountProvider {
|
||||
}
|
||||
|
||||
/// Decrypts a message. If password is not provided the account must be unlocked.
|
||||
pub fn decrypt(&self, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
pub fn decrypt(&self, address: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let account = StoreAccountRef::root(address);
|
||||
let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?;
|
||||
Ok(self.sstore.decrypt(&account, &password, shared_mac, message)?)
|
||||
}
|
||||
@ -410,7 +418,9 @@ impl AccountProvider {
|
||||
|
||||
/// Returns the underlying `SecretStore` reference if one exists.
|
||||
pub fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error> {
|
||||
self.sstore.import_geth_accounts(desired, testnet).map_err(Into::into)
|
||||
self.sstore.import_geth_accounts(SecretVaultRef::Root, desired, testnet)
|
||||
.map(|a| a.into_iter().map(|a| a.address).collect())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@ -419,6 +429,7 @@ mod tests {
|
||||
use super::{AccountProvider, Unlock, DappId};
|
||||
use std::time::Instant;
|
||||
use ethstore::ethkey::{Generator, Random};
|
||||
use ethstore::StoreAccountRef;
|
||||
|
||||
#[test]
|
||||
fn unlock_account_temp() {
|
||||
@ -453,7 +464,7 @@ mod tests {
|
||||
assert!(ap.unlock_account_timed(kp.address(), "test1".into(), 60000).is_err());
|
||||
assert!(ap.unlock_account_timed(kp.address(), "test".into(), 60000).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
ap.unlocked.write().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now());
|
||||
ap.unlocked.write().get_mut(&StoreAccountRef::root(kp.address())).unwrap().unlock = Unlock::Timed(Instant::now());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_err());
|
||||
}
|
||||
|
||||
|
2
ethstore/Cargo.toml
Normal file → Executable file
2
ethstore/Cargo.toml
Normal file → Executable file
@ -21,6 +21,8 @@ lazy_static = "0.2"
|
||||
itertools = "0.4"
|
||||
parking_lot = "0.3"
|
||||
ethcrypto = { path = "../ethcrypto" }
|
||||
ethcore-util = { path = "../util" }
|
||||
smallvec = "0.3.1"
|
||||
|
||||
[build-dependencies]
|
||||
serde_codegen = { version = "0.8", optional = true }
|
||||
|
188
ethstore/src/account/crypto.rs
Executable file
188
ethstore/src/account/crypto.rs
Executable file
@ -0,0 +1,188 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::iter::repeat;
|
||||
use ethkey::Secret;
|
||||
use {json, Error, crypto};
|
||||
use crypto::Keccak256;
|
||||
use random::Random;
|
||||
use smallvec::SmallVec;
|
||||
use account::{Cipher, Kdf, Aes128Ctr, Pbkdf2, Prf};
|
||||
|
||||
/// Encrypted data
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Crypto {
|
||||
/// Encryption parameters
|
||||
pub cipher: Cipher,
|
||||
/// Encrypted data buffer
|
||||
pub ciphertext: Vec<u8>,
|
||||
/// Key derivation function parameters
|
||||
pub kdf: Kdf,
|
||||
/// Message authentication code
|
||||
pub mac: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<json::Crypto> for Crypto {
|
||||
fn from(json: json::Crypto) -> Self {
|
||||
Crypto {
|
||||
cipher: json.cipher.into(),
|
||||
ciphertext: json.ciphertext.into(),
|
||||
kdf: json.kdf.into(),
|
||||
mac: json.mac.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<json::Crypto> for Crypto {
|
||||
fn into(self) -> json::Crypto {
|
||||
json::Crypto {
|
||||
cipher: self.cipher.into(),
|
||||
ciphertext: self.ciphertext.into(),
|
||||
kdf: self.kdf.into(),
|
||||
mac: self.mac.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Crypto {
|
||||
pub fn with_secret(secret: &Secret, password: &str, iterations: u32) -> Self {
|
||||
Crypto::with_plain(&*secret, password, iterations)
|
||||
}
|
||||
|
||||
pub fn with_plain(plain: &[u8], password: &str, iterations: u32) -> Self {
|
||||
let salt: [u8; 32] = Random::random();
|
||||
let iv: [u8; 16] = Random::random();
|
||||
|
||||
// two parts of derived key
|
||||
// DK = [ DK[0..15] DK[16..31] ] = [derived_left_bits, derived_right_bits]
|
||||
let (derived_left_bits, derived_right_bits) = crypto::derive_key_iterations(password, &salt, iterations);
|
||||
|
||||
// preallocated (on-stack in case of `Secret`) buffer to hold cipher
|
||||
// length = length(plain) as we are using CTR-approach
|
||||
let plain_len = plain.len();
|
||||
let mut ciphertext: SmallVec<[u8; 32]> = SmallVec::new();
|
||||
ciphertext.grow(plain_len);
|
||||
ciphertext.extend(repeat(0).take(plain_len));
|
||||
|
||||
// aes-128-ctr with initial vector of iv
|
||||
crypto::aes::encrypt(&derived_left_bits, &iv, plain, &mut *ciphertext);
|
||||
|
||||
// KECCAK(DK[16..31] ++ <ciphertext>), where DK[16..31] - derived_right_bits
|
||||
let mac = crypto::derive_mac(&derived_right_bits, &*ciphertext).keccak256();
|
||||
|
||||
Crypto {
|
||||
cipher: Cipher::Aes128Ctr(Aes128Ctr {
|
||||
iv: iv,
|
||||
}),
|
||||
ciphertext: (*ciphertext).to_vec(),
|
||||
kdf: Kdf::Pbkdf2(Pbkdf2 {
|
||||
dklen: crypto::KEY_LENGTH as u32,
|
||||
salt: salt,
|
||||
c: iterations,
|
||||
prf: Prf::HmacSha256,
|
||||
}),
|
||||
mac: mac,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secret(&self, password: &str) -> Result<Secret, Error> {
|
||||
if self.ciphertext.len() > 32 {
|
||||
return Err(Error::InvalidSecret);
|
||||
}
|
||||
|
||||
let secret = self.do_decrypt(password, 32)?;
|
||||
Ok(Secret::from_slice(&secret)?)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, password: &str) -> Result<Vec<u8>, Error> {
|
||||
let expected_len = self.ciphertext.len();
|
||||
self.do_decrypt(password, expected_len)
|
||||
}
|
||||
|
||||
fn do_decrypt(&self, password: &str, expected_len: usize) -> Result<Vec<u8>, Error> {
|
||||
let (derived_left_bits, derived_right_bits) = match self.kdf {
|
||||
Kdf::Pbkdf2(ref params) => crypto::derive_key_iterations(password, ¶ms.salt, params.c),
|
||||
Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r)?,
|
||||
};
|
||||
|
||||
let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256();
|
||||
|
||||
if mac != self.mac {
|
||||
return Err(Error::InvalidPassword);
|
||||
}
|
||||
|
||||
let mut plain: SmallVec<[u8; 32]> = SmallVec::new();
|
||||
plain.grow(expected_len);
|
||||
plain.extend(repeat(0).take(expected_len));
|
||||
|
||||
match self.cipher {
|
||||
Cipher::Aes128Ctr(ref params) => {
|
||||
// checker by callers
|
||||
debug_assert!(expected_len >= self.ciphertext.len());
|
||||
|
||||
let from = expected_len - self.ciphertext.len();
|
||||
crypto::aes::decrypt(&derived_left_bits, ¶ms.iv, &self.ciphertext, &mut plain[from..]);
|
||||
Ok(plain.into_iter().collect())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ethkey::{Generator, Random};
|
||||
use super::Crypto;
|
||||
|
||||
#[test]
|
||||
fn crypto_with_secret_create() {
|
||||
let keypair = Random.generate().unwrap();
|
||||
let crypto = Crypto::with_secret(keypair.secret(), "this is sparta", 10240);
|
||||
let secret = crypto.secret("this is sparta").unwrap();
|
||||
assert_eq!(keypair.secret(), &secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn crypto_with_secret_invalid_password() {
|
||||
let keypair = Random.generate().unwrap();
|
||||
let crypto = Crypto::with_secret(keypair.secret(), "this is sparta", 10240);
|
||||
let _ = crypto.secret("this is sparta!").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crypto_with_null_plain_data() {
|
||||
let original_data = b"";
|
||||
let crypto = Crypto::with_plain(&original_data[..], "this is sparta", 10240);
|
||||
let decrypted_data = crypto.decrypt("this is sparta").unwrap();
|
||||
assert_eq!(original_data[..], *decrypted_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crypto_with_tiny_plain_data() {
|
||||
let original_data = b"{}";
|
||||
let crypto = Crypto::with_plain(&original_data[..], "this is sparta", 10240);
|
||||
let decrypted_data = crypto.decrypt("this is sparta").unwrap();
|
||||
assert_eq!(original_data[..], *decrypted_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crypto_with_huge_plain_data() {
|
||||
let original_data: Vec<_> = (1..65536).map(|i| (i % 256) as u8).collect();
|
||||
let crypto = Crypto::with_plain(&original_data, "this is sparta", 10240);
|
||||
let decrypted_data = crypto.decrypt("this is sparta").unwrap();
|
||||
assert_eq!(&original_data, &decrypted_data);
|
||||
}
|
||||
}
|
4
ethstore/src/account/mod.rs
Normal file → Executable file
4
ethstore/src/account/mod.rs
Normal file → Executable file
@ -15,11 +15,13 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
mod cipher;
|
||||
mod crypto;
|
||||
mod kdf;
|
||||
mod safe_account;
|
||||
mod version;
|
||||
|
||||
pub use self::cipher::{Cipher, Aes128Ctr};
|
||||
pub use self::crypto::Crypto;
|
||||
pub use self::kdf::{Kdf, Pbkdf2, Scrypt, Prf};
|
||||
pub use self::safe_account::{SafeAccount, Crypto};
|
||||
pub use self::safe_account::SafeAccount;
|
||||
pub use self::version::Version;
|
||||
|
154
ethstore/src/account/safe_account.rs
Normal file → Executable file
154
ethstore/src/account/safe_account.rs
Normal file → Executable file
@ -14,19 +14,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use ethkey::{KeyPair, sign, Address, Secret, Signature, Message, Public};
|
||||
use ethkey::{KeyPair, sign, Address, Signature, Message, Public};
|
||||
use {json, Error, crypto};
|
||||
use crypto::Keccak256;
|
||||
use random::Random;
|
||||
use account::{Version, Cipher, Kdf, Aes128Ctr, Pbkdf2, Prf};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Crypto {
|
||||
pub cipher: Cipher,
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub kdf: Kdf,
|
||||
pub mac: [u8; 32],
|
||||
}
|
||||
use account::Version;
|
||||
use super::crypto::Crypto;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct SafeAccount {
|
||||
@ -39,28 +30,6 @@ pub struct SafeAccount {
|
||||
pub meta: String,
|
||||
}
|
||||
|
||||
impl From<json::Crypto> for Crypto {
|
||||
fn from(json: json::Crypto) -> Self {
|
||||
Crypto {
|
||||
cipher: json.cipher.into(),
|
||||
ciphertext: json.ciphertext.into(),
|
||||
kdf: json.kdf.into(),
|
||||
mac: json.mac.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<json::Crypto> for Crypto {
|
||||
fn into(self) -> json::Crypto {
|
||||
json::Crypto {
|
||||
cipher: self.cipher.into(),
|
||||
ciphertext: self.ciphertext.into(),
|
||||
kdf: self.kdf.into(),
|
||||
mac: self.mac.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<json::KeyFile> for SafeAccount {
|
||||
fn into(self) -> json::KeyFile {
|
||||
json::KeyFile {
|
||||
@ -74,65 +43,6 @@ impl Into<json::KeyFile> for SafeAccount {
|
||||
}
|
||||
}
|
||||
|
||||
impl Crypto {
|
||||
pub fn create(secret: &Secret, password: &str, iterations: u32) -> Self {
|
||||
let salt: [u8; 32] = Random::random();
|
||||
let iv: [u8; 16] = Random::random();
|
||||
|
||||
// two parts of derived key
|
||||
// DK = [ DK[0..15] DK[16..31] ] = [derived_left_bits, derived_right_bits]
|
||||
let (derived_left_bits, derived_right_bits) = crypto::derive_key_iterations(password, &salt, iterations);
|
||||
|
||||
let mut ciphertext = [0u8; 32];
|
||||
|
||||
// aes-128-ctr with initial vector of iv
|
||||
crypto::aes::encrypt(&derived_left_bits, &iv, &**secret, &mut ciphertext);
|
||||
|
||||
// KECCAK(DK[16..31] ++ <ciphertext>), where DK[16..31] - derived_right_bits
|
||||
let mac = crypto::derive_mac(&derived_right_bits, &ciphertext).keccak256();
|
||||
|
||||
Crypto {
|
||||
cipher: Cipher::Aes128Ctr(Aes128Ctr {
|
||||
iv: iv,
|
||||
}),
|
||||
ciphertext: ciphertext.to_vec(),
|
||||
kdf: Kdf::Pbkdf2(Pbkdf2 {
|
||||
dklen: crypto::KEY_LENGTH as u32,
|
||||
salt: salt,
|
||||
c: iterations,
|
||||
prf: Prf::HmacSha256,
|
||||
}),
|
||||
mac: mac,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secret(&self, password: &str) -> Result<Secret, Error> {
|
||||
if self.ciphertext.len() > 32 {
|
||||
return Err(Error::InvalidSecret);
|
||||
}
|
||||
|
||||
let (derived_left_bits, derived_right_bits) = match self.kdf {
|
||||
Kdf::Pbkdf2(ref params) => crypto::derive_key_iterations(password, ¶ms.salt, params.c),
|
||||
Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r)?,
|
||||
};
|
||||
|
||||
let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256();
|
||||
|
||||
if mac != self.mac {
|
||||
return Err(Error::InvalidPassword);
|
||||
}
|
||||
|
||||
match self.cipher {
|
||||
Cipher::Aes128Ctr(ref params) => {
|
||||
let from = 32 - self.ciphertext.len();
|
||||
let mut secret = [0; 32];
|
||||
crypto::aes::decrypt(&derived_left_bits, ¶ms.iv, &self.ciphertext, &mut secret[from..]);
|
||||
Ok(Secret::from_slice(&secret)?)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SafeAccount {
|
||||
pub fn create(
|
||||
keypair: &KeyPair,
|
||||
@ -145,7 +55,7 @@ impl SafeAccount {
|
||||
SafeAccount {
|
||||
id: id,
|
||||
version: Version::V3,
|
||||
crypto: Crypto::create(keypair.secret(), password, iterations),
|
||||
crypto: Crypto::with_secret(keypair.secret(), password, iterations),
|
||||
address: keypair.address(),
|
||||
filename: None,
|
||||
name: name,
|
||||
@ -168,6 +78,42 @@ impl SafeAccount {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `SafeAccount` from the given vault `json`; if it was read from a
|
||||
/// file, the `filename` should be `Some` name. If it is as yet anonymous, then it
|
||||
/// can be left `None`.
|
||||
pub fn from_vault_file(password: &str, json: json::VaultKeyFile, filename: Option<String>) -> Result<Self, Error> {
|
||||
let meta_crypto: Crypto = json.metacrypto.into();
|
||||
let meta_plain = meta_crypto.decrypt(password)?;
|
||||
let meta_plain = json::VaultKeyMeta::load(&meta_plain).map_err(|e| Error::Custom(format!("{:?}", e)))?;
|
||||
|
||||
Ok(SafeAccount::from_file(json::KeyFile {
|
||||
id: json.id,
|
||||
version: json.version,
|
||||
crypto: json.crypto,
|
||||
address: meta_plain.address,
|
||||
name: meta_plain.name,
|
||||
meta: meta_plain.meta,
|
||||
}, filename))
|
||||
}
|
||||
|
||||
/// Create a new `VaultKeyFile` from the given `self`
|
||||
pub fn into_vault_file(self, iterations: u32, password: &str) -> Result<json::VaultKeyFile, Error> {
|
||||
let meta_plain = json::VaultKeyMeta {
|
||||
address: self.address.into(),
|
||||
name: Some(self.name),
|
||||
meta: Some(self.meta),
|
||||
};
|
||||
let meta_plain = meta_plain.write().map_err(|e| Error::Custom(format!("{:?}", e)))?;
|
||||
let meta_crypto = Crypto::with_plain(&meta_plain, password, iterations);
|
||||
|
||||
Ok(json::VaultKeyFile {
|
||||
id: self.id.into(),
|
||||
version: self.version.into(),
|
||||
crypto: self.crypto.into(),
|
||||
metacrypto: meta_crypto.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sign(&self, password: &str, message: &Message) -> Result<Signature, Error> {
|
||||
let secret = self.crypto.secret(password)?;
|
||||
sign(&secret, message).map_err(From::from)
|
||||
@ -188,7 +134,7 @@ impl SafeAccount {
|
||||
let result = SafeAccount {
|
||||
id: self.id.clone(),
|
||||
version: self.version.clone(),
|
||||
crypto: Crypto::create(&secret, new_password, iterations),
|
||||
crypto: Crypto::with_secret(&secret, new_password, iterations),
|
||||
address: self.address.clone(),
|
||||
filename: self.filename.clone(),
|
||||
name: self.name.clone(),
|
||||
@ -205,23 +151,7 @@ impl SafeAccount {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ethkey::{Generator, Random, verify_public, Message};
|
||||
use super::{Crypto, SafeAccount};
|
||||
|
||||
#[test]
|
||||
fn crypto_create() {
|
||||
let keypair = Random.generate().unwrap();
|
||||
let crypto = Crypto::create(keypair.secret(), "this is sparta", 10240);
|
||||
let secret = crypto.secret("this is sparta").unwrap();
|
||||
assert_eq!(keypair.secret(), &secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn crypto_invalid_password() {
|
||||
let keypair = Random.generate().unwrap();
|
||||
let crypto = Crypto::create(keypair.secret(), "this is sparta", 10240);
|
||||
let _ = crypto.secret("this is sparta!").unwrap();
|
||||
}
|
||||
use super::SafeAccount;
|
||||
|
||||
#[test]
|
||||
fn sign_and_verify_public() {
|
||||
|
186
ethstore/src/dir/disk.rs
Normal file → Executable file
186
ethstore/src/dir/disk.rs
Normal file → Executable file
@ -20,7 +20,8 @@ use std::collections::HashMap;
|
||||
use time;
|
||||
use {json, SafeAccount, Error};
|
||||
use json::Uuid;
|
||||
use super::KeyDirectory;
|
||||
use super::{KeyDirectory, VaultKeyDirectory, VaultKeyDirectoryProvider, VaultKey};
|
||||
use super::vault::VaultDiskDirectory;
|
||||
|
||||
const IGNORED_FILES: &'static [&'static str] = &[
|
||||
"thumbs.db",
|
||||
@ -28,6 +29,7 @@ const IGNORED_FILES: &'static [&'static str] = &[
|
||||
"dapps_policy.json",
|
||||
"dapps_accounts.json",
|
||||
"dapps_history.json",
|
||||
"vault.json",
|
||||
];
|
||||
|
||||
#[cfg(not(windows))]
|
||||
@ -48,19 +50,43 @@ fn restrict_permissions_to_owner(_file_path: &Path) -> Result<(), i32> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct DiskDirectory {
|
||||
path: PathBuf,
|
||||
/// Root keys directory implementation
|
||||
pub type RootDiskDirectory = DiskDirectory<DiskKeyFileManager>;
|
||||
|
||||
/// Disk directory key file manager
|
||||
pub trait KeyFileManager: Send + Sync {
|
||||
/// Read `SafeAccount` from given key file stream
|
||||
fn read<T>(&self, filename: Option<String>, reader: T) -> Result<SafeAccount, Error> where T: io::Read;
|
||||
/// Write `SafeAccount` to given key file stream
|
||||
fn write<T>(&self, account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write;
|
||||
}
|
||||
|
||||
impl DiskDirectory {
|
||||
/// Disk-based keys directory implementation
|
||||
pub struct DiskDirectory<T> where T: KeyFileManager {
|
||||
path: PathBuf,
|
||||
key_manager: T,
|
||||
}
|
||||
|
||||
/// Keys file manager for root keys directory
|
||||
pub struct DiskKeyFileManager;
|
||||
|
||||
impl RootDiskDirectory {
|
||||
pub fn create<P>(path: P) -> Result<Self, Error> where P: AsRef<Path> {
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(Self::at(path))
|
||||
}
|
||||
|
||||
pub fn at<P>(path: P) -> Self where P: AsRef<Path> {
|
||||
DiskDirectory::new(path, DiskKeyFileManager)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DiskDirectory<T> where T: KeyFileManager {
|
||||
/// Create new disk directory instance
|
||||
pub fn new<P>(path: P, key_manager: T) -> Self where P: AsRef<Path> {
|
||||
DiskDirectory {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
key_manager: key_manager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,28 +111,60 @@ impl DiskDirectory {
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
Ok(paths
|
||||
.iter()
|
||||
.map(|p| (
|
||||
fs::File::open(p)
|
||||
.map_err(Error::from)
|
||||
.and_then(|r| json::KeyFile::load(r).map_err(|e| Error::Custom(format!("{:?}", e)))),
|
||||
p
|
||||
))
|
||||
.filter_map(|(file, path)| match file {
|
||||
Ok(file) => Some((path.clone(), SafeAccount::from_file(
|
||||
file, Some(path.file_name().and_then(|n| n.to_str()).expect("Keys have valid UTF8 names only.").to_owned())
|
||||
))),
|
||||
Err(err) => {
|
||||
.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);
|
||||
None
|
||||
},
|
||||
err
|
||||
})
|
||||
.map(|account| (path, account))
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
/// insert account with given file name
|
||||
pub fn insert_with_filename(&self, account: SafeAccount, filename: String) -> Result<SafeAccount, Error> {
|
||||
// update account filename
|
||||
let original_account = account.clone();
|
||||
let mut account = account;
|
||||
account.filename = Some(filename.clone());
|
||||
|
||||
{
|
||||
// Path to keyfile
|
||||
let mut keyfile_path = self.path.clone();
|
||||
keyfile_path.push(filename.as_str());
|
||||
|
||||
// save the file
|
||||
let mut file = fs::File::create(&keyfile_path)?;
|
||||
if let Err(err) = self.key_manager.write(original_account, &mut file).map_err(|e| Error::Custom(format!("{:?}", e))) {
|
||||
drop(file);
|
||||
fs::remove_file(keyfile_path).expect("Expected to remove recently created file");
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(_) = restrict_permissions_to_owner(keyfile_path.as_path()) {
|
||||
drop(file);
|
||||
fs::remove_file(keyfile_path).expect("Expected to remove recently created file");
|
||||
return Err(Error::Io(io::Error::last_os_error()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
/// Get key file manager
|
||||
pub fn key_manager(&self) -> &T {
|
||||
&self.key_manager
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyDirectory for DiskDirectory {
|
||||
impl<T> KeyDirectory for DiskDirectory<T> where T: KeyFileManager {
|
||||
fn load(&self) -> Result<Vec<SafeAccount>, Error> {
|
||||
let accounts = self.files()?
|
||||
.into_iter()
|
||||
@ -121,35 +179,13 @@ impl KeyDirectory for DiskDirectory {
|
||||
}
|
||||
|
||||
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
// transform account into key file
|
||||
let keyfile: json::KeyFile = account.clone().into();
|
||||
|
||||
// build file path
|
||||
let filename = account.filename.as_ref().cloned().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))
|
||||
});
|
||||
|
||||
// update account filename
|
||||
let mut account = account;
|
||||
account.filename = Some(filename.clone());
|
||||
|
||||
{
|
||||
// Path to keyfile
|
||||
let mut keyfile_path = self.path.clone();
|
||||
keyfile_path.push(filename.as_str());
|
||||
|
||||
// save the file
|
||||
let mut file = fs::File::create(&keyfile_path)?;
|
||||
keyfile.write(&mut file).map_err(|e| Error::Custom(format!("{:?}", e)))?;
|
||||
|
||||
if let Err(_) = restrict_permissions_to_owner(keyfile_path.as_path()) {
|
||||
fs::remove_file(keyfile_path).expect("Expected to remove recently created file");
|
||||
return Err(Error::Io(io::Error::last_os_error()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(account)
|
||||
self.insert_with_filename(account, filename)
|
||||
}
|
||||
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
@ -167,14 +203,41 @@ impl KeyDirectory for DiskDirectory {
|
||||
}
|
||||
|
||||
fn path(&self) -> Option<&PathBuf> { Some(&self.path) }
|
||||
|
||||
fn as_vault_provider(&self) -> Option<&VaultKeyDirectoryProvider> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> VaultKeyDirectoryProvider for DiskDirectory<T> where T: KeyFileManager {
|
||||
fn create(&self, name: &str, key: VaultKey) -> Result<Box<VaultKeyDirectory>, Error> {
|
||||
let vault_dir = VaultDiskDirectory::create(&self.path, name, key)?;
|
||||
Ok(Box::new(vault_dir))
|
||||
}
|
||||
|
||||
fn open(&self, name: &str, key: VaultKey) -> Result<Box<VaultKeyDirectory>, Error> {
|
||||
let vault_dir = VaultDiskDirectory::at(&self.path, name, key)?;
|
||||
Ok(Box::new(vault_dir))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyFileManager for DiskKeyFileManager {
|
||||
fn read<T>(&self, filename: Option<String>, reader: T) -> Result<SafeAccount, Error> where T: io::Read {
|
||||
let key_file = json::KeyFile::load(reader).map_err(|e| Error::Custom(format!("{:?}", e)))?;
|
||||
Ok(SafeAccount::from_file(key_file, filename))
|
||||
}
|
||||
|
||||
fn write<T>(&self, account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write {
|
||||
let key_file: json::KeyFile = account.into();
|
||||
key_file.write(writer).map_err(|e| Error::Custom(format!("{:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{env, fs};
|
||||
use super::DiskDirectory;
|
||||
use dir::KeyDirectory;
|
||||
use super::RootDiskDirectory;
|
||||
use dir::{KeyDirectory, VaultKey};
|
||||
use account::SafeAccount;
|
||||
use ethkey::{Random, Generator};
|
||||
|
||||
@ -185,7 +248,7 @@ mod test {
|
||||
dir.push("ethstore_should_create_new_account");
|
||||
let keypair = Random.generate().unwrap();
|
||||
let password = "hello world";
|
||||
let directory = DiskDirectory::create(dir.clone()).unwrap();
|
||||
let directory = RootDiskDirectory::create(dir.clone()).unwrap();
|
||||
|
||||
// when
|
||||
let account = SafeAccount::create(&keypair, [0u8; 16], password, 1024, "Test".to_owned(), "{}".to_owned());
|
||||
@ -199,4 +262,37 @@ mod test {
|
||||
// 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";
|
||||
|
||||
// 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, 1024));
|
||||
|
||||
// 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, 1024));
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
8
ethstore/src/dir/geth.rs
Normal file → Executable file
8
ethstore/src/dir/geth.rs
Normal file → Executable file
@ -17,7 +17,7 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use {SafeAccount, Error};
|
||||
use super::{KeyDirectory, DiskDirectory, DirectoryType};
|
||||
use super::{KeyDirectory, RootDiskDirectory, DirectoryType};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn geth_dir_path() -> PathBuf {
|
||||
@ -60,13 +60,13 @@ fn geth_keystore(t: DirectoryType) -> PathBuf {
|
||||
}
|
||||
|
||||
pub struct GethDirectory {
|
||||
dir: DiskDirectory,
|
||||
dir: RootDiskDirectory,
|
||||
}
|
||||
|
||||
impl GethDirectory {
|
||||
pub fn create(t: DirectoryType) -> Result<Self, Error> {
|
||||
let result = GethDirectory {
|
||||
dir: DiskDirectory::create(geth_keystore(t))?,
|
||||
dir: RootDiskDirectory::create(geth_keystore(t))?,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
@ -74,7 +74,7 @@ impl GethDirectory {
|
||||
|
||||
pub fn open(t: DirectoryType) -> Self {
|
||||
GethDirectory {
|
||||
dir: DiskDirectory::at(geth_keystore(t)),
|
||||
dir: RootDiskDirectory::at(geth_keystore(t)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
ethstore/src/dir/mod.rs
Normal file → Executable file
72
ethstore/src/dir/mod.rs
Normal file → Executable file
@ -21,21 +21,79 @@ mod disk;
|
||||
mod geth;
|
||||
mod memory;
|
||||
mod parity;
|
||||
mod vault;
|
||||
|
||||
pub enum DirectoryType {
|
||||
Testnet,
|
||||
Main,
|
||||
}
|
||||
|
||||
pub trait KeyDirectory: Send + Sync {
|
||||
fn load(&self) -> Result<Vec<SafeAccount>, Error>;
|
||||
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error>;
|
||||
fn path(&self) -> Option<&PathBuf> { None }
|
||||
/// `VaultKeyDirectory::set_key` error
|
||||
#[derive(Debug)]
|
||||
pub enum SetKeyError {
|
||||
/// Error is fatal and directory is probably in inconsistent state
|
||||
Fatal(Error),
|
||||
/// Error is non fatal, directory is reverted to pre-operation state
|
||||
NonFatalOld(Error),
|
||||
/// Error is non fatal, directory is consistent with new key
|
||||
NonFatalNew(Error),
|
||||
}
|
||||
|
||||
pub use self::disk::DiskDirectory;
|
||||
/// Vault key
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VaultKey {
|
||||
/// Vault password
|
||||
pub password: String,
|
||||
/// Number of iterations to produce a derived key from password
|
||||
pub iterations: u32,
|
||||
}
|
||||
|
||||
/// Keys directory
|
||||
pub trait KeyDirectory: Send + Sync {
|
||||
/// Read keys from directory
|
||||
fn load(&self) -> Result<Vec<SafeAccount>, Error>;
|
||||
/// Insert new key to directory
|
||||
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
|
||||
//// Update key in directory
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
|
||||
/// Remove key from directory
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error>;
|
||||
/// Get directory filesystem path, if available
|
||||
fn path(&self) -> Option<&PathBuf> { None }
|
||||
/// Return vault provider, if available
|
||||
fn as_vault_provider(&self) -> Option<&VaultKeyDirectoryProvider> { None }
|
||||
}
|
||||
|
||||
/// Vaults provider
|
||||
pub trait VaultKeyDirectoryProvider {
|
||||
/// Create new vault with given key
|
||||
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>;
|
||||
}
|
||||
|
||||
/// Vault directory
|
||||
pub trait VaultKeyDirectory: KeyDirectory {
|
||||
/// Cast to `KeyDirectory`
|
||||
fn as_key_directory(&self) -> &KeyDirectory;
|
||||
/// Vault name
|
||||
fn name(&self) -> &str;
|
||||
/// Set new key for vault
|
||||
fn set_key(&self, old_key: VaultKey, key: VaultKey) -> Result<(), SetKeyError>;
|
||||
}
|
||||
|
||||
pub use self::disk::RootDiskDirectory;
|
||||
pub use self::geth::GethDirectory;
|
||||
pub use self::memory::MemoryDirectory;
|
||||
pub use self::parity::ParityDirectory;
|
||||
pub use self::vault::VaultDiskDirectory;
|
||||
|
||||
impl VaultKey {
|
||||
/// Create new vault key
|
||||
pub fn new(password: &str, iterations: u32) -> Self {
|
||||
VaultKey {
|
||||
password: password.to_owned(),
|
||||
iterations: iterations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
ethstore/src/dir/parity.rs
Normal file → Executable file
8
ethstore/src/dir/parity.rs
Normal file → Executable file
@ -17,7 +17,7 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use {SafeAccount, Error};
|
||||
use super::{KeyDirectory, DiskDirectory, DirectoryType};
|
||||
use super::{KeyDirectory, RootDiskDirectory, DirectoryType};
|
||||
|
||||
fn parity_dir_path() -> PathBuf {
|
||||
let mut home = env::home_dir().expect("Failed to get home dir");
|
||||
@ -39,13 +39,13 @@ fn parity_keystore(t: DirectoryType) -> PathBuf {
|
||||
}
|
||||
|
||||
pub struct ParityDirectory {
|
||||
dir: DiskDirectory,
|
||||
dir: RootDiskDirectory,
|
||||
}
|
||||
|
||||
impl ParityDirectory {
|
||||
pub fn create(t: DirectoryType) -> Result<Self, Error> {
|
||||
let result = ParityDirectory {
|
||||
dir: DiskDirectory::create(parity_keystore(t))?,
|
||||
dir: RootDiskDirectory::create(parity_keystore(t))?,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
@ -53,7 +53,7 @@ impl ParityDirectory {
|
||||
|
||||
pub fn open(t: DirectoryType) -> Self {
|
||||
ParityDirectory {
|
||||
dir: DiskDirectory::at(parity_keystore(t)),
|
||||
dir: RootDiskDirectory::at(parity_keystore(t)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
422
ethstore/src/dir/vault.rs
Executable file
422
ethstore/src/dir/vault.rs
Executable file
@ -0,0 +1,422 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{fs, io};
|
||||
use std::path::{PathBuf, Path};
|
||||
use {json, SafeAccount, Error};
|
||||
use util::sha3::Hashable;
|
||||
use super::super::account::Crypto;
|
||||
use super::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError};
|
||||
use super::disk::{DiskDirectory, KeyFileManager};
|
||||
|
||||
const VAULT_FILE_NAME: &'static str = "vault.json";
|
||||
|
||||
/// Vault directory implementation
|
||||
pub type VaultDiskDirectory = DiskDirectory<VaultKeyFileManager>;
|
||||
|
||||
/// Vault key file manager
|
||||
pub struct VaultKeyFileManager {
|
||||
key: VaultKey,
|
||||
}
|
||||
|
||||
impl VaultDiskDirectory {
|
||||
/// Create new vault directory with given key
|
||||
pub fn create<P>(root: P, name: &str, key: VaultKey) -> Result<Self, Error> where P: AsRef<Path> {
|
||||
// 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
|
||||
fs::create_dir_all(&vault_dir_path)?;
|
||||
if let Err(err) = create_vault_file(&vault_dir_path, &key) {
|
||||
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(key)))
|
||||
}
|
||||
|
||||
/// Open existing vault directory with given key
|
||||
pub fn at<P>(root: P, name: &str, key: VaultKey) -> Result<Self, Error> where P: AsRef<Path> {
|
||||
// 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
|
||||
check_vault_file(&vault_dir_path, &key)?;
|
||||
|
||||
Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(key)))
|
||||
}
|
||||
|
||||
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); // 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, vault_key: &VaultKey) -> Result<(), Error> {
|
||||
let password = &self.key_manager().key.password;
|
||||
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)?;
|
||||
}
|
||||
|
||||
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.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")
|
||||
}
|
||||
|
||||
fn set_key(&self, key: VaultKey, new_key: VaultKey) -> Result<(), SetKeyError> {
|
||||
if self.key_manager().key != key {
|
||||
return Err(SetKeyError::NonFatalOld(Error::InvalidPassword));
|
||||
}
|
||||
|
||||
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, &new_key)
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
||||
impl VaultKeyFileManager {
|
||||
pub fn new(key: VaultKey) -> Self {
|
||||
VaultKeyFileManager {
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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<P>(root: P, name: &str, check_name: bool) -> Result<PathBuf, Error> where P: AsRef<Path> {
|
||||
// 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<P>(vault_dir_path: P, key: &VaultKey) -> Result<(), Error> where P: AsRef<Path> {
|
||||
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 vault_file = fs::File::create(vault_file_path)?;
|
||||
let vault_file_contents = json::VaultFile {
|
||||
crypto: crypto.into(),
|
||||
};
|
||||
vault_file_contents.write(&mut vault_file).map_err(|e| Error::Custom(format!("{:?}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// When vault is opened => we must check that password matches
|
||||
fn check_vault_file<P>(vault_dir_path: P, key: &VaultKey) -> Result<(), 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_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(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{env, fs};
|
||||
use std::io::Write;
|
||||
use dir::VaultKey;
|
||||
use super::{VAULT_FILE_NAME, check_vault_name, make_vault_dir_path, create_vault_file, check_vault_file, VaultDiskDirectory};
|
||||
|
||||
#[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 key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("create_vault_file_succeeds");
|
||||
let mut vault_dir = dir.clone();
|
||||
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());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_vault_file_succeeds() {
|
||||
// given
|
||||
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();
|
||||
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 = check_vault_file(&dir, &key);
|
||||
|
||||
// then
|
||||
assert!(result.is_ok());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_vault_file_fails() {
|
||||
// given
|
||||
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();
|
||||
vault_file_path.push(VAULT_FILE_NAME);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
// when
|
||||
let result = check_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 = check_vault_file(&dir, &key);
|
||||
|
||||
// then
|
||||
assert!(result.is_err());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_directory_can_be_created() {
|
||||
// given
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("vault_directory_can_be_created");
|
||||
|
||||
// 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());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_directory_cannot_be_created_if_already_exists() {
|
||||
// given
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("vault_directory_cannot_be_created_if_already_exists");
|
||||
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());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_directory_cannot_be_opened_if_not_exists() {
|
||||
// given
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("vault_directory_cannot_be_opened_if_not_exists");
|
||||
|
||||
// when
|
||||
let vault = VaultDiskDirectory::at(&dir, "vault", key);
|
||||
|
||||
// then
|
||||
assert!(vault.is_err());
|
||||
|
||||
// cleanup
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
10
ethstore/src/error.rs
Normal file → Executable file
10
ethstore/src/error.rs
Normal file → Executable file
@ -24,9 +24,14 @@ pub enum Error {
|
||||
Io(IoError),
|
||||
InvalidPassword,
|
||||
InvalidSecret,
|
||||
InvalidCryptoMeta,
|
||||
InvalidAccount,
|
||||
InvalidMessage,
|
||||
InvalidKeyFile(String),
|
||||
VaultsAreNotSupported,
|
||||
UnsupportedVault,
|
||||
InvalidVaultName,
|
||||
VaultNotFound,
|
||||
CreationFailed,
|
||||
EthKey(EthKeyError),
|
||||
EthCrypto(EthCryptoError),
|
||||
@ -39,9 +44,14 @@ impl fmt::Display for Error {
|
||||
Error::Io(ref err) => err.to_string(),
|
||||
Error::InvalidPassword => "Invalid password".into(),
|
||||
Error::InvalidSecret => "Invalid secret".into(),
|
||||
Error::InvalidCryptoMeta => "Invalid crypted metadata".into(),
|
||||
Error::InvalidAccount => "Invalid account".into(),
|
||||
Error::InvalidMessage => "Invalid message".into(),
|
||||
Error::InvalidKeyFile(ref reason) => format!("Invalid key file: {}", reason),
|
||||
Error::VaultsAreNotSupported => "Vaults are not supported".into(),
|
||||
Error::UnsupportedVault => "Vault is not supported for this operation".into(),
|
||||
Error::InvalidVaultName => "Invalid vault name".into(),
|
||||
Error::VaultNotFound => "Vault not found".into(),
|
||||
Error::CreationFailed => "Account creation failed".into(),
|
||||
Error::EthKey(ref err) => err.to_string(),
|
||||
Error::EthCrypto(ref err) => err.to_string(),
|
||||
|
557
ethstore/src/ethstore.rs
Normal file → Executable file
557
ethstore/src/ethstore.rs
Normal file → Executable file
@ -14,18 +14,18 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::mem;
|
||||
use parking_lot::RwLock;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
|
||||
use crypto::KEY_ITERATIONS;
|
||||
use random::Random;
|
||||
use ethkey::{Signature, Address, Message, Secret, Public, KeyPair};
|
||||
use dir::KeyDirectory;
|
||||
use dir::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError};
|
||||
use account::SafeAccount;
|
||||
use presale::PresaleWallet;
|
||||
use json::{self, Uuid};
|
||||
use {import, Error, SimpleSecretStore, SecretStore};
|
||||
use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef};
|
||||
|
||||
pub struct EthStore {
|
||||
store: EthMultiStore,
|
||||
@ -42,106 +42,126 @@ impl EthStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn get(&self, address: &Address) -> Result<SafeAccount, Error> {
|
||||
let mut accounts = self.store.get(address)?.into_iter();
|
||||
fn get(&self, account: &StoreAccountRef) -> Result<SafeAccount, Error> {
|
||||
let mut accounts = self.store.get(account)?.into_iter();
|
||||
accounts.next().ok_or(Error::InvalidAccount)
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleSecretStore for EthStore {
|
||||
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
|
||||
self.store.insert_account(secret, password)
|
||||
fn insert_account(&self, vault: SecretVaultRef, secret: Secret, password: &str) -> Result<StoreAccountRef, Error> {
|
||||
self.store.insert_account(vault, secret, password)
|
||||
}
|
||||
|
||||
fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
fn accounts(&self) -> Result<Vec<StoreAccountRef>, Error> {
|
||||
self.store.accounts()
|
||||
}
|
||||
|
||||
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> {
|
||||
self.store.change_password(address, old_password, new_password)
|
||||
fn change_password(&self, account: &StoreAccountRef, old_password: &str, new_password: &str) -> Result<(), Error> {
|
||||
self.store.change_password(account, old_password, new_password)
|
||||
}
|
||||
|
||||
fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
|
||||
self.store.remove_account(address, password)
|
||||
fn remove_account(&self, account: &StoreAccountRef, password: &str) -> Result<(), Error> {
|
||||
self.store.remove_account(account, password)
|
||||
}
|
||||
|
||||
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
|
||||
let account = self.get(address)?;
|
||||
fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result<Signature, Error> {
|
||||
let account = self.get(account)?;
|
||||
account.sign(password, message)
|
||||
}
|
||||
|
||||
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let account = self.get(account)?;
|
||||
account.decrypt(password, shared_mac, message)
|
||||
}
|
||||
|
||||
fn create_vault(&self, name: &str, password: &str) -> Result<(), Error> {
|
||||
self.store.create_vault(name, password)
|
||||
}
|
||||
|
||||
fn open_vault(&self, name: &str, password: &str) -> Result<(), Error> {
|
||||
self.store.open_vault(name, password)
|
||||
}
|
||||
|
||||
fn close_vault(&self, name: &str) -> Result<(), Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretStore for EthStore {
|
||||
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error> {
|
||||
let json_wallet = json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))?;
|
||||
let wallet = PresaleWallet::from(json_wallet);
|
||||
let keypair = wallet.decrypt(password).map_err(|_| Error::InvalidPassword)?;
|
||||
self.insert_account(keypair.secret().clone(), password)
|
||||
self.insert_account(vault, keypair.secret().clone(), password)
|
||||
}
|
||||
|
||||
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
fn import_wallet(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error> {
|
||||
let json_keyfile = json::KeyFile::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))?;
|
||||
let mut safe_account = SafeAccount::from_file(json_keyfile, None);
|
||||
let secret = safe_account.crypto.secret(password).map_err(|_| Error::InvalidPassword)?;
|
||||
safe_account.address = KeyPair::from_secret(secret)?.address();
|
||||
let address = safe_account.address.clone();
|
||||
self.store.import(safe_account)?;
|
||||
Ok(address)
|
||||
self.store.import(vault, safe_account)
|
||||
}
|
||||
|
||||
fn test_password(&self, address: &Address, password: &str) -> Result<bool, Error> {
|
||||
let account = self.get(address)?;
|
||||
fn test_password(&self, account: &StoreAccountRef, password: &str) -> Result<bool, Error> {
|
||||
let account = self.get(account)?;
|
||||
Ok(account.check_password(password))
|
||||
}
|
||||
|
||||
fn copy_account(&self, new_store: &SimpleSecretStore, address: &Address, password: &str, new_password: &str) -> Result<(), Error> {
|
||||
let account = self.get(address)?;
|
||||
fn copy_account(&self, new_store: &SimpleSecretStore, new_vault: SecretVaultRef, account: &StoreAccountRef, password: &str, new_password: &str) -> Result<(), Error> {
|
||||
let account = self.get(account)?;
|
||||
let secret = account.crypto.secret(password)?;
|
||||
new_store.insert_account(secret, new_password)?;
|
||||
new_store.insert_account(new_vault, secret, new_password)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn public(&self, account: &Address, password: &str) -> Result<Public, Error> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn uuid(&self, address: &Address) -> Result<Uuid, Error> {
|
||||
let account = self.get(address)?;
|
||||
fn uuid(&self, account: &StoreAccountRef) -> Result<Uuid, Error> {
|
||||
let account = self.get(account)?;
|
||||
Ok(account.id.into())
|
||||
}
|
||||
|
||||
fn name(&self, address: &Address) -> Result<String, Error> {
|
||||
let account = self.get(address)?;
|
||||
fn name(&self, account: &StoreAccountRef) -> Result<String, Error> {
|
||||
let account = self.get(account)?;
|
||||
Ok(account.name.clone())
|
||||
}
|
||||
|
||||
fn meta(&self, address: &Address) -> Result<String, Error> {
|
||||
let account = self.get(address)?;
|
||||
fn meta(&self, account: &StoreAccountRef) -> Result<String, Error> {
|
||||
let account = self.get(account)?;
|
||||
Ok(account.meta.clone())
|
||||
}
|
||||
|
||||
fn set_name(&self, address: &Address, name: String) -> Result<(), Error> {
|
||||
let old = self.get(address)?;
|
||||
let mut account = old.clone();
|
||||
account.name = name;
|
||||
fn set_name(&self, account_ref: &StoreAccountRef, name: String) -> Result<(), Error> {
|
||||
let old = self.get(account_ref)?;
|
||||
let mut safe_account = old.clone();
|
||||
safe_account.name = name;
|
||||
|
||||
// save to file
|
||||
self.store.update(old, account)
|
||||
self.store.update(account_ref, old, safe_account)
|
||||
}
|
||||
|
||||
fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error> {
|
||||
let old = self.get(address)?;
|
||||
let mut account = old.clone();
|
||||
account.meta = meta;
|
||||
fn set_meta(&self, account_ref: &StoreAccountRef, meta: String) -> Result<(), Error> {
|
||||
let old = self.get(account_ref)?;
|
||||
let mut safe_account = old.clone();
|
||||
safe_account.meta = meta;
|
||||
|
||||
// save to file
|
||||
self.store.update(old, account)
|
||||
self.store.update(account_ref, old, safe_account)
|
||||
}
|
||||
|
||||
fn local_path(&self) -> String {
|
||||
@ -152,8 +172,20 @@ impl SecretStore for EthStore {
|
||||
import::read_geth_accounts(testnet)
|
||||
}
|
||||
|
||||
fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error> {
|
||||
import::import_geth_accounts(&*self.store.dir, desired.into_iter().collect(), testnet)
|
||||
fn import_geth_accounts(&self, vault: SecretVaultRef, desired: Vec<Address>, testnet: bool) -> Result<Vec<StoreAccountRef>, Error> {
|
||||
let imported_addresses = match vault {
|
||||
SecretVaultRef::Root => import::import_geth_accounts(&*self.store.dir, desired.into_iter().collect(), testnet),
|
||||
SecretVaultRef::Vault(vault_name) => {
|
||||
if let Some(vault) = self.store.vaults.lock().get(&vault_name) {
|
||||
import::import_geth_accounts(vault.as_key_directory(), desired.into_iter().collect(), testnet)
|
||||
} else {
|
||||
Err(Error::VaultNotFound)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
imported_addresses
|
||||
.map(|a| a.into_iter().map(|a| StoreAccountRef::root(a)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +193,9 @@ impl SecretStore for EthStore {
|
||||
pub struct EthMultiStore {
|
||||
dir: Box<KeyDirectory>,
|
||||
iterations: u32,
|
||||
cache: RwLock<BTreeMap<Address, Vec<SafeAccount>>>,
|
||||
// order lock: cache, then vaults
|
||||
cache: RwLock<BTreeMap<StoreAccountRef, Vec<SafeAccount>>>,
|
||||
vaults: Mutex<HashMap<String, Box<VaultKeyDirectory>>>,
|
||||
}
|
||||
|
||||
impl EthMultiStore {
|
||||
@ -173,6 +207,7 @@ impl EthMultiStore {
|
||||
pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> {
|
||||
let store = EthMultiStore {
|
||||
dir: directory,
|
||||
vaults: Mutex::new(HashMap::new()),
|
||||
iterations: iterations,
|
||||
cache: Default::default(),
|
||||
};
|
||||
@ -182,21 +217,33 @@ impl EthMultiStore {
|
||||
|
||||
fn reload_accounts(&self) -> Result<(), Error> {
|
||||
let mut cache = self.cache.write();
|
||||
let accounts = self.dir.load()?;
|
||||
|
||||
let mut new_accounts = BTreeMap::new();
|
||||
for account in accounts {
|
||||
let mut entry = new_accounts.entry(account.address.clone()).or_insert_with(Vec::new);
|
||||
entry.push(account);
|
||||
for account in self.dir.load()? {
|
||||
let account_ref = StoreAccountRef::root(account.address);
|
||||
new_accounts
|
||||
.entry(account_ref)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(account);
|
||||
}
|
||||
for (vault_name, vault) in &*self.vaults.lock() {
|
||||
for account in vault.load()? {
|
||||
let account_ref = StoreAccountRef::vault(vault_name, account.address);
|
||||
new_accounts
|
||||
.entry(account_ref)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(account);
|
||||
}
|
||||
}
|
||||
|
||||
mem::replace(&mut *cache, new_accounts);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get(&self, address: &Address) -> Result<Vec<SafeAccount>, Error> {
|
||||
fn get(&self, account: &StoreAccountRef) -> Result<Vec<SafeAccount>, Error> {
|
||||
{
|
||||
let cache = self.cache.read();
|
||||
if let Some(accounts) = cache.get(address) {
|
||||
if let Some(accounts) = cache.get(account) {
|
||||
if !accounts.is_empty() {
|
||||
return Ok(accounts.clone())
|
||||
}
|
||||
@ -205,32 +252,41 @@ impl EthMultiStore {
|
||||
|
||||
self.reload_accounts()?;
|
||||
let cache = self.cache.read();
|
||||
let accounts = cache.get(address).cloned().ok_or(Error::InvalidAccount)?;
|
||||
let accounts = cache.get(account).ok_or(Error::InvalidAccount)?;
|
||||
if accounts.is_empty() {
|
||||
Err(Error::InvalidAccount)
|
||||
} else {
|
||||
Ok(accounts)
|
||||
Ok(accounts.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn import(&self, account: SafeAccount) -> Result<(), Error> {
|
||||
fn import(&self, vault: SecretVaultRef, account: SafeAccount) -> Result<StoreAccountRef, Error> {
|
||||
// save to file
|
||||
let account = self.dir.insert(account)?;
|
||||
let account = match vault {
|
||||
SecretVaultRef::Root => self.dir.insert(account)?,
|
||||
SecretVaultRef::Vault(ref vault_name) => self.vaults.lock().get_mut(vault_name).ok_or(Error::VaultNotFound)?.insert(account)?,
|
||||
};
|
||||
|
||||
// update cache
|
||||
let account_ref = StoreAccountRef::new(vault, account.address.clone());
|
||||
let mut cache = self.cache.write();
|
||||
cache.entry(account_ref.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(account);
|
||||
|
||||
Ok(account_ref)
|
||||
}
|
||||
|
||||
fn update(&self, account_ref: &StoreAccountRef, old: SafeAccount, new: SafeAccount) -> Result<(), Error> {
|
||||
// save to file
|
||||
let account = match account_ref.vault {
|
||||
SecretVaultRef::Root => self.dir.update(new)?,
|
||||
SecretVaultRef::Vault(ref vault_name) => self.vaults.lock().get_mut(vault_name).ok_or(Error::VaultNotFound)?.update(new)?,
|
||||
};
|
||||
|
||||
// update cache
|
||||
let mut cache = self.cache.write();
|
||||
let mut accounts = cache.entry(account.address.clone()).or_insert_with(Vec::new);
|
||||
accounts.push(account);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&self, old: SafeAccount, new: SafeAccount) -> Result<(), Error> {
|
||||
// save to file
|
||||
let account = self.dir.update(new)?;
|
||||
|
||||
// update cache
|
||||
let mut cache = self.cache.write();
|
||||
let mut accounts = cache.entry(account.address.clone()).or_insert_with(Vec::new);
|
||||
let mut accounts = cache.entry(account_ref.clone()).or_insert_with(Vec::new);
|
||||
// Remove old account
|
||||
accounts.retain(|acc| acc != &old);
|
||||
// And push updated to the end
|
||||
@ -242,22 +298,20 @@ impl EthMultiStore {
|
||||
}
|
||||
|
||||
impl SimpleSecretStore for EthMultiStore {
|
||||
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
|
||||
fn insert_account(&self, vault: SecretVaultRef, secret: Secret, password: &str) -> Result<StoreAccountRef, Error> {
|
||||
let keypair = KeyPair::from_secret(secret).map_err(|_| Error::CreationFailed)?;
|
||||
let id: [u8; 16] = Random::random();
|
||||
let account = SafeAccount::create(&keypair, id, password, self.iterations, "".to_owned(), "{}".to_owned());
|
||||
let address = account.address.clone();
|
||||
self.import(account)?;
|
||||
Ok(address)
|
||||
self.import(vault, account)
|
||||
}
|
||||
|
||||
fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
fn accounts(&self) -> Result<Vec<StoreAccountRef>, Error> {
|
||||
self.reload_accounts()?;
|
||||
Ok(self.cache.read().keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
|
||||
let accounts = self.get(address)?;
|
||||
fn remove_account(&self, account_ref: &StoreAccountRef, password: &str) -> Result<(), Error> {
|
||||
let accounts = self.get(account_ref)?;
|
||||
|
||||
for account in accounts {
|
||||
// Skip if password is invalid
|
||||
@ -266,20 +320,26 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
}
|
||||
|
||||
// Remove from dir
|
||||
self.dir.remove(&account)?;
|
||||
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 = {
|
||||
let mut accounts = cache.get_mut(address).expect("Entry exists, because it was returned by `get`; qed");
|
||||
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(address);
|
||||
cache.remove(account_ref);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
@ -287,18 +347,26 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
Err(Error::InvalidPassword)
|
||||
}
|
||||
|
||||
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> {
|
||||
let accounts = self.get(address)?;
|
||||
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)?;
|
||||
|
||||
for account in accounts {
|
||||
// Change password
|
||||
let new_account = account.change_password(old_password, new_password, self.iterations)?;
|
||||
self.update(account, new_account)?;
|
||||
self.update(account_ref, account, new_account)?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
SecretVaultRef::Vault(ref vault_name) => {
|
||||
self.change_vault_password(vault_name, old_password, new_password)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
|
||||
let accounts = self.get(address)?;
|
||||
fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result<Signature, Error> {
|
||||
let accounts = self.get(account)?;
|
||||
for account in accounts {
|
||||
if account.check_password(password) {
|
||||
return account.sign(password, message);
|
||||
@ -308,7 +376,7 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
Err(Error::InvalidPassword)
|
||||
}
|
||||
|
||||
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let accounts = self.get(account)?;
|
||||
for account in accounts {
|
||||
if account.check_password(password) {
|
||||
@ -317,14 +385,85 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
}
|
||||
Err(Error::InvalidPassword)
|
||||
}
|
||||
|
||||
fn create_vault(&self, name: &str, password: &str) -> Result<(), Error> {
|
||||
let is_vault_created = { // lock border
|
||||
let mut vaults = self.vaults.lock();
|
||||
if !vaults.contains_key(&name.to_owned()) {
|
||||
let vault_provider = self.dir.as_vault_provider().ok_or(Error::VaultsAreNotSupported)?;
|
||||
let vault = vault_provider.create(name, VaultKey::new(password, self.iterations))?;
|
||||
vaults.insert(name.to_owned(), vault);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if is_vault_created {
|
||||
self.reload_accounts()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_vault(&self, name: &str, password: &str) -> Result<(), Error> {
|
||||
let is_vault_opened = { // lock border
|
||||
let mut vaults = self.vaults.lock();
|
||||
if !vaults.contains_key(&name.to_owned()) {
|
||||
let vault_provider = self.dir.as_vault_provider().ok_or(Error::VaultsAreNotSupported)?;
|
||||
let vault = vault_provider.open(name, VaultKey::new(password, self.iterations))?;
|
||||
vaults.insert(name.to_owned(), vault);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if is_vault_opened {
|
||||
self.reload_accounts()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_vault(&self, name: &str) -> Result<(), Error> {
|
||||
let is_vault_removed = self.vaults.lock().remove(&name.to_owned()).is_some();
|
||||
if is_vault_removed {
|
||||
self.reload_accounts()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn change_vault_password(&self, name: &str, password: &str, new_password: &str) -> Result<(), 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)) {
|
||||
Ok(_) => {
|
||||
self.close_vault(name)
|
||||
.and_then(|_| self.open_vault(name, new_password))
|
||||
},
|
||||
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));
|
||||
Err(err)
|
||||
},
|
||||
Err(SetKeyError::NonFatalOld(err)) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use dir::MemoryDirectory;
|
||||
use std::{env, fs};
|
||||
use std::path::PathBuf;
|
||||
use dir::{KeyDirectory, MemoryDirectory, RootDiskDirectory};
|
||||
use ethkey::{Random, Generator, KeyPair};
|
||||
use secret_store::{SimpleSecretStore, SecretStore};
|
||||
use secret_store::{SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef};
|
||||
use super::{EthStore, EthMultiStore};
|
||||
|
||||
fn keypair() -> KeyPair {
|
||||
@ -339,6 +478,32 @@ mod tests {
|
||||
EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory always load successfuly; qed")
|
||||
}
|
||||
|
||||
struct RootDiskDirectoryGuard {
|
||||
pub key_dir: Option<Box<KeyDirectory>>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_insert_account_successfully() {
|
||||
// given
|
||||
@ -346,10 +511,10 @@ mod tests {
|
||||
let keypair = keypair();
|
||||
|
||||
// when
|
||||
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
|
||||
let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(address, keypair.address());
|
||||
assert_eq!(address, StoreAccountRef::root(keypair.address()));
|
||||
assert!(store.get(&address).is_ok(), "Should contain account.");
|
||||
assert_eq!(store.accounts().unwrap().len(), 1, "Should have one account.");
|
||||
}
|
||||
@ -359,7 +524,7 @@ mod tests {
|
||||
// given
|
||||
let store = store();
|
||||
let keypair = keypair();
|
||||
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
|
||||
let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap();
|
||||
assert_eq!(&store.meta(&address).unwrap(), "{}");
|
||||
assert_eq!(&store.name(&address).unwrap(), "");
|
||||
|
||||
@ -378,7 +543,7 @@ mod tests {
|
||||
// given
|
||||
let store = store();
|
||||
let keypair = keypair();
|
||||
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
|
||||
let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap();
|
||||
|
||||
// when
|
||||
store.remove_account(&address, "test").unwrap();
|
||||
@ -392,7 +557,7 @@ mod tests {
|
||||
// given
|
||||
let store = store();
|
||||
let keypair = keypair();
|
||||
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
|
||||
let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap();
|
||||
|
||||
// when
|
||||
let res1 = store.test_password(&address, "x").unwrap();
|
||||
@ -407,8 +572,8 @@ mod tests {
|
||||
// given
|
||||
let store = multi_store();
|
||||
let keypair = keypair();
|
||||
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
|
||||
let address2 = store.insert_account(keypair.secret().clone(), "xyz").unwrap();
|
||||
let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap();
|
||||
let address2 = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "xyz").unwrap();
|
||||
assert_eq!(address, address2);
|
||||
|
||||
// when
|
||||
@ -425,11 +590,11 @@ mod tests {
|
||||
let store = store();
|
||||
let multi_store = multi_store();
|
||||
let keypair = keypair();
|
||||
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
|
||||
let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap();
|
||||
assert_eq!(multi_store.accounts().unwrap().len(), 0);
|
||||
|
||||
// when
|
||||
store.copy_account(&multi_store, &address, "test", "xyz").unwrap();
|
||||
store.copy_account(&multi_store, SecretVaultRef::Root, &address, "test", "xyz").unwrap();
|
||||
|
||||
// then
|
||||
assert!(store.test_password(&address, "test").unwrap(), "First password should work for store.");
|
||||
@ -437,4 +602,204 @@ mod tests {
|
||||
assert_eq!(multi_store.accounts().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_create_and_open_vaults() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_create_and_open_vaults");
|
||||
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();
|
||||
let keypair2 = keypair();
|
||||
let keypair3 = keypair(); let password3 = "password3";
|
||||
|
||||
// when
|
||||
store.create_vault(name1, password1).unwrap();
|
||||
store.create_vault(name2, password2).unwrap();
|
||||
|
||||
// then [can create vaults] ^^^
|
||||
|
||||
// and when
|
||||
store.insert_account(SecretVaultRef::Vault(name1.to_owned()), keypair1.secret().clone(), password1).unwrap();
|
||||
store.insert_account(SecretVaultRef::Vault(name2.to_owned()), keypair2.secret().clone(), password2).unwrap();
|
||||
store.insert_account(SecretVaultRef::Root, keypair3.secret().clone(), password3).unwrap();
|
||||
store.insert_account(SecretVaultRef::Vault("vault3".to_owned()), keypair1.secret().clone(), password3).unwrap_err();
|
||||
let accounts = store.accounts().unwrap();
|
||||
|
||||
// then [can create accounts in vaults]
|
||||
assert_eq!(accounts.len(), 3);
|
||||
assert!(accounts.iter().any(|a| a.vault == SecretVaultRef::Root));
|
||||
assert!(accounts.iter().any(|a| a.vault == SecretVaultRef::Vault(name1.to_owned())));
|
||||
assert!(accounts.iter().any(|a| a.vault == SecretVaultRef::Vault(name2.to_owned())));
|
||||
|
||||
// and when
|
||||
store.close_vault(name1).unwrap();
|
||||
store.close_vault(name2).unwrap();
|
||||
store.close_vault("vault3").unwrap();
|
||||
let accounts = store.accounts().unwrap();
|
||||
|
||||
// then [can close vaults + accounts from vaults disappear]
|
||||
assert_eq!(accounts.len(), 1);
|
||||
assert!(accounts.iter().any(|a| a.vault == SecretVaultRef::Root));
|
||||
|
||||
// and when
|
||||
store.open_vault(name1, password2).unwrap_err();
|
||||
store.open_vault(name2, password1).unwrap_err();
|
||||
store.open_vault(name1, password1).unwrap();
|
||||
store.open_vault(name2, password2).unwrap();
|
||||
let accounts = store.accounts().unwrap();
|
||||
|
||||
// then [can check vaults on open + can reopen vaults + accounts from vaults appear]
|
||||
assert_eq!(accounts.len(), 3);
|
||||
assert!(accounts.iter().any(|a| a.vault == SecretVaultRef::Root));
|
||||
assert!(accounts.iter().any(|a| a.vault == SecretVaultRef::Vault(name1.to_owned())));
|
||||
assert!(accounts.iter().any(|a| a.vault == SecretVaultRef::Vault(name2.to_owned())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_move_vault_acounts() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_move_vault_acounts");
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let name2 = "vault2"; let password2 = "password2";
|
||||
let password3 = "password3";
|
||||
let keypair1 = keypair();
|
||||
let keypair2 = keypair();
|
||||
let keypair3 = 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();
|
||||
let account2 = store.insert_account(SecretVaultRef::Vault(name1.to_owned()), keypair2.secret().clone(), password1).unwrap();
|
||||
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 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())));
|
||||
}
|
||||
|
||||
#[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 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();
|
||||
|
||||
// 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 store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
let keypair1 = keypair();
|
||||
|
||||
// when
|
||||
store.create_vault(name1, password1).unwrap();
|
||||
let account1 = store.insert_account(SecretVaultRef::Vault(name1.to_owned()), keypair1.secret().clone(), password1).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
|
||||
// then
|
||||
store.remove_account(&account1, password1).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[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 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();
|
||||
let account1 = store.insert_account(SecretVaultRef::Vault(name1.to_owned()), keypair1.secret().clone(), password1).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
|
||||
// then
|
||||
store.remove_account(&account1, password2).unwrap_err();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_change_vault_password() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new("should_change_vault_password");
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name = "vault"; let password = "password";
|
||||
let keypair = keypair();
|
||||
|
||||
// when
|
||||
store.create_vault(name, password).unwrap();
|
||||
store.insert_account(SecretVaultRef::Vault(name.to_owned()), keypair.secret().clone(), password).unwrap();
|
||||
|
||||
// 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();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
|
||||
// and when
|
||||
store.close_vault(name).unwrap();
|
||||
|
||||
// then
|
||||
store.open_vault(name, new_password).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
|
4
ethstore/src/json/mod.rs.in
Normal file → Executable file
4
ethstore/src/json/mod.rs.in
Normal file → Executable file
@ -7,6 +7,8 @@ mod id;
|
||||
mod kdf;
|
||||
mod key_file;
|
||||
mod presale;
|
||||
mod vault_file;
|
||||
mod vault_key_file;
|
||||
mod version;
|
||||
|
||||
pub use self::bytes::Bytes;
|
||||
@ -18,5 +20,7 @@ pub use self::id::Uuid;
|
||||
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::version::Version;
|
||||
|
||||
|
136
ethstore/src/json/vault_file.rs
Executable file
136
ethstore/src/json/vault_file.rs
Executable file
@ -0,0 +1,136 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use serde::{Deserialize, Deserializer, Error};
|
||||
use serde::de::{Visitor, MapVisitor};
|
||||
use serde_json;
|
||||
use super::Crypto;
|
||||
|
||||
/// Vault meta file
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct VaultFile {
|
||||
/// Vault password, encrypted with vault password
|
||||
pub crypto: Crypto,
|
||||
}
|
||||
|
||||
enum VaultFileField {
|
||||
Crypto,
|
||||
}
|
||||
|
||||
impl Deserialize for VaultFileField {
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<VaultFileField, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
deserializer.deserialize(VaultFileFieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct VaultFileFieldVisitor;
|
||||
|
||||
impl Visitor for VaultFileFieldVisitor {
|
||||
type Value = VaultFileField;
|
||||
|
||||
fn visit_str<E>(&mut self, value: &str) -> Result<Self::Value, E>
|
||||
where E: Error
|
||||
{
|
||||
match value {
|
||||
"crypto" => Ok(VaultFileField::Crypto),
|
||||
_ => Err(Error::custom(format!("Unknown field: '{}'", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for VaultFile {
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<VaultFile, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
static FIELDS: &'static [&'static str] = &["crypto"];
|
||||
deserializer.deserialize_struct("VaultFile", FIELDS, VaultFileVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct VaultFileVisitor;
|
||||
|
||||
impl Visitor for VaultFileVisitor {
|
||||
type Value = VaultFile;
|
||||
|
||||
fn visit_map<V>(&mut self, mut visitor: V) -> Result<Self::Value, V::Error>
|
||||
where V: MapVisitor
|
||||
{
|
||||
let mut crypto = None;
|
||||
|
||||
loop {
|
||||
match visitor.visit_key()? {
|
||||
Some(VaultFileField::Crypto) => { crypto = Some(visitor.visit_value()?); }
|
||||
None => { break; }
|
||||
}
|
||||
}
|
||||
|
||||
let crypto = match crypto {
|
||||
Some(crypto) => crypto,
|
||||
None => visitor.missing_field("crypto")?,
|
||||
};
|
||||
|
||||
visitor.end()?;
|
||||
|
||||
let result = VaultFile {
|
||||
crypto: crypto,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl VaultFile {
|
||||
pub fn load<R>(reader: R) -> Result<Self, serde_json::Error> where R: Read {
|
||||
serde_json::from_reader(reader)
|
||||
}
|
||||
|
||||
pub fn write<W>(&self, writer: &mut W) -> Result<(), serde_json::Error> where W: Write {
|
||||
serde_json::to_writer(writer, self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_json;
|
||||
use json::{VaultFile, Crypto, Cipher, Aes128Ctr, Kdf, Pbkdf2, Prf};
|
||||
|
||||
#[test]
|
||||
fn to_and_from_json() {
|
||||
let file = VaultFile {
|
||||
crypto: Crypto {
|
||||
cipher: Cipher::Aes128Ctr(Aes128Ctr {
|
||||
iv: "0155e3690be19fbfbecabcd440aa284b".into(),
|
||||
}),
|
||||
ciphertext: "4d6938a1f49b7782".into(),
|
||||
kdf: Kdf::Pbkdf2(Pbkdf2 {
|
||||
c: 1024,
|
||||
dklen: 32,
|
||||
prf: Prf::HmacSha256,
|
||||
salt: "b6a9338a7ccd39288a86dba73bfecd9101b4f3db9c9830e7c76afdbd4f6872e5".into(),
|
||||
}),
|
||||
mac: "16381463ea11c6eb2239a9f339c2e780516d29d234ce30ac5f166f9080b5a262".into(),
|
||||
}
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&file).unwrap();
|
||||
let deserialized = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(file, deserialized);
|
||||
}
|
||||
}
|
287
ethstore/src/json/vault_key_file.rs
Executable file
287
ethstore/src/json/vault_key_file.rs
Executable file
@ -0,0 +1,287 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use serde::{Deserialize, Deserializer, Error};
|
||||
use serde::de::{Visitor, MapVisitor};
|
||||
use serde_json;
|
||||
use super::{Uuid, Version, Crypto, H160};
|
||||
|
||||
/// Key file as stored in vaults
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct VaultKeyFile {
|
||||
/// Key id
|
||||
pub id: Uuid,
|
||||
/// Key version
|
||||
pub version: Version,
|
||||
/// Encrypted secret
|
||||
pub crypto: Crypto,
|
||||
/// Encrypted serialized `VaultKeyMeta`
|
||||
pub metacrypto: Crypto,
|
||||
}
|
||||
|
||||
/// Data, stored in `VaultKeyFile::metacrypto`
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct VaultKeyMeta {
|
||||
/// Key address
|
||||
pub address: H160,
|
||||
/// Key name
|
||||
pub name: Option<String>,
|
||||
/// Key metadata
|
||||
pub meta: Option<String>,
|
||||
}
|
||||
|
||||
enum VaultKeyFileField {
|
||||
Id,
|
||||
Version,
|
||||
Crypto,
|
||||
MetaCrypto,
|
||||
}
|
||||
|
||||
enum VaultKeyMetaField {
|
||||
Address,
|
||||
Name,
|
||||
Meta,
|
||||
}
|
||||
|
||||
impl Deserialize for VaultKeyFileField {
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<VaultKeyFileField, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
deserializer.deserialize(VaultKeyFileFieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct VaultKeyFileFieldVisitor;
|
||||
|
||||
impl Visitor for VaultKeyFileFieldVisitor {
|
||||
type Value = VaultKeyFileField;
|
||||
|
||||
fn visit_str<E>(&mut self, value: &str) -> Result<Self::Value, E>
|
||||
where E: Error
|
||||
{
|
||||
match value {
|
||||
"id" => Ok(VaultKeyFileField::Id),
|
||||
"version" => Ok(VaultKeyFileField::Version),
|
||||
"crypto" => Ok(VaultKeyFileField::Crypto),
|
||||
"metacrypto" => Ok(VaultKeyFileField::MetaCrypto),
|
||||
_ => Err(Error::custom(format!("Unknown field: '{}'", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for VaultKeyFile {
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<VaultKeyFile, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
static FIELDS: &'static [&'static str] = &["id", "version", "crypto", "metacrypto"];
|
||||
deserializer.deserialize_struct("VaultKeyFile", FIELDS, VaultKeyFileVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct VaultKeyFileVisitor;
|
||||
|
||||
impl Visitor for VaultKeyFileVisitor {
|
||||
type Value = VaultKeyFile;
|
||||
|
||||
fn visit_map<V>(&mut self, mut visitor: V) -> Result<Self::Value, V::Error>
|
||||
where V: MapVisitor
|
||||
{
|
||||
let mut id = None;
|
||||
let mut version = None;
|
||||
let mut crypto = None;
|
||||
let mut metacrypto = None;
|
||||
|
||||
loop {
|
||||
match visitor.visit_key()? {
|
||||
Some(VaultKeyFileField::Id) => { id = Some(visitor.visit_value()?); }
|
||||
Some(VaultKeyFileField::Version) => { version = Some(visitor.visit_value()?); }
|
||||
Some(VaultKeyFileField::Crypto) => { crypto = Some(visitor.visit_value()?); }
|
||||
Some(VaultKeyFileField::MetaCrypto) => { metacrypto = Some(visitor.visit_value()?); }
|
||||
None => { break; }
|
||||
}
|
||||
}
|
||||
|
||||
let id = match id {
|
||||
Some(id) => id,
|
||||
None => visitor.missing_field("id")?,
|
||||
};
|
||||
|
||||
let version = match version {
|
||||
Some(version) => version,
|
||||
None => visitor.missing_field("version")?,
|
||||
};
|
||||
|
||||
let crypto = match crypto {
|
||||
Some(crypto) => crypto,
|
||||
None => visitor.missing_field("crypto")?,
|
||||
};
|
||||
|
||||
let metacrypto = match metacrypto {
|
||||
Some(metacrypto) => metacrypto,
|
||||
None => visitor.missing_field("metacrypto")?,
|
||||
};
|
||||
|
||||
visitor.end()?;
|
||||
|
||||
let result = VaultKeyFile {
|
||||
id: id,
|
||||
version: version,
|
||||
crypto: crypto,
|
||||
metacrypto: metacrypto,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for VaultKeyMetaField {
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<VaultKeyMetaField, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
deserializer.deserialize(VaultKeyMetaFieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct VaultKeyMetaFieldVisitor;
|
||||
|
||||
impl Visitor for VaultKeyMetaFieldVisitor {
|
||||
type Value = VaultKeyMetaField;
|
||||
|
||||
fn visit_str<E>(&mut self, value: &str) -> Result<Self::Value, E>
|
||||
where E: Error
|
||||
{
|
||||
match value {
|
||||
"address" => Ok(VaultKeyMetaField::Address),
|
||||
"name" => Ok(VaultKeyMetaField::Name),
|
||||
"meta" => Ok(VaultKeyMetaField::Meta),
|
||||
_ => Err(Error::custom(format!("Unknown field: '{}'", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for VaultKeyMeta {
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<VaultKeyMeta, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
static FIELDS: &'static [&'static str] = &["address", "name", "meta"];
|
||||
deserializer.deserialize_struct("VaultKeyMeta", FIELDS, VaultKeyMetaVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct VaultKeyMetaVisitor;
|
||||
|
||||
impl Visitor for VaultKeyMetaVisitor {
|
||||
type Value = VaultKeyMeta;
|
||||
|
||||
fn visit_map<V>(&mut self, mut visitor: V) -> Result<Self::Value, V::Error>
|
||||
where V: MapVisitor
|
||||
{
|
||||
let mut address = None;
|
||||
let mut name = None;
|
||||
let mut meta = None;
|
||||
|
||||
loop {
|
||||
match visitor.visit_key()? {
|
||||
Some(VaultKeyMetaField::Address) => { address = Some(visitor.visit_value()?); }
|
||||
Some(VaultKeyMetaField::Name) => { name = Some(visitor.visit_value()?); }
|
||||
Some(VaultKeyMetaField::Meta) => { meta = Some(visitor.visit_value()?); }
|
||||
None => { break; }
|
||||
}
|
||||
}
|
||||
|
||||
let address = match address {
|
||||
Some(address) => address,
|
||||
None => visitor.missing_field("address")?,
|
||||
};
|
||||
|
||||
visitor.end()?;
|
||||
|
||||
let result = VaultKeyMeta {
|
||||
address: address,
|
||||
name: name,
|
||||
meta: meta,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl VaultKeyFile {
|
||||
pub fn load<R>(reader: R) -> Result<Self, serde_json::Error> where R: Read {
|
||||
serde_json::from_reader(reader)
|
||||
}
|
||||
|
||||
pub fn write<W>(&self, writer: &mut W) -> Result<(), serde_json::Error> where W: Write {
|
||||
serde_json::to_writer(writer, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl VaultKeyMeta {
|
||||
pub fn load(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_slice(&bytes)
|
||||
}
|
||||
|
||||
pub fn write(&self) -> Result<Vec<u8>, serde_json::Error> {
|
||||
let s = serde_json::to_string(self)?;
|
||||
Ok(s.as_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_json;
|
||||
use json::{VaultKeyFile, Version, Crypto, Cipher, Aes128Ctr, Kdf, Pbkdf2, Prf};
|
||||
|
||||
#[test]
|
||||
fn to_and_from_json() {
|
||||
let file = VaultKeyFile {
|
||||
id: "08d82c39-88e3-7a71-6abb-89c8f36c3ceb".into(),
|
||||
version: Version::V3,
|
||||
crypto: Crypto {
|
||||
cipher: Cipher::Aes128Ctr(Aes128Ctr {
|
||||
iv: "fecb968bbc8c7e608a89ebcfe53a41d0".into(),
|
||||
}),
|
||||
ciphertext: "4befe0a66d9a4b6fec8e39eb5c90ac5dafdeaab005fff1af665fd1f9af925c91".into(),
|
||||
kdf: Kdf::Pbkdf2(Pbkdf2 {
|
||||
c: 10240,
|
||||
dklen: 32,
|
||||
prf: Prf::HmacSha256,
|
||||
salt: "f17731e84ecac390546692dbd4ccf6a3a2720dc9652984978381e61c28a471b2".into(),
|
||||
}),
|
||||
mac: "7c7c3daafb24cf11eb3079dfb9064a11e92f309a0ee1dd676486bab119e686b7".into(),
|
||||
},
|
||||
metacrypto: Crypto {
|
||||
cipher: Cipher::Aes128Ctr(Aes128Ctr {
|
||||
iv: "9c353fb3f894fc05946843616c26bb3f".into(),
|
||||
}),
|
||||
ciphertext: "fef0d113d7576c1702daf380ad6f4c5408389e57991cae2a174facd74bd549338e1014850bddbab7eb486ff5f5c9c5532800c6a6d4db2be2212cd5cd3769244ab230e1f369e8382a9e6d7c0a".into(),
|
||||
kdf: Kdf::Pbkdf2(Pbkdf2 {
|
||||
c: 10240,
|
||||
dklen: 32,
|
||||
prf: Prf::HmacSha256,
|
||||
salt: "aca82865174a82249a198814b263f43a631f272cbf7ed329d0f0839d259c652a".into(),
|
||||
}),
|
||||
mac: "b7413946bfe459d2801268dc331c04b3a84d92be11ef4dd9a507f895e8d9b5bd".into(),
|
||||
}
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&file).unwrap();
|
||||
let deserialized = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(file, deserialized);
|
||||
}
|
||||
}
|
6
ethstore/src/lib.rs
Normal file → Executable file
6
ethstore/src/lib.rs
Normal file → Executable file
@ -19,6 +19,7 @@
|
||||
|
||||
extern crate libc;
|
||||
extern crate itertools;
|
||||
extern crate smallvec;
|
||||
extern crate rand;
|
||||
extern crate time;
|
||||
extern crate serde;
|
||||
@ -31,6 +32,7 @@ extern crate parking_lot;
|
||||
// reexport it nicely
|
||||
extern crate ethkey as _ethkey;
|
||||
extern crate ethcrypto as crypto;
|
||||
extern crate ethcore_util as util;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@ -51,10 +53,10 @@ mod presale;
|
||||
mod random;
|
||||
mod secret_store;
|
||||
|
||||
pub use self::account::SafeAccount;
|
||||
pub use self::account::{SafeAccount};
|
||||
pub use self::error::Error;
|
||||
pub use self::ethstore::{EthStore, EthMultiStore};
|
||||
pub use self::import::{import_accounts, read_geth_accounts};
|
||||
pub use self::presale::PresaleWallet;
|
||||
pub use self::secret_store::{SimpleSecretStore, SecretStore};
|
||||
pub use self::secret_store::{SecretVaultRef, StoreAccountRef, SimpleSecretStore, SecretStore};
|
||||
pub use self::random::{random_phrase, random_string};
|
||||
|
88
ethstore/src/secret_store.rs
Normal file → Executable file
88
ethstore/src/secret_store.rs
Normal file → Executable file
@ -14,38 +14,92 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
use ethkey::{Address, Message, Signature, Secret, Public};
|
||||
use Error;
|
||||
use json::Uuid;
|
||||
|
||||
/// Key directory reference
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SecretVaultRef {
|
||||
/// Reference to key in root directory
|
||||
Root,
|
||||
/// Referenc to key in specific vault
|
||||
Vault(String),
|
||||
}
|
||||
|
||||
/// Stored account reference
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct StoreAccountRef {
|
||||
/// Vault reference
|
||||
pub vault: SecretVaultRef,
|
||||
/// Account address
|
||||
pub address: Address,
|
||||
}
|
||||
|
||||
pub trait SimpleSecretStore: Send + Sync {
|
||||
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>;
|
||||
fn change_password(&self, account: &Address, old_password: &str, new_password: &str) -> Result<(), Error>;
|
||||
fn remove_account(&self, account: &Address, password: &str) -> Result<(), Error>;
|
||||
fn insert_account(&self, vault: SecretVaultRef, secret: Secret, password: &str) -> Result<StoreAccountRef, Error>;
|
||||
fn change_password(&self, account: &StoreAccountRef, old_password: &str, new_password: &str) -> Result<(), Error>;
|
||||
fn remove_account(&self, account: &StoreAccountRef, password: &str) -> Result<(), Error>;
|
||||
|
||||
fn sign(&self, account: &Address, password: &str, message: &Message) -> Result<Signature, Error>;
|
||||
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>;
|
||||
fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result<Signature, Error>;
|
||||
fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>;
|
||||
|
||||
fn accounts(&self) -> Result<Vec<Address>, Error>;
|
||||
fn accounts(&self) -> Result<Vec<StoreAccountRef>, Error>;
|
||||
|
||||
/// Create new vault with given password
|
||||
fn create_vault(&self, name: &str, password: &str) -> Result<(), Error>;
|
||||
/// Open vault with given password
|
||||
fn open_vault(&self, name: &str, password: &str) -> Result<(), Error>;
|
||||
/// Close vault
|
||||
fn close_vault(&self, name: &str) -> Result<(), Error>;
|
||||
/// Change vault password
|
||||
fn change_vault_password(&self, name: &str, password: &str, new_password: &str) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub trait SecretStore: SimpleSecretStore {
|
||||
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error>;
|
||||
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error>;
|
||||
fn copy_account(&self, new_store: &SimpleSecretStore, account: &Address, password: &str, new_password: &str) -> Result<(), Error>;
|
||||
fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error>;
|
||||
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: &Address, password: &str) -> Result<Public, Error>;
|
||||
fn public(&self, account: &StoreAccountRef, password: &str) -> Result<Public, Error>;
|
||||
|
||||
fn uuid(&self, account: &Address) -> Result<Uuid, Error>;
|
||||
fn name(&self, account: &Address) -> Result<String, Error>;
|
||||
fn meta(&self, account: &Address) -> Result<String, Error>;
|
||||
fn uuid(&self, account: &StoreAccountRef) -> Result<Uuid, Error>;
|
||||
fn name(&self, account: &StoreAccountRef) -> Result<String, Error>;
|
||||
fn meta(&self, account: &StoreAccountRef) -> Result<String, Error>;
|
||||
|
||||
fn set_name(&self, address: &Address, name: String) -> Result<(), Error>;
|
||||
fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error>;
|
||||
fn set_name(&self, account: &StoreAccountRef, name: String) -> Result<(), Error>;
|
||||
fn set_meta(&self, account: &StoreAccountRef, meta: String) -> Result<(), Error>;
|
||||
|
||||
fn local_path(&self) -> String;
|
||||
fn list_geth_accounts(&self, testnet: bool) -> Vec<Address>;
|
||||
fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error>;
|
||||
fn import_geth_accounts(&self, vault: SecretVaultRef, desired: Vec<Address>, testnet: bool) -> Result<Vec<StoreAccountRef>, Error>;
|
||||
}
|
||||
|
||||
impl StoreAccountRef {
|
||||
/// Create reference to root account with given address
|
||||
pub fn root(address: Address) -> Self {
|
||||
StoreAccountRef::new(SecretVaultRef::Root, address)
|
||||
}
|
||||
|
||||
/// Create reference to vault account with given address
|
||||
pub fn vault(vault_name: &str, address: Address) -> Self {
|
||||
StoreAccountRef::new(SecretVaultRef::Vault(vault_name.to_owned()), address)
|
||||
}
|
||||
|
||||
/// Create new account reference
|
||||
pub fn new(vault_ref: SecretVaultRef, address: Address) -> Self {
|
||||
StoreAccountRef {
|
||||
vault: vault_ref,
|
||||
address: address,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for StoreAccountRef {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.address.hash(state);
|
||||
}
|
||||
}
|
||||
|
36
ethstore/tests/api.rs
Normal file → Executable file
36
ethstore/tests/api.rs
Normal file → Executable file
@ -19,9 +19,9 @@ extern crate ethstore;
|
||||
|
||||
mod util;
|
||||
|
||||
use ethstore::{EthStore, SimpleSecretStore};
|
||||
use ethstore::{EthStore, SimpleSecretStore, SecretVaultRef, StoreAccountRef};
|
||||
use ethstore::ethkey::{Random, Generator, Secret, KeyPair, verify_address};
|
||||
use ethstore::dir::DiskDirectory;
|
||||
use ethstore::dir::RootDiskDirectory;
|
||||
use util::TransientDir;
|
||||
|
||||
#[test]
|
||||
@ -46,9 +46,9 @@ fn secret_store_create_account() {
|
||||
let dir = TransientDir::create().unwrap();
|
||||
let store = EthStore::open(Box::new(dir)).unwrap();
|
||||
assert_eq!(store.accounts().unwrap().len(), 0);
|
||||
assert!(store.insert_account(random_secret(), "").is_ok());
|
||||
assert!(store.insert_account(SecretVaultRef::Root, random_secret(), "").is_ok());
|
||||
assert_eq!(store.accounts().unwrap().len(), 1);
|
||||
assert!(store.insert_account(random_secret(), "").is_ok());
|
||||
assert!(store.insert_account(SecretVaultRef::Root, random_secret(), "").is_ok());
|
||||
assert_eq!(store.accounts().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ fn secret_store_create_account() {
|
||||
fn secret_store_sign() {
|
||||
let dir = TransientDir::create().unwrap();
|
||||
let store = EthStore::open(Box::new(dir)).unwrap();
|
||||
assert!(store.insert_account(random_secret(), "").is_ok());
|
||||
assert!(store.insert_account(SecretVaultRef::Root, random_secret(), "").is_ok());
|
||||
let accounts = store.accounts().unwrap();
|
||||
assert_eq!(accounts.len(), 1);
|
||||
assert!(store.sign(&accounts[0], "", &Default::default()).is_ok());
|
||||
@ -67,7 +67,7 @@ fn secret_store_sign() {
|
||||
fn secret_store_change_password() {
|
||||
let dir = TransientDir::create().unwrap();
|
||||
let store = EthStore::open(Box::new(dir)).unwrap();
|
||||
assert!(store.insert_account(random_secret(), "").is_ok());
|
||||
assert!(store.insert_account(SecretVaultRef::Root, random_secret(), "").is_ok());
|
||||
let accounts = store.accounts().unwrap();
|
||||
assert_eq!(accounts.len(), 1);
|
||||
assert!(store.sign(&accounts[0], "", &Default::default()).is_ok());
|
||||
@ -80,7 +80,7 @@ fn secret_store_change_password() {
|
||||
fn secret_store_remove_account() {
|
||||
let dir = TransientDir::create().unwrap();
|
||||
let store = EthStore::open(Box::new(dir)).unwrap();
|
||||
assert!(store.insert_account(random_secret(), "").is_ok());
|
||||
assert!(store.insert_account(SecretVaultRef::Root, random_secret(), "").is_ok());
|
||||
let accounts = store.accounts().unwrap();
|
||||
assert_eq!(accounts.len(), 1);
|
||||
assert!(store.remove_account(&accounts[0], "").is_ok());
|
||||
@ -111,22 +111,22 @@ fn ciphertext_path() -> &'static str {
|
||||
|
||||
#[test]
|
||||
fn secret_store_laod_geth_files() {
|
||||
let dir = DiskDirectory::at(test_path());
|
||||
let dir = RootDiskDirectory::at(test_path());
|
||||
let store = EthStore::open(Box::new(dir)).unwrap();
|
||||
assert_eq!(store.accounts().unwrap(), vec![
|
||||
"3f49624084b67849c7b4e805c5988c21a430f9d9".into(),
|
||||
"5ba4dcf897e97c2bdf8315b9ef26c13c085988cf".into(),
|
||||
"63121b431a52f8043c16fcf0d1df9cb7b5f66649".into(),
|
||||
StoreAccountRef::root("3f49624084b67849c7b4e805c5988c21a430f9d9".into()),
|
||||
StoreAccountRef::root("5ba4dcf897e97c2bdf8315b9ef26c13c085988cf".into()),
|
||||
StoreAccountRef::root("63121b431a52f8043c16fcf0d1df9cb7b5f66649".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_store_load_pat_files() {
|
||||
let dir = DiskDirectory::at(pat_path());
|
||||
let dir = RootDiskDirectory::at(pat_path());
|
||||
let store = EthStore::open(Box::new(dir)).unwrap();
|
||||
assert_eq!(store.accounts().unwrap(), vec![
|
||||
"3f49624084b67849c7b4e805c5988c21a430f9d9".into(),
|
||||
"5ba4dcf897e97c2bdf8315b9ef26c13c085988cf".into(),
|
||||
StoreAccountRef::root("3f49624084b67849c7b4e805c5988c21a430f9d9".into()),
|
||||
StoreAccountRef::root("5ba4dcf897e97c2bdf8315b9ef26c13c085988cf".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -136,19 +136,19 @@ fn test_decrypting_files_with_short_ciphertext() {
|
||||
let kp1 = KeyPair::from_secret("000081c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018".parse().unwrap()).unwrap();
|
||||
// d1e64e5480bfaf733ba7d48712decb8227797a4e , 31
|
||||
let kp2 = KeyPair::from_secret("00fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35".parse().unwrap()).unwrap();
|
||||
let dir = DiskDirectory::at(ciphertext_path());
|
||||
let dir = RootDiskDirectory::at(ciphertext_path());
|
||||
let store = EthStore::open(Box::new(dir)).unwrap();
|
||||
let accounts = store.accounts().unwrap();
|
||||
assert_eq!(accounts, vec![
|
||||
"31e9d1e6d844bd3a536800ef8d8be6a9975db509".into(),
|
||||
"d1e64e5480bfaf733ba7d48712decb8227797a4e".into(),
|
||||
StoreAccountRef::root("31e9d1e6d844bd3a536800ef8d8be6a9975db509".into()),
|
||||
StoreAccountRef::root("d1e64e5480bfaf733ba7d48712decb8227797a4e".into()),
|
||||
]);
|
||||
|
||||
let message = Default::default();
|
||||
|
||||
let s1 = store.sign(&accounts[0], "foo", &message).unwrap();
|
||||
let s2 = store.sign(&accounts[1], "foo", &message).unwrap();
|
||||
assert!(verify_address(&accounts[0], &s1, &message).unwrap());
|
||||
assert!(verify_address(&accounts[0].address, &s1, &message).unwrap());
|
||||
assert!(verify_address(&kp1.address(), &s1, &message).unwrap());
|
||||
assert!(verify_address(&kp2.address(), &s2, &message).unwrap());
|
||||
}
|
||||
|
8
ethstore/tests/util/transient_dir.rs
Normal file → Executable file
8
ethstore/tests/util/transient_dir.rs
Normal file → Executable file
@ -17,7 +17,7 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
use rand::{Rng, OsRng};
|
||||
use ethstore::dir::{KeyDirectory, DiskDirectory};
|
||||
use ethstore::dir::{KeyDirectory, RootDiskDirectory};
|
||||
use ethstore::{Error, SafeAccount};
|
||||
|
||||
pub fn random_dir() -> PathBuf {
|
||||
@ -28,7 +28,7 @@ pub fn random_dir() -> PathBuf {
|
||||
}
|
||||
|
||||
pub struct TransientDir {
|
||||
dir: DiskDirectory,
|
||||
dir: RootDiskDirectory,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ impl TransientDir {
|
||||
pub fn create() -> Result<Self, Error> {
|
||||
let path = random_dir();
|
||||
let result = TransientDir {
|
||||
dir: DiskDirectory::create(&path)?,
|
||||
dir: RootDiskDirectory::create(&path)?,
|
||||
path: path,
|
||||
};
|
||||
|
||||
@ -46,7 +46,7 @@ impl TransientDir {
|
||||
pub fn open() -> Self {
|
||||
let path = random_dir();
|
||||
TransientDir {
|
||||
dir: DiskDirectory::at(&path),
|
||||
dir: RootDiskDirectory::at(&path),
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,8 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
use ethcore::ethstore::{EthStore, SecretStore, import_accounts, read_geth_accounts};
|
||||
use ethcore::ethstore::dir::DiskDirectory;
|
||||
use ethcore::ethstore::dir::RootDiskDirectory;
|
||||
use ethcore::ethstore::SecretVaultRef;
|
||||
use ethcore::account_provider::AccountProvider;
|
||||
use helpers::{password_prompt, password_from_file};
|
||||
use params::SpecType;
|
||||
@ -69,14 +70,14 @@ pub fn execute(cmd: AccountCmd) -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn keys_dir(path: String, spec: SpecType) -> Result<DiskDirectory, String> {
|
||||
fn keys_dir(path: String, spec: SpecType) -> Result<RootDiskDirectory, String> {
|
||||
let spec = spec.spec()?;
|
||||
let mut path = PathBuf::from(&path);
|
||||
path.push(spec.data_dir);
|
||||
DiskDirectory::create(path).map_err(|e| format!("Could not open keys directory: {}", e))
|
||||
RootDiskDirectory::create(path).map_err(|e| format!("Could not open keys directory: {}", e))
|
||||
}
|
||||
|
||||
fn secret_store(dir: Box<DiskDirectory>, iterations: Option<u32>) -> Result<EthStore, String> {
|
||||
fn secret_store(dir: Box<RootDiskDirectory>, iterations: Option<u32>) -> Result<EthStore, String> {
|
||||
match iterations {
|
||||
Some(i) => EthStore::open_with_iterations(dir, i),
|
||||
_ => EthStore::open(dir)
|
||||
@ -113,7 +114,7 @@ fn import(i: ImportAccounts) -> Result<String, String> {
|
||||
let to = keys_dir(i.to, i.spec)?;
|
||||
let mut imported = 0;
|
||||
for path in &i.from {
|
||||
let from = DiskDirectory::at(path);
|
||||
let from = RootDiskDirectory::at(path);
|
||||
imported += import_accounts(&from, &to).map_err(|_| "Importing accounts failed.")?.len();
|
||||
}
|
||||
Ok(format!("{} account(s) imported", imported))
|
||||
@ -126,7 +127,7 @@ fn import_geth(i: ImportFromGethAccounts) -> Result<String, String> {
|
||||
let dir = Box::new(keys_dir(i.to, i.spec)?);
|
||||
let secret_store = Box::new(secret_store(dir, None)?);
|
||||
let geth_accounts = read_geth_accounts(i.testnet);
|
||||
match secret_store.import_geth_accounts(geth_accounts, i.testnet) {
|
||||
match secret_store.import_geth_accounts(SecretVaultRef::Root, geth_accounts, i.testnet) {
|
||||
Ok(v) => Ok(format!("Successfully imported {} account(s) from geth.", v.len())),
|
||||
Err(Error::Io(ref io_err)) if io_err.kind() == ErrorKind::NotFound => Err("Failed to find geth keys folder.".into()),
|
||||
Err(err) => Err(format!("Import geth accounts failed. {}", err))
|
||||
|
@ -15,7 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use ethcore::ethstore::{PresaleWallet, EthStore};
|
||||
use ethcore::ethstore::dir::DiskDirectory;
|
||||
use ethcore::ethstore::dir::RootDiskDirectory;
|
||||
use ethcore::account_provider::AccountProvider;
|
||||
use helpers::{password_prompt, password_from_file};
|
||||
use params::SpecType;
|
||||
@ -35,7 +35,7 @@ pub fn execute(cmd: ImportWallet) -> Result<String, String> {
|
||||
None => password_prompt()?,
|
||||
};
|
||||
|
||||
let dir = Box::new(DiskDirectory::create(cmd.path).unwrap());
|
||||
let dir = Box::new(RootDiskDirectory::create(cmd.path).unwrap());
|
||||
let secret_store = Box::new(EthStore::open_with_iterations(dir, cmd.iterations).unwrap());
|
||||
let acc_provider = AccountProvider::new(secret_store);
|
||||
let wallet = PresaleWallet::open(cmd.wallet_path).map_err(|_| "Unable to open presale wallet.")?;
|
||||
|
@ -506,11 +506,11 @@ fn daemonize(_pid_file: String) -> Result<(), String> {
|
||||
|
||||
fn prepare_account_provider(spec: &SpecType, dirs: &Directories, data_dir: &str, cfg: AccountsConfig, passwords: &[String]) -> Result<AccountProvider, String> {
|
||||
use ethcore::ethstore::EthStore;
|
||||
use ethcore::ethstore::dir::DiskDirectory;
|
||||
use ethcore::ethstore::dir::RootDiskDirectory;
|
||||
|
||||
let path = dirs.keys_path(data_dir);
|
||||
upgrade_key_location(&dirs.legacy_keys_path(cfg.testnet), &path);
|
||||
let dir = Box::new(DiskDirectory::create(&path).map_err(|e| format!("Could not open keys directory: {}", e))?);
|
||||
let dir = Box::new(RootDiskDirectory::create(&path).map_err(|e| format!("Could not open keys directory: {}", e))?);
|
||||
let account_provider = AccountProvider::new(Box::new(
|
||||
EthStore::open_with_iterations(dir, cfg.iterations).map_err(|e| format!("Could not open keys directory: {}", e))?
|
||||
));
|
||||
|
Loading…
Reference in New Issue
Block a user