From bb1bbebfd63b5870478ff4bcd60d4a1ac1e00d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 23 Mar 2017 13:23:03 +0100 Subject: [PATCH] Export account RPC (#4967) * Export account RPC * Removing GethDirectory and ParityDirectory * Updating ethstore-cli help. --- ethcore/src/account_provider/mod.rs | 13 ++- ethstore/src/account/safe_account.rs | 14 +++ ethstore/src/bin/ethstore.rs | 18 ++-- ethstore/src/dir/geth.rs | 102 --------------------- ethstore/src/dir/memory.rs | 1 + ethstore/src/dir/mod.rs | 14 +-- ethstore/src/dir/parity.rs | 81 ---------------- ethstore/src/dir/paths.rs | 96 +++++++++++++++++++ ethstore/src/error.rs | 17 ++++ ethstore/src/ethstore.rs | 99 +++++++++++--------- ethstore/src/import.rs | 21 +---- ethstore/src/json/key_file.rs | 22 ++++- ethstore/src/json/mod.rs | 2 +- ethstore/src/lib.rs | 8 +- ethstore/src/presale.rs | 3 + ethstore/src/secret_store.rs | 28 +++++- rpc/src/v1/impls/parity_accounts.rs | 12 +++ rpc/src/v1/tests/mocked/parity_accounts.rs | 27 ++++++ rpc/src/v1/traits/parity_accounts.rs | 5 + 19 files changed, 318 insertions(+), 265 deletions(-) delete mode 100755 ethstore/src/dir/geth.rs delete mode 100755 ethstore/src/dir/parity.rs create mode 100644 ethstore/src/dir/paths.rs diff --git a/ethcore/src/account_provider/mod.rs b/ethcore/src/account_provider/mod.rs index f9b7727db..0ecbf3b17 100755 --- a/ethcore/src/account_provider/mod.rs +++ b/ethcore/src/account_provider/mod.rs @@ -24,14 +24,16 @@ use std::fmt; use std::collections::{HashMap, HashSet}; use std::time::{Instant, Duration}; use util::{RwLock}; -use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore, - random_string, SecretVaultRef, StoreAccountRef}; +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; use hardware_wallet::{Error as HardwareError, HardwareWalletManager, KeyPath}; pub use ethstore::ethkey::Signature; -pub use ethstore::{Derivation, IndexDerivation}; +pub use ethstore::{Derivation, IndexDerivation, KeyFile}; /// Type of unlock. #[derive(Clone)] @@ -500,6 +502,11 @@ impl AccountProvider { self.sstore.change_password(&self.sstore.account_ref(address)?, &password, &new_password) } + /// Exports an account for given address. + pub fn export_account(&self, address: &Address, password: String) -> Result { + self.sstore.export_account(&self.sstore.account_ref(address)?, &password) + } + /// Helper method used for unlocking accounts. fn unlock_account(&self, address: Address, password: String, unlock: Unlock) -> Result<(), Error> { // verify password by signing dump message diff --git a/ethstore/src/account/safe_account.rs b/ethstore/src/account/safe_account.rs index d628b56ac..e0512fe8d 100755 --- a/ethstore/src/account/safe_account.rs +++ b/ethstore/src/account/safe_account.rs @@ -19,14 +19,22 @@ use {json, Error, crypto}; use account::Version; use super::crypto::Crypto; +/// Account representation. #[derive(Debug, PartialEq, Clone)] pub struct SafeAccount { + /// Account ID pub id: [u8; 16], + /// Account version pub version: Version, + /// Account address pub address: Address, + /// Account private key derivation definition. pub crypto: Crypto, + /// Account filename pub filename: Option, + /// Account name pub name: String, + /// Account metadata pub meta: String, } @@ -44,6 +52,7 @@ impl Into for SafeAccount { } impl SafeAccount { + /// Create a new account pub fn create( keypair: &KeyPair, id: [u8; 16], @@ -114,21 +123,25 @@ impl SafeAccount { }) } + /// Sign a message. pub fn sign(&self, password: &str, message: &Message) -> Result { let secret = self.crypto.secret(password)?; sign(&secret, message).map_err(From::from) } + /// Decrypt a message. pub fn decrypt(&self, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error> { let secret = self.crypto.secret(password)?; crypto::ecies::decrypt(&secret, shared_mac, message).map_err(From::from) } + /// Derive public key. pub fn public(&self, password: &str) -> Result { let secret = self.crypto.secret(password)?; Ok(KeyPair::from_secret(secret)?.public().clone()) } + /// Change account's password. pub fn change_password(&self, old_password: &str, new_password: &str, iterations: u32) -> Result { let secret = self.crypto.secret(old_password)?; let result = SafeAccount { @@ -143,6 +156,7 @@ impl SafeAccount { Ok(result) } + /// Check if password matches the account. pub fn check_password(&self, password: &str) -> bool { self.crypto.secret(password).is_ok() } diff --git a/ethstore/src/bin/ethstore.rs b/ethstore/src/bin/ethstore.rs index 20411a629..3e8df3a35 100644 --- a/ethstore/src/bin/ethstore.rs +++ b/ethstore/src/bin/ethstore.rs @@ -22,7 +22,7 @@ use std::{env, process, fs}; use std::io::Read; use docopt::Docopt; use ethstore::ethkey::Address; -use ethstore::dir::{KeyDirectory, ParityDirectory, RootDiskDirectory, GethDirectory, DirectoryType}; +use ethstore::dir::{paths, KeyDirectory, RootDiskDirectory}; use ethstore::{EthStore, SimpleSecretStore, SecretStore, import_accounts, Error, PresaleWallet, SecretVaultRef, StoreAccountRef}; @@ -49,14 +49,14 @@ Usage: Options: -h, --help Display this message and exit. --dir DIR Specify the secret store directory. It may be either - parity, parity-test, geth, geth-test + parity, parity-(chain), geth, geth-test or a path [default: parity]. --vault VAULT Specify vault to use in this operation. --vault-pwd VAULTPWD Specify vault password to use in this operation. Please note that this option is required when vault option is set. Otherwise it is ignored. --src DIR Specify import source. It may be either - parity, parity-test, get, geth-test + parity, parity-(chain), get, geth-test or a path [default: geth]. Commands: @@ -116,10 +116,13 @@ fn main() { fn key_dir(location: &str) -> Result, Error> { let dir: Box = match location { - "parity" => Box::new(ParityDirectory::create(DirectoryType::Main)?), - "parity-test" => Box::new(ParityDirectory::create(DirectoryType::Testnet)?), - "geth" => Box::new(GethDirectory::create(DirectoryType::Main)?), - "geth-test" => Box::new(GethDirectory::create(DirectoryType::Testnet)?), + "geth" => Box::new(RootDiskDirectory::create(paths::geth(false))?), + "geth-test" => Box::new(RootDiskDirectory::create(paths::geth(true))?), + path if path.starts_with("parity") => { + let chain = path.split('-').nth(1).unwrap_or("ethereum"); + let path = paths::parity(chain); + Box::new(RootDiskDirectory::create(path)?) + }, path => Box::new(RootDiskDirectory::create(path)?), }; @@ -254,4 +257,3 @@ fn execute(command: I) -> Result where I: IntoIterator. - -use std::env; -use std::path::PathBuf; -use {SafeAccount, Error}; -use super::{KeyDirectory, RootDiskDirectory, DirectoryType}; - -#[cfg(target_os = "macos")] -fn geth_dir_path() -> PathBuf { - let mut home = env::home_dir().expect("Failed to get home dir"); - home.push("Library"); - home.push("Ethereum"); - home -} - -#[cfg(windows)] -/// Default path for ethereum installation on Windows -pub fn geth_dir_path() -> PathBuf { - let mut home = env::home_dir().expect("Failed to get home dir"); - home.push("AppData"); - home.push("Roaming"); - home.push("Ethereum"); - home -} - -#[cfg(not(any(target_os = "macos", windows)))] -/// Default path for ethereum installation on posix system which is not Mac OS -pub fn geth_dir_path() -> PathBuf { - let mut home = env::home_dir().expect("Failed to get home dir"); - home.push(".ethereum"); - home -} - -fn geth_keystore(t: DirectoryType) -> PathBuf { - let mut dir = geth_dir_path(); - match t { - DirectoryType::Testnet => { - dir.push("testnet"); - dir.push("keystore"); - }, - DirectoryType::Main => { - dir.push("keystore"); - } - } - dir -} - -pub struct GethDirectory { - dir: RootDiskDirectory, -} - -impl GethDirectory { - pub fn create(t: DirectoryType) -> Result { - let result = GethDirectory { - dir: RootDiskDirectory::create(geth_keystore(t))?, - }; - - Ok(result) - } - - pub fn open(t: DirectoryType) -> Self { - GethDirectory { - dir: RootDiskDirectory::at(geth_keystore(t)), - } - } -} - -impl KeyDirectory for GethDirectory { - fn load(&self) -> Result, Error> { - self.dir.load() - } - - fn insert(&self, account: SafeAccount) -> Result { - self.dir.insert(account) - } - - fn update(&self, account: SafeAccount) -> Result { - self.dir.update(account) - } - - fn remove(&self, account: &SafeAccount) -> Result<(), Error> { - self.dir.remove(account) - } - - fn unique_repr(&self) -> Result { - self.dir.unique_repr() - } -} diff --git a/ethstore/src/dir/memory.rs b/ethstore/src/dir/memory.rs index 955afc5b0..b8c2ad9ff 100644 --- a/ethstore/src/dir/memory.rs +++ b/ethstore/src/dir/memory.rs @@ -22,6 +22,7 @@ use ethkey::Address; use {SafeAccount, Error}; use super::KeyDirectory; +/// Accounts in-memory storage. #[derive(Default)] pub struct MemoryDirectory { accounts: RwLock>>, diff --git a/ethstore/src/dir/mod.rs b/ethstore/src/dir/mod.rs index 83e978707..fb22c06ee 100755 --- a/ethstore/src/dir/mod.rs +++ b/ethstore/src/dir/mod.rs @@ -14,19 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +//! Accounts Directory + use std::path::{PathBuf}; use {SafeAccount, Error}; mod disk; -mod geth; mod memory; -mod parity; mod vault; - -pub enum DirectoryType { - Testnet, - Main, -} +pub mod paths; /// `VaultKeyDirectory::set_key` error #[derive(Debug)] @@ -54,7 +50,7 @@ pub trait KeyDirectory: Send + Sync { fn load(&self) -> Result, Error>; /// Insert new key to directory fn insert(&self, account: SafeAccount) -> Result; - //// Update key in directory + /// Update key in the directory fn update(&self, account: SafeAccount) -> Result; /// Remove key from directory fn remove(&self, account: &SafeAccount) -> Result<(), Error>; @@ -95,9 +91,7 @@ pub trait VaultKeyDirectory: KeyDirectory { } 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 { diff --git a/ethstore/src/dir/parity.rs b/ethstore/src/dir/parity.rs deleted file mode 100755 index df03260d3..000000000 --- a/ethstore/src/dir/parity.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2015-2017 Parity Technologies (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -use std::env; -use std::path::PathBuf; -use {SafeAccount, Error}; -use super::{KeyDirectory, RootDiskDirectory, DirectoryType}; - -fn parity_dir_path() -> PathBuf { - let mut home = env::home_dir().expect("Failed to get home dir"); - home.push(".parity"); - home -} - -fn parity_keystore(t: DirectoryType) -> PathBuf { - let mut dir = parity_dir_path(); - match t { - DirectoryType::Testnet => { - dir.push("testnet_keys"); - }, - DirectoryType::Main => { - dir.push("keys"); - } - } - dir -} - -pub struct ParityDirectory { - dir: RootDiskDirectory, -} - -impl ParityDirectory { - pub fn create(t: DirectoryType) -> Result { - let result = ParityDirectory { - dir: RootDiskDirectory::create(parity_keystore(t))?, - }; - - Ok(result) - } - - pub fn open(t: DirectoryType) -> Self { - ParityDirectory { - dir: RootDiskDirectory::at(parity_keystore(t)), - } - } -} - -impl KeyDirectory for ParityDirectory { - fn load(&self) -> Result, Error> { - self.dir.load() - } - - fn insert(&self, account: SafeAccount) -> Result { - self.dir.insert(account) - } - - fn update(&self, account: SafeAccount) -> Result { - self.dir.update(account) - } - - fn remove(&self, account: &SafeAccount) -> Result<(), Error> { - self.dir.remove(account) - } - - fn unique_repr(&self) -> Result { - self.dir.unique_repr() - } -} diff --git a/ethstore/src/dir/paths.rs b/ethstore/src/dir/paths.rs new file mode 100644 index 000000000..db3178cff --- /dev/null +++ b/ethstore/src/dir/paths.rs @@ -0,0 +1,96 @@ +// Copyright 2015-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 . + +//! Common tools paths. + +use std::env; +use std::path::PathBuf; + +fn home() -> PathBuf { + env::home_dir().expect("Failed to get home dir") +} + +/// Geth path +pub fn geth(testnet: bool) -> PathBuf { + let mut base = geth_base(); + if testnet { + base.push("testnet"); + } + base.push("keystore"); + base +} + +/// Parity path for specific chain +pub fn parity(chain: &str) -> PathBuf { + let mut base = parity_base(); + base.push(chain); + base +} + +#[cfg(target_os = "macos")] +fn parity_base() -> PathBuf { + let mut home = home(); + home.push("Library"); + home.push("Application Support"); + home.push("io.parity.ethereum"); + home.push("keys"); + home +} + +#[cfg(windows)] +fn parity_base() -> PathBuf { + let mut home = home(); + home.push("AppData"); + home.push("Roaming"); + home.push("Parity"); + home.push("Ethereum"); + home.push("keys"); + home +} + +#[cfg(not(any(target_os = "macos", windows)))] +fn parity_base() -> PathBuf { + let mut home = home(); + home.push(".local"); + home.push("share"); + home.push("io.parity.ethereum"); + home.push("keys"); + home +} + +#[cfg(target_os = "macos")] +fn geth_base() -> PathBuf { + let mut home = home(); + home.push("Library"); + home.push("Ethereum"); + home +} + +#[cfg(windows)] +fn geth_base() -> PathBuf { + let mut home = home(); + home.push("AppData"); + home.push("Roaming"); + home.push("Ethereum"); + home +} + +#[cfg(not(any(target_os = "macos", windows)))] +fn geth_base() -> PathBuf { + let mut home = home(); + home.push(".ethereum"); + home +} diff --git a/ethstore/src/error.rs b/ethstore/src/error.rs index 8a2eb5e8b..f7e0b0bfa 100755 --- a/ethstore/src/error.rs +++ b/ethstore/src/error.rs @@ -20,23 +20,40 @@ use ethkey::Error as EthKeyError; use crypto::Error as EthCryptoError; use ethkey::DerivationError; +/// Account-related errors. #[derive(Debug)] pub enum Error { + /// IO error Io(IoError), + /// Invalid Password InvalidPassword, + /// Account's secret is invalid. InvalidSecret, + /// Invalid Vault Crypto meta. InvalidCryptoMeta, + /// Invalid Account. InvalidAccount, + /// Invalid Message. InvalidMessage, + /// Invalid Key File InvalidKeyFile(String), + /// Vaults are not supported. VaultsAreNotSupported, + /// Unsupported vault UnsupportedVault, + /// Invalid vault name InvalidVaultName, + /// Vault not found VaultNotFound, + /// Account creation failed. CreationFailed, + /// `EthKey` error EthKey(EthKeyError), + /// `EthCrypto` error EthCrypto(EthCryptoError), + /// Derivation error Derivation(DerivationError), + /// Custom error Custom(String), } diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs index cacb6054f..5fb76791e 100755 --- a/ethstore/src/ethstore.rs +++ b/ethstore/src/ethstore.rs @@ -25,18 +25,21 @@ use ethkey::{self, Signature, Address, Message, Secret, Public, KeyPair, Extende use dir::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError}; use account::SafeAccount; use presale::PresaleWallet; -use json::{self, Uuid}; +use json::{self, Uuid, OpaqueKeyFile}; use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation}; +/// Accounts store. pub struct EthStore { store: EthMultiStore, } impl EthStore { + /// Open a new accounts store with given key directory backend. pub fn open(directory: Box) -> Result { Self::open_with_iterations(directory, KEY_ITERATIONS as u32) } + /// Open a new account store with given key directory backend and custom number of iterations. pub fn open_with_iterations(directory: Box, iterations: u32) -> Result { Ok(EthStore { store: EthMultiStore::open_with_iterations(directory, iterations)?, @@ -44,7 +47,7 @@ impl EthStore { } fn get(&self, account: &StoreAccountRef) -> Result { - let mut accounts = self.store.get(account)?.into_iter(); + let mut accounts = self.store.get_accounts(account)?.into_iter(); accounts.next().ok_or(Error::InvalidAccount) } } @@ -76,6 +79,10 @@ impl SimpleSecretStore for EthStore { self.store.change_password(account, old_password, new_password) } + fn export_account(&self, account: &StoreAccountRef, password: &str) -> Result { + self.store.export_account(account, password) + } + fn remove_account(&self, account: &StoreAccountRef, password: &str) -> Result<(), Error> { self.store.remove_account(account, password) } @@ -234,11 +241,12 @@ pub struct EthMultiStore { } impl EthMultiStore { - + /// Open new multi-accounts store with given key directory backend. pub fn open(directory: Box) -> Result { Self::open_with_iterations(directory, KEY_ITERATIONS as u32) } + /// Open new multi-accounts store with given key directory backend and custom number of iterations for new keys. pub fn open_with_iterations(directory: Box, iterations: u32) -> Result { let store = EthMultiStore { dir: directory, @@ -259,7 +267,7 @@ impl EthMultiStore { } self.reload_accounts()?; *last_dir_hash = dir_hash; - Ok(()) + Ok(()) } fn reload_accounts(&self) -> Result<(), Error> { @@ -287,7 +295,7 @@ impl EthMultiStore { Ok(()) } - fn get(&self, account: &StoreAccountRef) -> Result, Error> { + fn get_accounts(&self, account: &StoreAccountRef) -> Result, Error> { { let cache = self.cache.read(); if let Some(accounts) = cache.get(account) { @@ -307,6 +315,15 @@ impl EthMultiStore { } } + fn get_matching(&self, account: &StoreAccountRef, password: &str) -> Result, Error> { + let accounts = self.get_accounts(account)?; + + Ok(accounts.into_iter() + .filter(|acc| acc.check_password(password)) + .collect() + ) + } + fn import(&self, vault: SecretVaultRef, account: SafeAccount) -> Result { // save to file let account = match vault { @@ -398,12 +415,8 @@ impl SimpleSecretStore for EthMultiStore { fn insert_derived(&self, vault: SecretVaultRef, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) -> Result { - let accounts = self.get(account_ref)?; + let accounts = self.get_matching(account_ref, password)?; for account in accounts { - // Skip if password is invalid - if !account.check_password(password) { - continue; - } let extended = self.generate(account.crypto.secret(password)?, derivation)?; return self.insert_account(vault, extended.secret().as_raw().clone(), password); } @@ -413,14 +426,9 @@ impl SimpleSecretStore for EthMultiStore { fn generate_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) -> Result { - let accounts = self.get(&account_ref)?; + let accounts = self.get_matching(&account_ref, password)?; for account in accounts { - // Skip if password is invalid - if !account.check_password(password) { - continue; - } let extended = self.generate(account.crypto.secret(password)?, derivation)?; - return Ok(ethkey::public_to_address(extended.public().public())); } Err(Error::InvalidPassword) @@ -429,18 +437,13 @@ impl SimpleSecretStore for EthMultiStore { fn sign_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation, message: &Message) -> Result { - let accounts = self.get(&account_ref)?; + let accounts = self.get_matching(&account_ref, password)?; for account in accounts { - // Skip if password is invalid - if !account.check_password(password) { - continue; - } let extended = self.generate(account.crypto.secret(password)?, derivation)?; let secret = extended.secret().as_raw(); return Ok(ethkey::sign(&secret, message)?) } Err(Error::InvalidPassword) - } fn account_ref(&self, address: &Address) -> Result { @@ -457,47 +460,47 @@ impl SimpleSecretStore for EthMultiStore { } fn remove_account(&self, account_ref: &StoreAccountRef, password: &str) -> Result<(), Error> { - let accounts = self.get(account_ref)?; + let accounts = self.get_matching(account_ref, password)?; for account in accounts { - // Skip if password is invalid - if !account.check_password(password) { - continue; - } - return self.remove_safe_account(account_ref, &account); } + Err(Error::InvalidPassword) } fn change_password(&self, account_ref: &StoreAccountRef, old_password: &str, new_password: &str) -> Result<(), Error> { - let accounts = self.get(account_ref)?; + let accounts = self.get_matching(account_ref, old_password)?; + + if accounts.is_empty() { + return Err(Error::InvalidPassword); + } for account in accounts { // Change password let new_account = account.change_password(old_password, new_password, self.iterations)?; self.update(account_ref, account, new_account)?; } + Ok(()) } - fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result { - let accounts = self.get(account)?; - for account in accounts { - if account.check_password(password) { - return account.sign(password, message); - } - } + fn export_account(&self, account_ref: &StoreAccountRef, password: &str) -> Result { + self.get_matching(account_ref, password)?.into_iter().nth(0).map(Into::into).ok_or(Error::InvalidPassword) + } + fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result { + let accounts = self.get_matching(account, password)?; + for account in accounts { + return account.sign(password, message); + } Err(Error::InvalidPassword) } fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error> { - let accounts = self.get(account)?; + let accounts = self.get_matching(account, password)?; for account in accounts { - if account.check_password(password) { - return account.decrypt(password, shared_mac, message); - } + return account.decrypt(password, shared_mac, message); } Err(Error::InvalidPassword) } @@ -586,7 +589,7 @@ impl SimpleSecretStore for EthMultiStore { return Ok(account_ref); } - let account = self.get(&account_ref)?.into_iter().nth(0).ok_or(Error::InvalidAccount)?; + let account = self.get_accounts(&account_ref)?.into_iter().nth(0).ok_or(Error::InvalidAccount)?; let new_account_ref = self.import(vault, account.clone())?; self.remove_safe_account(&account_ref, &account)?; self.reload_accounts()?; @@ -1032,4 +1035,18 @@ mod tests { // then assert_eq!(store.get_vault_meta(name).unwrap(), "OldMeta".to_owned()); } + + #[test] + fn should_export_account() { + // given + let store = store(); + let keypair = keypair(); + let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap(); + + // when + let exported = store.export_account(&address, "test"); + + // then + assert!(exported.is_ok(), "Should export single account: {:?}", exported); + } } diff --git a/ethstore/src/import.rs b/ethstore/src/import.rs index 0090631bd..b7497c9ff 100644 --- a/ethstore/src/import.rs +++ b/ethstore/src/import.rs @@ -16,9 +16,10 @@ use std::collections::HashSet; use ethkey::Address; -use dir::{GethDirectory, KeyDirectory, DirectoryType}; +use dir::{paths, KeyDirectory, RootDiskDirectory}; use Error; +/// Import all accounts from one directory to the other. pub fn import_accounts(src: &KeyDirectory, dst: &KeyDirectory) -> Result, Error> { let accounts = src.load()?; let existing_accounts = dst.load()?.into_iter().map(|a| a.address).collect::>(); @@ -34,27 +35,15 @@ pub fn import_accounts(src: &KeyDirectory, dst: &KeyDirectory) -> Result Vec
{ - let t = if testnet { - DirectoryType::Testnet - } else { - DirectoryType::Main - }; - - GethDirectory::open(t) + RootDiskDirectory::at(paths::geth(testnet)) .load() .map(|d| d.into_iter().map(|a| a.address).collect()) .unwrap_or_else(|_| Vec::new()) } -/// Import specific `desired` accounts from the Geth keystore into `dst`. +/// Import specific `desired` accounts from the Geth keystore into `dst`. pub fn import_geth_accounts(dst: &KeyDirectory, desired: HashSet
, testnet: bool) -> Result, Error> { - let t = if testnet { - DirectoryType::Testnet - } else { - DirectoryType::Main - }; - - let src = GethDirectory::open(t); + let src = RootDiskDirectory::at(paths::geth(testnet)); let accounts = src.load()?; let existing_accounts = dst.load()?.into_iter().map(|a| a.address).collect::>(); diff --git a/ethstore/src/json/key_file.rs b/ethstore/src/json/key_file.rs index 21711df8f..a1c20acf2 100644 --- a/ethstore/src/json/key_file.rs +++ b/ethstore/src/json/key_file.rs @@ -16,11 +16,31 @@ use std::fmt; use std::io::{Read, Write}; -use serde::{Deserialize, Deserializer}; +use serde::{Serialize, Serializer, Deserialize, Deserializer}; use serde::de::{Error, Visitor, MapVisitor}; use serde_json; use super::{Uuid, Version, Crypto, H160}; +/// Public opaque type representing serializable `KeyFile`. +#[derive(Debug, PartialEq)] +pub struct OpaqueKeyFile { + key_file: KeyFile +} + +impl Serialize for OpaqueKeyFile { + fn serialize(&self, serializer: S) -> Result where + S: Serializer, + { + self.key_file.serialize(serializer) + } +} + +impl From for OpaqueKeyFile where T: Into { + fn from(val: T) -> Self { + OpaqueKeyFile { key_file: val.into() } + } +} + #[derive(Debug, PartialEq, Serialize)] pub struct KeyFile { pub id: Uuid, diff --git a/ethstore/src/json/mod.rs b/ethstore/src/json/mod.rs index 98033effd..865b75dea 100644 --- a/ethstore/src/json/mod.rs +++ b/ethstore/src/json/mod.rs @@ -36,7 +36,7 @@ pub use self::error::Error; pub use self::hash::{H128, H160, H256}; pub use self::id::Uuid; pub use self::kdf::{Kdf, KdfSer, Prf, Pbkdf2, Scrypt, KdfSerParams}; -pub use self::key_file::KeyFile; +pub use self::key_file::{KeyFile, OpaqueKeyFile}; pub use self::presale::{PresaleWallet, Encseed}; pub use self::vault_file::VaultFile; pub use self::vault_key_file::{VaultKeyFile, VaultKeyMeta, insert_vault_name_to_json_meta, remove_vault_name_from_json_meta}; diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs index f092c3fe6..8203feeec 100755 --- a/ethstore/src/lib.rs +++ b/ethstore/src/lib.rs @@ -14,6 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +//! Ethereum key-management. + + +#![warn(missing_docs)] + extern crate libc; extern crate itertools; extern crate smallvec; @@ -52,10 +57,11 @@ 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::json::OpaqueKeyFile as KeyFile; pub use self::presale::PresaleWallet; pub use self::secret_store::{ SecretVaultRef, StoreAccountRef, SimpleSecretStore, SecretStore, diff --git a/ethstore/src/presale.rs b/ethstore/src/presale.rs index 45d127664..dbbdcdc8d 100644 --- a/ethstore/src/presale.rs +++ b/ethstore/src/presale.rs @@ -8,6 +8,7 @@ use ethkey::{Address, Secret, KeyPair}; use crypto::Keccak256; use {crypto, Error}; +/// Pre-sale wallet. pub struct PresaleWallet { iv: [u8; 16], ciphertext: Vec, @@ -31,6 +32,7 @@ impl From for PresaleWallet { } impl PresaleWallet { + /// Open a pre-sale wallet. pub fn open

(path: P) -> Result where P: AsRef { let file = fs::File::open(path)?; let presale = json::PresaleWallet::load(file) @@ -38,6 +40,7 @@ impl PresaleWallet { Ok(PresaleWallet::from(presale)) } + /// Decrypt the wallet. pub fn decrypt(&self, password: &str) -> Result { let mut h_mac = Hmac::new(Sha256::new(), password.as_bytes()); let mut derived_key = vec![0u8; 16]; diff --git a/ethstore/src/secret_store.rs b/ethstore/src/secret_store.rs index 1eff95335..fd7eea50d 100755 --- a/ethstore/src/secret_store.rs +++ b/ethstore/src/secret_store.rs @@ -18,7 +18,7 @@ use std::hash::{Hash, Hasher}; use std::path::PathBuf; use ethkey::{Address, Message, Signature, Secret, Public}; use Error; -use json::Uuid; +use json::{Uuid, OpaqueKeyFile}; use util::H256; /// Key directory reference @@ -39,16 +39,28 @@ pub struct StoreAccountRef { pub address: Address, } +/// Simple Secret Store API pub trait SimpleSecretStore: Send + Sync { + /// Inserts new accounts to the store (or vault) with given password. fn insert_account(&self, vault: SecretVaultRef, secret: Secret, password: &str) -> Result; + /// Inserts new derived account to the store (or vault) with given password. fn insert_derived(&self, vault: SecretVaultRef, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) -> Result; + /// Changes accounts password. fn change_password(&self, account: &StoreAccountRef, old_password: &str, new_password: &str) -> Result<(), Error>; + /// Exports key details for account. + fn export_account(&self, account: &StoreAccountRef, password: &str) -> Result; + /// Entirely removes account from the store and underlying storage. fn remove_account(&self, account: &StoreAccountRef, password: &str) -> Result<(), Error>; + /// Generates new derived account. fn generate_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) -> Result; + /// Sign a message with given account. fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result; + /// Sign a message with derived account. fn sign_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation, message: &Message) -> Result; + /// Decrypt a messages with given account. fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error>; + /// Returns all accounts in this secret store. fn accounts(&self) -> Result, Error>; /// Get reference to some account with given address. /// This method could be removed if we will guarantee that there is max(1) account for given address. @@ -74,23 +86,37 @@ pub trait SimpleSecretStore: Send + Sync { fn set_vault_meta(&self, name: &str, meta: &str) -> Result<(), Error>; } +/// Secret Store API pub trait SecretStore: SimpleSecretStore { + /// Imports presale wallet fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result; + /// Imports existing JSON wallet fn import_wallet(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result; + /// Copies account between stores and vaults. fn copy_account(&self, new_store: &SimpleSecretStore, new_vault: SecretVaultRef, account: &StoreAccountRef, password: &str, new_password: &str) -> Result<(), Error>; + /// Checks if password matches given account. fn test_password(&self, account: &StoreAccountRef, password: &str) -> Result; + /// Returns a public key for given account. fn public(&self, account: &StoreAccountRef, password: &str) -> Result; + /// Returns uuid of an account. fn uuid(&self, account: &StoreAccountRef) -> Result; + /// Returns account's name. fn name(&self, account: &StoreAccountRef) -> Result; + /// Returns account's metadata. fn meta(&self, account: &StoreAccountRef) -> Result; + /// Modifies account metadata. fn set_name(&self, account: &StoreAccountRef, name: String) -> Result<(), Error>; + /// Modifies account name. fn set_meta(&self, account: &StoreAccountRef, meta: String) -> Result<(), Error>; + /// Returns local path of the store. fn local_path(&self) -> PathBuf; + /// Lists all found geth accounts. fn list_geth_accounts(&self, testnet: bool) -> Vec

; + /// Imports geth accounts to the store/vault. fn import_geth_accounts(&self, vault: SecretVaultRef, desired: Vec
, testnet: bool) -> Result, Error>; } diff --git a/rpc/src/v1/impls/parity_accounts.rs b/rpc/src/v1/impls/parity_accounts.rs index 60b615897..828dbf8f4 100644 --- a/rpc/src/v1/impls/parity_accounts.rs +++ b/rpc/src/v1/impls/parity_accounts.rs @@ -20,6 +20,7 @@ use std::collections::BTreeMap; use util::{Address}; use ethkey::{Brain, Generator, Secret}; +use ethstore::KeyFile; use ethcore::account_provider::AccountProvider; use jsonrpc_core::Error; @@ -315,6 +316,17 @@ impl ParityAccounts for ParityAccountsClient { .map(Into::into) .map_err(|e| errors::account("Could not derive account.", e)) } + + fn export_account(&self, addr: RpcH160, password: String) -> Result { + let addr = addr.into(); + take_weak!(self.accounts) + .export_account( + &addr, + password, + ) + .map(Into::into) + .map_err(|e| errors::account("Could not export account.", e)) + } } fn into_vec(a: Vec) -> Vec where diff --git a/rpc/src/v1/tests/mocked/parity_accounts.rs b/rpc/src/v1/tests/mocked/parity_accounts.rs index ae4f74b49..ef356cd42 100644 --- a/rpc/src/v1/tests/mocked/parity_accounts.rs +++ b/rpc/src/v1/tests/mocked/parity_accounts.rs @@ -472,3 +472,30 @@ fn derive_key_index() { let res = tester.io.handle_request_sync(&request); assert_eq!(res, Some(response.into())); } + + +#[test] +fn should_export_account() { + // given + let tester = setup(); + let wallet = r#"{"id":"6a186c80-7797-cff2-bc2e-7c1d6a6cc76e","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"a1c6ff99070f8032ca1c4e8add006373"},"ciphertext":"df27e3db64aa18d984b6439443f73660643c2d119a6f0fa2fa9a6456fc802d75","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"ddc325335cda5567a1719313e73b4842511f3e4a837c9658eeb78e51ebe8c815"},"mac":"3dc888ae79cbb226ff9c455669f6cf2d79be72120f2298f6cb0d444fddc0aa3d"},"address":"0042e5d2a662eeaca8a7e828c174f98f35d8925b","name":"parity-export-test","meta":"{\"passwordHint\":\"parity-export-test\",\"timestamp\":1490017814987}"}"#; + tester.accounts.import_wallet(wallet.as_bytes(), "parity-export-test").unwrap(); + let accounts = tester.accounts.accounts().unwrap(); + assert_eq!(accounts.len(), 1); + + // invalid password + let request = r#"{"jsonrpc":"2.0","method":"parity_exportAccount","params":["0x0042e5d2a662eeaca8a7e828c174f98f35d8925b","123"],"id":1}"#; + let response = r#"{"jsonrpc":"2.0","error":{"code":-32023,"message":"Could not export account.","data":"InvalidPassword"},"id":1}"#; + let res = tester.io.handle_request_sync(&request); + assert_eq!(res, Some(response.into())); + + // correct password + let request = r#"{"jsonrpc":"2.0","method":"parity_exportAccount","params":["0x0042e5d2a662eeaca8a7e828c174f98f35d8925b","parity-export-test"],"id":1}"#; + + let response = r#"{"jsonrpc":"2.0","result":{"address":"0042e5d2a662eeaca8a7e828c174f98f35d8925b","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"a1c6ff99070f8032ca1c4e8add006373"},"ciphertext":"df27e3db64aa18d984b6439443f73660643c2d119a6f0fa2fa9a6456fc802d75","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"ddc325335cda5567a1719313e73b4842511f3e4a837c9658eeb78e51ebe8c815"},"mac":"3dc888ae79cbb226ff9c455669f6cf2d79be72120f2298f6cb0d444fddc0aa3d"},"id":"6a186c80-7797-cff2-bc2e-7c1d6a6cc76e","meta":"{\"passwordHint\":\"parity-export-test\",\"timestamp\":1490017814987}","name":"parity-export-test","version":3},"id":1}"#; + let result = tester.io.handle_request_sync(&request); + + println!("Result: {:?}", result); + println!("Response: {:?}", response); + assert_eq!(result, Some(response.into())); +} diff --git a/rpc/src/v1/traits/parity_accounts.rs b/rpc/src/v1/traits/parity_accounts.rs index a3a9a8d9f..46372560c 100644 --- a/rpc/src/v1/traits/parity_accounts.rs +++ b/rpc/src/v1/traits/parity_accounts.rs @@ -18,6 +18,7 @@ use std::collections::BTreeMap; use jsonrpc_core::Error; +use ethstore::KeyFile; use v1::types::{H160, H256, DappId, DeriveHash, DeriveHierarchical}; build_rpc_trait! { @@ -175,5 +176,9 @@ build_rpc_trait! { /// Resulting address can be either saved as a new account (with the same password). #[rpc(name = "parity_deriveAddressIndex")] fn derive_key_index(&self, H160, String, DeriveHierarchical, bool) -> Result; + + /// Exports an account with given address if provided password matches. + #[rpc(name = "parity_exportAccount")] + fn export_account(&self, H160, String) -> Result; } }