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:
Svyatoslav Nikolsky 2017-01-30 13:44:09 +03:00 committed by Nikolay Volf
parent bf1e7ecfcb
commit 9ac4d83ca3
23 changed files with 1926 additions and 356 deletions

2
Cargo.lock generated
View File

@ -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
View 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
View 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
View 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, &params.salt, params.c),
Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, &params.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, &params.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
View 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
View 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, &params.salt, params.c),
Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, &params.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, &params.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() {

188
ethstore/src/dir/disk.rs Normal file → Executable file
View 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) => {
warn!("Invalid key file: {:?} ({})", path, err);
None
},
.into_iter()
.filter_map(|path| {
let filename = Some(path.file_name().and_then(|n| n.to_str()).expect("Keys have valid UTF8 names only.").to_owned());
fs::File::open(path.clone())
.map_err(Into::into)
.and_then(|file| self.key_manager.read(filename, file))
.map_err(|err| {
warn!("Invalid key file: {:?} ({})", path, err);
err
})
.map(|account| (path, account))
.ok()
})
.collect()
)
}
/// insert account with given 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
View 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
View 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
View 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
View 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
View 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(),

559
ethstore/src/ethstore.rs Normal file → Executable file
View 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();
let mut accounts = cache.entry(account.address.clone()).or_insert_with(Vec::new);
accounts.push(account);
Ok(())
cache.entry(account_ref.clone())
.or_insert_with(Vec::new)
.push(account);
Ok(account_ref)
}
fn update(&self, old: SafeAccount, new: SafeAccount) -> Result<(), Error> {
fn update(&self, account_ref: &StoreAccountRef, old: SafeAccount, new: SafeAccount) -> Result<(), Error> {
// save to file
let account = self.dir.update(new)?;
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);
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(position) = accounts.iter().position(|acc| acc == &account) {
accounts.remove(position);
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
}
accounts.is_empty()
};
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)?;
for account in accounts {
// Change password
let new_account = account.change_password(old_password, new_password, self.iterations)?;
self.update(account, new_account)?;
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_ref, account, new_account)?;
}
Ok(())
},
SecretVaultRef::Vault(ref vault_name) => {
self.change_vault_password(vault_name, old_password, new_password)
},
}
Ok(())
}
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
View 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
View 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);
}
}

View 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
View 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
View 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
View 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
View 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,
}
}

View File

@ -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))

View File

@ -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.")?;

View File

@ -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))?
));