Merge pull request #3691 from ethcore/rotating-key

Signing transactions with rotating token
This commit is contained in:
Gav Wood 2016-12-15 13:08:19 +01:00 committed by GitHub
commit c4406c9198
21 changed files with 770 additions and 208 deletions

View File

@ -23,9 +23,9 @@ use self::stores::{AddressBook, DappsSettingsStore};
use std::fmt; use std::fmt;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};
use util::{Mutex, RwLock}; use util::RwLock;
use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore}; use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore, random_string};
use ethstore::dir::{KeyDirectory}; use ethstore::dir::MemoryDirectory;
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator}; use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
use ethjson::misc::AccountMeta; use ethjson::misc::AccountMeta;
pub use ethstore::ethkey::Signature; pub use ethstore::ethkey::Signature;
@ -73,58 +73,47 @@ impl From<SSError> for Error {
} }
} }
#[derive(Default)]
struct NullDir {
accounts: RwLock<HashMap<Address, SafeAccount>>,
}
impl KeyDirectory for NullDir {
fn load(&self) -> Result<Vec<SafeAccount>, SSError> {
Ok(self.accounts.read().values().cloned().collect())
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, SSError> {
self.accounts.write().insert(account.address.clone(), account.clone());
Ok(account)
}
fn remove(&self, address: &Address) -> Result<(), SSError> {
self.accounts.write().remove(address);
Ok(())
}
}
/// Dapp identifier /// Dapp identifier
pub type DappId = String; pub type DappId = String;
fn transient_sstore() -> EthMultiStore {
EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")
}
type AccountToken = String;
/// Account management. /// Account management.
/// Responsible for unlocking accounts. /// Responsible for unlocking accounts.
pub struct AccountProvider { pub struct AccountProvider {
unlocked: Mutex<HashMap<Address, AccountData>>, unlocked: RwLock<HashMap<Address, AccountData>>,
sstore: Box<SecretStore>,
address_book: RwLock<AddressBook>, address_book: RwLock<AddressBook>,
dapps_settings: RwLock<DappsSettingsStore>, dapps_settings: RwLock<DappsSettingsStore>,
/// Accounts on disk
sstore: Box<SecretStore>,
/// Accounts unlocked with rolling tokens
transient_sstore: EthMultiStore,
} }
impl AccountProvider { impl AccountProvider {
/// Creates new account provider. /// Creates new account provider.
pub fn new(sstore: Box<SecretStore>) -> Self { pub fn new(sstore: Box<SecretStore>) -> Self {
AccountProvider { AccountProvider {
unlocked: Mutex::new(HashMap::new()), unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())), address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())), dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
sstore: sstore, sstore: sstore,
transient_sstore: transient_sstore(),
} }
} }
/// Creates not disk backed provider. /// Creates not disk backed provider.
pub fn transient_provider() -> Self { pub fn transient_provider() -> Self {
AccountProvider { AccountProvider {
unlocked: Mutex::new(HashMap::new()), unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(AddressBook::transient()), address_book: RwLock::new(AddressBook::transient()),
dapps_settings: RwLock::new(DappsSettingsStore::transient()), dapps_settings: RwLock::new(DappsSettingsStore::transient()),
sstore: Box::new(EthStore::open(Box::new(NullDir::default())) sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
.expect("NullDir load always succeeds; qed")) transient_sstore: transient_sstore(),
} }
} }
@ -231,11 +220,8 @@ impl AccountProvider {
/// Returns `true` if the password for `account` is `password`. `false` if not. /// Returns `true` if the password for `account` is `password`. `false` if not.
pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> { pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> {
match self.sstore.sign(account, password, &Default::default()) { self.sstore.test_password(account, password)
Ok(_) => Ok(true), .map_err(Into::into)
Err(SSError::InvalidPassword) => Ok(false),
Err(e) => Err(Error::SStore(e)),
}
} }
/// Permanently removes an account. /// Permanently removes an account.
@ -256,7 +242,7 @@ impl AccountProvider {
let _ = try!(self.sstore.sign(&account, &password, &Default::default())); let _ = try!(self.sstore.sign(&account, &password, &Default::default()));
// check if account is already unlocked pernamently, if it is, do nothing // check if account is already unlocked pernamently, if it is, do nothing
let mut unlocked = self.unlocked.lock(); let mut unlocked = self.unlocked.write();
if let Some(data) = unlocked.get(&account) { if let Some(data) = unlocked.get(&account) {
if let Unlock::Perm = data.unlock { if let Unlock::Perm = data.unlock {
return Ok(()) return Ok(())
@ -273,7 +259,7 @@ impl AccountProvider {
} }
fn password(&self, account: &Address) -> Result<String, Error> { fn password(&self, account: &Address) -> Result<String, Error> {
let mut unlocked = self.unlocked.lock(); let mut unlocked = self.unlocked.write();
let data = try!(unlocked.get(account).ok_or(Error::NotUnlocked)).clone(); let data = try!(unlocked.get(account).ok_or(Error::NotUnlocked)).clone();
if let Unlock::Temp = data.unlock { if let Unlock::Temp = data.unlock {
unlocked.remove(account).expect("data exists: so key must exist: qed"); unlocked.remove(account).expect("data exists: so key must exist: qed");
@ -304,7 +290,7 @@ impl AccountProvider {
/// Checks if given account is unlocked /// Checks if given account is unlocked
pub fn is_unlocked(&self, account: Address) -> bool { pub fn is_unlocked(&self, account: Address) -> bool {
let unlocked = self.unlocked.lock(); let unlocked = self.unlocked.read();
unlocked.get(&account).is_some() unlocked.get(&account).is_some()
} }
@ -314,6 +300,48 @@ impl AccountProvider {
Ok(try!(self.sstore.sign(&account, &password, &message))) Ok(try!(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> {
let is_std_password = try!(self.sstore.test_password(&account, &token));
let new_token = random_string(16);
let signature = if is_std_password {
// Insert to transient store
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
// sign
try!(self.sstore.sign(&account, &token, &message))
} else {
// check transient store
try!(self.transient_sstore.change_password(&account, &token, &new_token));
// and sign
try!(self.transient_sstore.sign(&account, &new_token, &message))
};
Ok((signature, new_token))
}
/// 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])
-> Result<(Vec<u8>, AccountToken), Error>
{
let is_std_password = try!(self.sstore.test_password(&account, &token));
let new_token = random_string(16);
let message = if is_std_password {
// Insert to transient store
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
// decrypt
try!(self.sstore.decrypt(&account, &token, shared_mac, message))
} else {
// check transient store
try!(self.transient_sstore.change_password(&account, &token, &new_token));
// and decrypt
try!(self.transient_sstore.decrypt(&account, &token, shared_mac, message))
};
Ok((message, new_token))
}
/// Decrypts a message. If password is not provided the account must be unlocked. /// 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, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account))); let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account)));
@ -370,10 +398,26 @@ mod tests {
assert!(ap.unlock_account_timed(kp.address(), "test1".into(), 60000).is_err()); 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.unlock_account_timed(kp.address(), "test".into(), 60000).is_ok());
assert!(ap.sign(kp.address(), None, Default::default()).is_ok()); assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
ap.unlocked.lock().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now()); ap.unlocked.write().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now());
assert!(ap.sign(kp.address(), None, Default::default()).is_err()); assert!(ap.sign(kp.address(), None, Default::default()).is_err());
} }
#[test]
fn should_sign_and_return_token() {
// given
let kp = Random.generate().unwrap();
let ap = AccountProvider::transient_provider();
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());
// when
let (_signature, token) = ap.sign_with_token(kp.address(), "test".into(), Default::default()).unwrap();
// then
ap.sign_with_token(kp.address(), token.clone(), Default::default())
.expect("First usage of token should be correct.");
assert!(ap.sign_with_token(kp.address(), token, Default::default()).is_err(), "Second usage of the same token should fail.");
}
#[test] #[test]
fn should_set_dapps_addresses() { fn should_set_dapps_addresses() {
// given // given

View File

@ -18,7 +18,6 @@ use std::{fs, io};
use std::path::{PathBuf, Path}; use std::path::{PathBuf, Path};
use std::collections::HashMap; use std::collections::HashMap;
use time; use time;
use ethkey::Address;
use {json, SafeAccount, Error}; use {json, SafeAccount, Error};
use json::Uuid; use json::Uuid;
use super::KeyDirectory; use super::KeyDirectory;
@ -106,6 +105,11 @@ impl KeyDirectory for DiskDirectory {
Ok(accounts) Ok(accounts)
} }
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
// Disk store handles updates correctly iff filename is the same
self.insert(account)
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> { fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
// transform account into key file // transform account into key file
let keyfile: json::KeyFile = account.clone().into(); let keyfile: json::KeyFile = account.clone().into();
@ -138,12 +142,12 @@ impl KeyDirectory for DiskDirectory {
Ok(account) Ok(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
// enumerate all entries in keystore // enumerate all entries in keystore
// and find entry with given address // and find entry with given address
let to_remove = try!(self.files()) let to_remove = try!(self.files())
.into_iter() .into_iter()
.find(|&(_, ref account)| &account.address == address); .find(|&(_, ref acc)| acc == account);
// remove it // remove it
match to_remove { match to_remove {

View File

@ -16,7 +16,6 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use ethkey::Address;
use {SafeAccount, Error}; use {SafeAccount, Error};
use super::{KeyDirectory, DiskDirectory, DirectoryType}; use super::{KeyDirectory, DiskDirectory, DirectoryType};
@ -89,7 +88,11 @@ impl KeyDirectory for GethDirectory {
self.dir.insert(account) self.dir.insert(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.remove(address) self.dir.update(account)
}
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
self.dir.remove(account)
} }
} }

View File

@ -0,0 +1,67 @@
// Copyright 2015, 2016 Ethcore (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::collections::HashMap;
use parking_lot::RwLock;
use itertools::Itertools;
use ethkey::Address;
use {SafeAccount, Error};
use super::KeyDirectory;
#[derive(Default)]
pub struct MemoryDirectory {
accounts: RwLock<HashMap<Address, Vec<SafeAccount>>>,
}
impl KeyDirectory for MemoryDirectory {
fn load(&self) -> Result<Vec<SafeAccount>, Error> {
Ok(self.accounts.read().values().cloned().flatten().collect())
}
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
let mut lock = self.accounts.write();
let mut accounts = lock.entry(account.address.clone()).or_insert_with(Vec::new);
// If the filename is the same we just need to replace the entry
accounts.retain(|acc| acc.filename != account.filename);
accounts.push(account.clone());
Ok(account)
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
let mut lock = self.accounts.write();
let mut accounts = lock.entry(account.address.clone()).or_insert_with(Vec::new);
accounts.push(account.clone());
Ok(account)
}
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
let mut accounts = self.accounts.write();
let is_empty = if let Some(mut accounts) = accounts.get_mut(&account.address) {
if let Some(position) = accounts.iter().position(|acc| acc == account) {
accounts.remove(position);
}
accounts.is_empty()
} else {
false
};
if is_empty {
accounts.remove(&account.address);
}
Ok(())
}
}

View File

@ -14,12 +14,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use ethkey::Address;
use std::path::{PathBuf}; use std::path::{PathBuf};
use {SafeAccount, Error}; use {SafeAccount, Error};
mod disk; mod disk;
mod geth; mod geth;
mod memory;
mod parity; mod parity;
pub enum DirectoryType { pub enum DirectoryType {
@ -30,10 +30,12 @@ pub enum DirectoryType {
pub trait KeyDirectory: Send + Sync { pub trait KeyDirectory: Send + Sync {
fn load(&self) -> Result<Vec<SafeAccount>, Error>; fn load(&self) -> Result<Vec<SafeAccount>, Error>;
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error>; fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
fn remove(&self, address: &Address) -> Result<(), Error>; fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error>;
fn remove(&self, account: &SafeAccount) -> Result<(), Error>;
fn path(&self) -> Option<&PathBuf> { None } fn path(&self) -> Option<&PathBuf> { None }
} }
pub use self::disk::DiskDirectory; pub use self::disk::DiskDirectory;
pub use self::geth::GethDirectory; pub use self::geth::GethDirectory;
pub use self::memory::MemoryDirectory;
pub use self::parity::ParityDirectory; pub use self::parity::ParityDirectory;

View File

@ -16,7 +16,6 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use ethkey::Address;
use {SafeAccount, Error}; use {SafeAccount, Error};
use super::{KeyDirectory, DiskDirectory, DirectoryType}; use super::{KeyDirectory, DiskDirectory, DirectoryType};
@ -68,7 +67,11 @@ impl KeyDirectory for ParityDirectory {
self.dir.insert(account) self.dir.insert(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.remove(address) self.dir.update(account)
}
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
self.dir.remove(account)
} }
} }

View File

@ -16,23 +16,19 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::mem; use std::mem;
use ethkey::KeyPair; use parking_lot::RwLock;
use crypto::KEY_ITERATIONS; use crypto::KEY_ITERATIONS;
use random::Random; use random::Random;
use ethkey::{Signature, Address, Message, Secret, Public}; use ethkey::{Signature, Address, Message, Secret, Public, KeyPair};
use dir::KeyDirectory; use dir::KeyDirectory;
use account::SafeAccount; use account::SafeAccount;
use {Error, SecretStore};
use json;
use json::Uuid;
use parking_lot::RwLock;
use presale::PresaleWallet; use presale::PresaleWallet;
use import; use json::{self, Uuid};
use {import, Error, SimpleSecretStore, SecretStore};
pub struct EthStore { pub struct EthStore {
dir: Box<KeyDirectory>, store: EthMultiStore,
iterations: u32,
cache: RwLock<BTreeMap<Address, SafeAccount>>,
} }
impl EthStore { impl EthStore {
@ -41,57 +37,46 @@ impl EthStore {
} }
pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> { pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> {
let accounts = try!(directory.load()); Ok(EthStore {
let cache = accounts.into_iter().map(|account| (account.address.clone(), account)).collect(); store: try!(EthMultiStore::open_with_iterations(directory, iterations)),
let store = EthStore { })
dir: directory,
iterations: iterations,
cache: RwLock::new(cache),
};
Ok(store)
}
fn save(&self, account: SafeAccount) -> Result<(), Error> {
// save to file
let account = try!(self.dir.insert(account.clone()));
// update cache
let mut cache = self.cache.write();
cache.insert(account.address.clone(), account);
Ok(())
}
fn reload_accounts(&self) -> Result<(), Error> {
let mut cache = self.cache.write();
let accounts = try!(self.dir.load());
let new_accounts: BTreeMap<_, _> = accounts.into_iter().map(|account| (account.address.clone(), account)).collect();
mem::replace(&mut *cache, new_accounts);
Ok(())
} }
fn get(&self, address: &Address) -> Result<SafeAccount, Error> { fn get(&self, address: &Address) -> Result<SafeAccount, Error> {
{ let mut accounts = try!(self.store.get(address)).into_iter();
let cache = self.cache.read(); accounts.next().ok_or(Error::InvalidAccount)
if let Some(account) = cache.get(address) {
return Ok(account.clone())
} }
} }
try!(self.reload_accounts());
let cache = self.cache.read(); impl SimpleSecretStore for EthStore {
cache.get(address).cloned().ok_or(Error::InvalidAccount) fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
self.store.insert_account(secret, password)
}
fn accounts(&self) -> Result<Vec<Address>, 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 remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
self.store.remove_account(address, password)
}
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
let account = try!(self.get(address));
account.sign(password, message)
}
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let account = try!(self.get(account));
account.decrypt(password, shared_mac, message)
} }
} }
impl SecretStore for EthStore { impl SecretStore for EthStore {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
let keypair = try!(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();
try!(self.save(account));
Ok(address)
}
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error> { fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error> {
let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))); let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
let wallet = PresaleWallet::from(json_wallet); let wallet = PresaleWallet::from(json_wallet);
@ -105,48 +90,20 @@ impl SecretStore for EthStore {
let secret = try!(safe_account.crypto.secret(password).map_err(|_| Error::InvalidPassword)); let secret = try!(safe_account.crypto.secret(password).map_err(|_| Error::InvalidPassword));
safe_account.address = try!(KeyPair::from_secret(secret)).address(); safe_account.address = try!(KeyPair::from_secret(secret)).address();
let address = safe_account.address.clone(); let address = safe_account.address.clone();
try!(self.save(safe_account)); try!(self.store.import(safe_account));
Ok(address) Ok(address)
} }
fn accounts(&self) -> Result<Vec<Address>, Error> { fn test_password(&self, address: &Address, password: &str) -> Result<bool, Error> {
try!(self.reload_accounts()); let account = try!(self.get(address));
Ok(self.cache.read().keys().cloned().collect()) Ok(account.check_password(password))
} }
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> { fn copy_account(&self, new_store: &SimpleSecretStore, address: &Address, password: &str, new_password: &str) -> Result<(), Error> {
// change password
let account = try!(self.get(address)); let account = try!(self.get(address));
let account = try!(account.change_password(old_password, new_password, self.iterations)); let secret = try!(account.crypto.secret(password));
try!(new_store.insert_account(secret, new_password));
// save to file
self.save(account)
}
fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
let can_remove = {
let account = try!(self.get(address));
account.check_password(password)
};
if can_remove {
try!(self.dir.remove(address));
let mut cache = self.cache.write();
cache.remove(address);
Ok(()) Ok(())
} else {
Err(Error::InvalidPassword)
}
}
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
let account = try!(self.get(address));
account.sign(password, message)
}
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let account = try!(self.get(account));
account.decrypt(password, shared_mac, message)
} }
fn public(&self, account: &Address, password: &str) -> Result<Public, Error> { fn public(&self, account: &Address, password: &str) -> Result<Public, Error> {
@ -170,23 +127,25 @@ impl SecretStore for EthStore {
} }
fn set_name(&self, address: &Address, name: String) -> Result<(), Error> { fn set_name(&self, address: &Address, name: String) -> Result<(), Error> {
let mut account = try!(self.get(address)); let old = try!(self.get(address));
let mut account = old.clone();
account.name = name; account.name = name;
// save to file // save to file
self.save(account) self.store.update(old, account)
} }
fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error> { fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error> {
let mut account = try!(self.get(address)); let old = try!(self.get(address));
let mut account = old.clone();
account.meta = meta; account.meta = meta;
// save to file // save to file
self.save(account) self.store.update(old, account)
} }
fn local_path(&self) -> String { fn local_path(&self) -> String {
self.dir.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| String::new()) self.store.dir.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| String::new())
} }
fn list_geth_accounts(&self, testnet: bool) -> Vec<Address> { fn list_geth_accounts(&self, testnet: bool) -> Vec<Address> {
@ -194,6 +153,288 @@ impl SecretStore for EthStore {
} }
fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error> { fn import_geth_accounts(&self, desired: Vec<Address>, testnet: bool) -> Result<Vec<Address>, Error> {
import::import_geth_accounts(&*self.dir, desired.into_iter().collect(), testnet) import::import_geth_accounts(&*self.store.dir, desired.into_iter().collect(), testnet)
} }
} }
/// Similar to `EthStore` but may store many accounts (with different passwords) for the same `Address`
pub struct EthMultiStore {
dir: Box<KeyDirectory>,
iterations: u32,
cache: RwLock<BTreeMap<Address, Vec<SafeAccount>>>,
}
impl EthMultiStore {
pub fn open(directory: Box<KeyDirectory>) -> Result<Self, Error> {
Self::open_with_iterations(directory, KEY_ITERATIONS as u32)
}
pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> {
let store = EthMultiStore {
dir: directory,
iterations: iterations,
cache: Default::default(),
};
try!(store.reload_accounts());
Ok(store)
}
fn reload_accounts(&self) -> Result<(), Error> {
let mut cache = self.cache.write();
let accounts = try!(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);
}
mem::replace(&mut *cache, new_accounts);
Ok(())
}
fn get(&self, address: &Address) -> Result<Vec<SafeAccount>, Error> {
{
let cache = self.cache.read();
if let Some(accounts) = cache.get(address) {
if !accounts.is_empty() {
return Ok(accounts.clone())
}
}
}
try!(self.reload_accounts());
let cache = self.cache.read();
let accounts = try!(cache.get(address).cloned().ok_or(Error::InvalidAccount));
if accounts.is_empty() {
Err(Error::InvalidAccount)
} else {
Ok(accounts)
}
}
fn import(&self, account: SafeAccount) -> Result<(), Error> {
// save to file
let account = try!(self.dir.insert(account));
// update cache
let mut cache = self.cache.write();
let mut accounts = cache.entry(account.address.clone()).or_insert_with(Vec::new);
accounts.push(account);
Ok(())
}
fn update(&self, old: SafeAccount, new: SafeAccount) -> Result<(), Error> {
// save to file
let account = try!(self.dir.update(new));
// update cache
let mut cache = self.cache.write();
let mut accounts = cache.entry(account.address.clone()).or_insert_with(Vec::new);
// Remove old account
accounts.retain(|acc| acc != &old);
// And push updated to the end
accounts.push(account);
Ok(())
}
}
impl SimpleSecretStore for EthMultiStore {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
let keypair = try!(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();
try!(self.import(account));
Ok(address)
}
fn accounts(&self) -> Result<Vec<Address>, Error> {
try!(self.reload_accounts());
Ok(self.cache.read().keys().cloned().collect())
}
fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> {
let accounts = try!(self.get(address));
for account in accounts {
// Skip if password is invalid
if !account.check_password(password) {
continue;
}
// Remove from dir
try!(self.dir.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);
}
accounts.is_empty()
};
if is_empty {
cache.remove(address);
}
return Ok(());
}
Err(Error::InvalidPassword)
}
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> {
let accounts = try!(self.get(address));
for account in accounts {
// Change password
let new_account = try!(account.change_password(old_password, new_password, self.iterations));
try!(self.update(account, new_account));
}
Ok(())
}
fn sign(&self, address: &Address, password: &str, message: &Message) -> Result<Signature, Error> {
let accounts = try!(self.get(address));
for account in accounts {
if account.check_password(password) {
return account.sign(password, message);
}
}
Err(Error::InvalidPassword)
}
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let accounts = try!(self.get(account));
for account in accounts {
if account.check_password(password) {
return account.decrypt(password, shared_mac, message);
}
}
Err(Error::InvalidPassword)
}
}
#[cfg(test)]
mod tests {
use dir::MemoryDirectory;
use ethkey::{Random, Generator, KeyPair};
use secret_store::{SimpleSecretStore, SecretStore};
use super::{EthStore, EthMultiStore};
fn keypair() -> KeyPair {
Random.generate().unwrap()
}
fn store() -> EthStore {
EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory always load successfuly; qed")
}
fn multi_store() -> EthMultiStore {
EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory always load successfuly; qed")
}
#[test]
fn should_insert_account_successfully() {
// given
let store = store();
let keypair = keypair();
// when
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
// then
assert_eq!(address, keypair.address());
assert!(store.get(&address).is_ok(), "Should contain account.");
assert_eq!(store.accounts().unwrap().len(), 1, "Should have one account.");
}
#[test]
fn should_update_meta_and_name() {
// given
let store = store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
assert_eq!(&store.meta(&address).unwrap(), "{}");
assert_eq!(&store.name(&address).unwrap(), "");
// when
store.set_meta(&address, "meta".into()).unwrap();
store.set_name(&address, "name".into()).unwrap();
// then
assert_eq!(&store.meta(&address).unwrap(), "meta");
assert_eq!(&store.name(&address).unwrap(), "name");
assert_eq!(store.accounts().unwrap().len(), 1);
}
#[test]
fn should_remove_account() {
// given
let store = store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
// when
store.remove_account(&address, "test").unwrap();
// then
assert_eq!(store.accounts().unwrap().len(), 0, "Should remove account.");
}
#[test]
fn should_return_true_if_password_is_correct() {
// given
let store = store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
// when
let res1 = store.test_password(&address, "x").unwrap();
let res2 = store.test_password(&address, "test").unwrap();
assert!(!res1, "First password should be invalid.");
assert!(res2, "Second password should be correct.");
}
#[test]
fn multistore_should_be_able_to_have_the_same_account_twice() {
// 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();
assert_eq!(address, address2);
// when
assert!(store.remove_account(&address, "test").is_ok(), "First password should work.");
assert_eq!(store.accounts().unwrap().len(), 1);
assert!(store.remove_account(&address, "xyz").is_ok(), "Second password should work too.");
assert_eq!(store.accounts().unwrap().len(), 0);
}
#[test]
fn should_copy_account() {
// given
let store = store();
let multi_store = multi_store();
let keypair = keypair();
let address = store.insert_account(keypair.secret().clone(), "test").unwrap();
assert_eq!(multi_store.accounts().unwrap().len(), 0);
// when
store.copy_account(&multi_store, &address, "test", "xyz").unwrap();
// then
assert!(store.test_password(&address, "test").unwrap(), "First password should work for store.");
assert!(multi_store.sign(&address, "xyz", &Default::default()).is_ok(), "Second password should work for second store.");
assert_eq!(multi_store.accounts().unwrap().len(), 1);
}
}

View File

@ -50,8 +50,8 @@ mod secret_store;
pub use self::account::SafeAccount; pub use self::account::SafeAccount;
pub use self::error::Error; pub use self::error::Error;
pub use self::ethstore::EthStore; pub use self::ethstore::{EthStore, EthMultiStore};
pub use self::import::{import_accounts, read_geth_accounts}; pub use self::import::{import_accounts, read_geth_accounts};
pub use self::presale::PresaleWallet; pub use self::presale::PresaleWallet;
pub use self::secret_store::SecretStore; pub use self::secret_store::{SimpleSecretStore, SecretStore};
pub use self::random::random_phrase; pub use self::random::{random_phrase, random_string};

View File

@ -51,10 +51,16 @@ pub fn random_phrase(words: usize) -> String {
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.collect(); .collect();
} }
let mut rng = OsRng::new().unwrap(); let mut rng = OsRng::new().expect("Not able to operate without random source.");
(0..words).map(|_| rng.choose(&WORDS).unwrap()).join(" ") (0..words).map(|_| rng.choose(&WORDS).unwrap()).join(" ")
} }
/// Generate a random string of given length.
pub fn random_string(length: usize) -> String {
let mut rng = OsRng::new().expect("Not able to operate without random source.");
rng.gen_ascii_chars().take(length).collect()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::random_phrase; use super::random_phrase;

View File

@ -18,18 +18,25 @@ use ethkey::{Address, Message, Signature, Secret, Public};
use Error; use Error;
use json::Uuid; use json::Uuid;
pub trait SecretStore: Send + Sync { pub trait SimpleSecretStore: Send + Sync {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>; fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>;
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn change_password(&self, account: &Address, old_password: &str, new_password: &str) -> Result<(), 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 remove_account(&self, account: &Address, password: &str) -> Result<(), Error>;
fn sign(&self, account: &Address, password: &str, message: &Message) -> Result<Signature, 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 decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>;
fn public(&self, account: &Address, password: &str) -> Result<Public, Error>;
fn accounts(&self) -> Result<Vec<Address>, Error>; fn accounts(&self) -> Result<Vec<Address>, 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 public(&self, account: &Address, password: &str) -> Result<Public, Error>;
fn uuid(&self, account: &Address) -> Result<Uuid, Error>; fn uuid(&self, account: &Address) -> Result<Uuid, Error>;
fn name(&self, account: &Address) -> Result<String, Error>; fn name(&self, account: &Address) -> Result<String, Error>;
fn meta(&self, account: &Address) -> Result<String, Error>; fn meta(&self, account: &Address) -> Result<String, Error>;

View File

@ -19,7 +19,7 @@ extern crate ethstore;
mod util; mod util;
use ethstore::{SecretStore, EthStore}; use ethstore::{EthStore, SimpleSecretStore};
use ethstore::ethkey::{Random, Generator, Secret, KeyPair, verify_address}; use ethstore::ethkey::{Random, Generator, Secret, KeyPair, verify_address};
use ethstore::dir::DiskDirectory; use ethstore::dir::DiskDirectory;
use util::TransientDir; use util::TransientDir;

View File

@ -18,7 +18,6 @@ use std::path::PathBuf;
use std::{env, fs}; use std::{env, fs};
use rand::{Rng, OsRng}; use rand::{Rng, OsRng};
use ethstore::dir::{KeyDirectory, DiskDirectory}; use ethstore::dir::{KeyDirectory, DiskDirectory};
use ethstore::ethkey::Address;
use ethstore::{Error, SafeAccount}; use ethstore::{Error, SafeAccount};
pub fn random_dir() -> PathBuf { pub fn random_dir() -> PathBuf {
@ -64,11 +63,15 @@ impl KeyDirectory for TransientDir {
self.dir.load() self.dir.load()
} }
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.update(account)
}
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> { fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
self.dir.insert(account) self.dir.insert(account)
} }
fn remove(&self, address: &Address) -> Result<(), Error> { fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
self.dir.remove(address) self.dir.remove(account)
} }
} }

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::fmt::Debug;
use std::ops::Deref;
use rlp; use rlp;
use util::{Address, H256, U256, Uint, Bytes}; use util::{Address, H256, U256, Uint, Bytes};
use util::bytes::ToPretty; use util::bytes::ToPretty;
@ -37,45 +39,111 @@ use v1::types::{
pub const DEFAULT_MAC: [u8; 2] = [0, 0]; pub const DEFAULT_MAC: [u8; 2] = [0, 0];
pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload: ConfirmationPayload, pass: Option<String>) -> Result<ConfirmationResponse, Error> type AccountToken = String;
#[derive(Debug, Clone, PartialEq)]
pub enum SignWith {
Nothing,
Password(String),
Token(AccountToken),
}
#[derive(Debug)]
pub enum WithToken<T: Debug> {
No(T),
Yes(T, AccountToken),
}
impl<T: Debug> Deref for WithToken<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match *self {
WithToken::No(ref v) => v,
WithToken::Yes(ref v, _) => v,
}
}
}
impl<T: Debug> WithToken<T> {
pub fn map<S, F>(self, f: F) -> WithToken<S> where
S: Debug,
F: FnOnce(T) -> S,
{
match self {
WithToken::No(v) => WithToken::No(f(v)),
WithToken::Yes(v, token) => WithToken::Yes(f(v), token),
}
}
pub fn into_value(self) -> T {
match self {
WithToken::No(v) => v,
WithToken::Yes(v, _) => v,
}
}
}
impl<T: Debug> From<(T, AccountToken)> for WithToken<T> {
fn from(tuple: (T, AccountToken)) -> Self {
WithToken::Yes(tuple.0, tuple.1)
}
}
pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload: ConfirmationPayload, pass: SignWith) -> Result<WithToken<ConfirmationResponse>, Error>
where C: MiningBlockChainClient, M: MinerService where C: MiningBlockChainClient, M: MinerService
{ {
match payload { match payload {
ConfirmationPayload::SendTransaction(request) => { ConfirmationPayload::SendTransaction(request) => {
sign_and_dispatch(client, miner, accounts, request, pass) sign_and_dispatch(client, miner, accounts, request, pass)
.map(|result| result
.map(RpcH256::from) .map(RpcH256::from)
.map(ConfirmationResponse::SendTransaction) .map(ConfirmationResponse::SendTransaction)
)
}, },
ConfirmationPayload::SignTransaction(request) => { ConfirmationPayload::SignTransaction(request) => {
sign_no_dispatch(client, miner, accounts, request, pass) sign_no_dispatch(client, miner, accounts, request, pass)
.map(|result| result
.map(RpcRichRawTransaction::from) .map(RpcRichRawTransaction::from)
.map(ConfirmationResponse::SignTransaction) .map(ConfirmationResponse::SignTransaction)
)
}, },
ConfirmationPayload::Signature(address, hash) => { ConfirmationPayload::Signature(address, hash) => {
signature(accounts, address, hash, pass) signature(accounts, address, hash, pass)
.map(|result| result
.map(RpcH520::from) .map(RpcH520::from)
.map(ConfirmationResponse::Signature) .map(ConfirmationResponse::Signature)
)
}, },
ConfirmationPayload::Decrypt(address, data) => { ConfirmationPayload::Decrypt(address, data) => {
decrypt(accounts, address, data, pass) decrypt(accounts, address, data, pass)
.map(|result| result
.map(RpcBytes) .map(RpcBytes)
.map(ConfirmationResponse::Decrypt) .map(ConfirmationResponse::Decrypt)
)
}, },
} }
} }
fn signature(accounts: &AccountProvider, address: Address, hash: H256, password: Option<String>) -> Result<Signature, Error> { fn signature(accounts: &AccountProvider, address: Address, hash: H256, password: SignWith) -> Result<WithToken<Signature>, Error> {
accounts.sign(address, password.clone(), hash).map_err(|e| match password { match password.clone() {
Some(_) => errors::from_password_error(e), SignWith::Nothing => accounts.sign(address, None, hash).map(WithToken::No),
None => errors::from_signing_error(e), SignWith::Password(pass) => accounts.sign(address, Some(pass), hash).map(WithToken::No),
SignWith::Token(token) => accounts.sign_with_token(address, token, hash).map(Into::into),
}.map_err(|e| match password {
SignWith::Nothing => errors::from_signing_error(e),
_ => errors::from_password_error(e),
}) })
} }
fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: Option<String>) -> Result<Bytes, Error> { fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: SignWith) -> Result<WithToken<Bytes>, Error> {
accounts.decrypt(address, password.clone(), &DEFAULT_MAC, &msg) match password.clone() {
.map_err(|e| match password { SignWith::Nothing => accounts.decrypt(address, None, &DEFAULT_MAC, &msg).map(WithToken::No),
Some(_) => errors::from_password_error(e), SignWith::Password(pass) => accounts.decrypt(address, Some(pass), &DEFAULT_MAC, &msg).map(WithToken::No),
None => errors::from_signing_error(e), SignWith::Token(token) => accounts.decrypt_with_token(address, token, &DEFAULT_MAC, &msg).map(Into::into),
}.map_err(|e| match password {
SignWith::Nothing => errors::from_signing_error(e),
_ => errors::from_password_error(e),
}) })
} }
@ -88,7 +156,7 @@ pub fn dispatch_transaction<C, M>(client: &C, miner: &M, signed_transaction: Sig
.map(|_| hash) .map(|_| hash)
} }
pub fn sign_no_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: Option<String>) -> Result<SignedTransaction, Error> pub fn sign_no_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: SignWith) -> Result<WithToken<SignedTransaction>, Error>
where C: MiningBlockChainClient, M: MinerService { where C: MiningBlockChainClient, M: MinerService {
let network_id = client.signing_network_id(); let network_id = client.signing_network_id();
@ -110,20 +178,32 @@ pub fn sign_no_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider,
let hash = t.hash(network_id); let hash = t.hash(network_id);
let signature = try!(signature(accounts, address, hash, password)); let signature = try!(signature(accounts, address, hash, password));
t.with_signature(signature, network_id) signature.map(|sig| {
t.with_signature(sig, network_id)
})
}; };
Ok(signed_transaction) Ok(signed_transaction)
} }
pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: Option<String>) -> Result<H256, Error> pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: SignWith) -> Result<WithToken<H256>, Error>
where C: MiningBlockChainClient, M: MinerService where C: MiningBlockChainClient, M: MinerService
{ {
let network_id = client.signing_network_id(); let network_id = client.signing_network_id();
let signed_transaction = try!(sign_no_dispatch(client, miner, accounts, filled, password)); let signed_transaction = try!(sign_no_dispatch(client, miner, accounts, filled, password));
let (signed_transaction, token) = match signed_transaction {
WithToken::No(signed_transaction) => (signed_transaction, None),
WithToken::Yes(signed_transaction, token) => (signed_transaction, Some(token)),
};
trace!(target: "miner", "send_transaction: dispatching tx: {} for network ID {:?}", rlp::encode(&signed_transaction).to_vec().pretty(), network_id); trace!(target: "miner", "send_transaction: dispatching tx: {} for network ID {:?}", rlp::encode(&signed_transaction).to_vec().pretty(), network_id);
dispatch_transaction(&*client, &*miner, signed_transaction) dispatch_transaction(&*client, &*miner, signed_transaction).map(|hash| {
match token {
Some(ref token) => WithToken::Yes(hash, token.clone()),
None => WithToken::No(hash),
}
})
} }
pub fn fill_optional_fields<C, M>(request: TransactionRequest, client: &C, miner: &M) -> FilledTransactionRequest pub fn fill_optional_fields<C, M>(request: TransactionRequest, client: &C, miner: &M) -> FilledTransactionRequest

View File

@ -114,7 +114,7 @@ impl<C: 'static, M: 'static> Personal for PersonalClient<C, M> where C: MiningBl
&*miner, &*miner,
&*accounts, &*accounts,
request, request,
Some(password) dispatch::SignWith::Password(password)
).map(Into::into) ).map(|v| v.into_value().into())
} }
} }

View File

@ -26,9 +26,9 @@ use ethcore::miner::MinerService;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::traits::Signer; use v1::traits::Signer;
use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, U256, Bytes}; use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken, U256, Bytes};
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload}; use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload};
use v1::helpers::dispatch::{self, dispatch_transaction}; use v1::helpers::dispatch::{self, dispatch_transaction, WithToken};
/// Transactions confirmation (personal) rpc implementation. /// Transactions confirmation (personal) rpc implementation.
pub struct SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService { pub struct SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService {
@ -60,24 +60,10 @@ impl<C: 'static, M: 'static> SignerClient<C, M> where C: MiningBlockChainClient,
take_weak!(self.client).keep_alive(); take_weak!(self.client).keep_alive();
Ok(()) Ok(())
} }
}
impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService { fn confirm_internal<F>(&self, id: U256, modification: TransactionModification, f: F) -> Result<WithToken<ConfirmationResponse>, Error> where
F: FnOnce(&C, &M, &AccountProvider, ConfirmationPayload) -> Result<WithToken<ConfirmationResponse>, Error>,
fn requests_to_confirm(&self) -> Result<Vec<ConfirmationRequest>, Error> { {
try!(self.active());
let signer = take_weak!(self.signer);
Ok(signer.requests()
.into_iter()
.map(Into::into)
.collect()
)
}
// TODO [ToDr] TransactionModification is redundant for some calls
// might be better to replace it in future
fn confirm_request(&self, id: U256, modification: TransactionModification, pass: String) -> Result<ConfirmationResponse, Error> {
try!(self.active()); try!(self.active());
let id = id.into(); let id = id.into();
@ -97,14 +83,48 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC
request.gas = gas.into(); request.gas = gas.into();
} }
} }
let result = f(&*client, &*miner, &*accounts, payload);
// Execute // Execute
let result = dispatch::execute(&*client, &*miner, &*accounts, payload, Some(pass));
if let Ok(ref response) = result { if let Ok(ref response) = result {
signer.request_confirmed(id, Ok(response.clone())); signer.request_confirmed(id, Ok((*response).clone()));
} }
result result
}).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id))) }).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id)))
} }
}
impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService {
fn requests_to_confirm(&self) -> Result<Vec<ConfirmationRequest>, Error> {
try!(self.active());
let signer = take_weak!(self.signer);
Ok(signer.requests()
.into_iter()
.map(Into::into)
.collect()
)
}
// TODO [ToDr] TransactionModification is redundant for some calls
// might be better to replace it in future
fn confirm_request(&self, id: U256, modification: TransactionModification, pass: String) -> Result<ConfirmationResponse, Error> {
self.confirm_internal(id, modification, move |client, miner, accounts, payload| {
dispatch::execute(client, miner, accounts, payload, dispatch::SignWith::Password(pass))
}).map(|v| v.into_value())
}
fn confirm_request_with_token(&self, id: U256, modification: TransactionModification, token: String) -> Result<ConfirmationResponseWithToken, Error> {
self.confirm_internal(id, modification, move |client, miner, accounts, payload| {
dispatch::execute(client, miner, accounts, payload, dispatch::SignWith::Token(token))
}).and_then(|v| match v {
WithToken::No(_) => Err(errors::internal("Unexpected response without token.", "")),
WithToken::Yes(response, token) => Ok(ConfirmationResponseWithToken {
result: response,
token: token,
}),
})
}
fn confirm_request_raw(&self, id: U256, bytes: Bytes) -> Result<ConfirmationResponse, Error> { fn confirm_request_raw(&self, id: U256, bytes: Bytes) -> Result<ConfirmationResponse, Error> {
try!(self.active()); try!(self.active());

View File

@ -99,7 +99,9 @@ impl<C, M> SigningQueueClient<C, M> where
let sender = payload.sender(); let sender = payload.sender();
if accounts.is_unlocked(sender) { if accounts.is_unlocked(sender) {
return dispatch::execute(&*client, &*miner, &*accounts, payload, None).map(DispatchResult::Value); return dispatch::execute(&*client, &*miner, &*accounts, payload, dispatch::SignWith::Nothing)
.map(|v| v.into_value())
.map(DispatchResult::Value);
} }
take_weak!(self.signer).add_request(payload) take_weak!(self.signer).add_request(payload)

View File

@ -76,7 +76,8 @@ impl<C, M> SigningUnsafeClient<C, M> where
let accounts = take_weak!(self.accounts); let accounts = take_weak!(self.accounts);
let payload = dispatch::from_rpc(payload, &*client, &*miner); let payload = dispatch::from_rpc(payload, &*client, &*miner);
dispatch::execute(&*client, &*miner, &*accounts, payload, None) dispatch::execute(&*client, &*miner, &*accounts, payload, dispatch::SignWith::Nothing)
.map(|v| v.into_value())
} }
} }

View File

@ -209,6 +209,53 @@ fn should_confirm_transaction_and_dispatch() {
assert_eq!(tester.miner.imported_transactions.lock().len(), 1); assert_eq!(tester.miner.imported_transactions.lock().len(), 1);
} }
#[test]
fn should_confirm_transaction_with_token() {
// given
let tester = signer_tester();
let address = tester.accounts.new_account("test").unwrap();
let recipient = Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap();
tester.signer.add_request(ConfirmationPayload::SendTransaction(FilledTransactionRequest {
from: address,
to: Some(recipient),
gas_price: U256::from(10_000),
gas: U256::from(10_000_000),
value: U256::from(1),
data: vec![],
nonce: None,
})).unwrap();
let t = Transaction {
nonce: U256::zero(),
gas_price: U256::from(0x1000),
gas: U256::from(10_000_000),
action: Action::Call(recipient),
value: U256::from(0x1),
data: vec![]
};
let (signature, token) = tester.accounts.sign_with_token(address, "test".into(), t.hash(None)).unwrap();
let t = t.with_signature(signature, None);
assert_eq!(tester.signer.requests().len(), 1);
// when
let request = r#"{
"jsonrpc":"2.0",
"method":"signer_confirmRequestWithToken",
"params":["0x1", {"gasPrice":"0x1000"}, ""#.to_owned() + &token + r#""],
"id":1
}"#;
let response = r#"{"jsonrpc":"2.0","result":{"result":""#.to_owned() +
format!("0x{:?}", t.hash()).as_ref() +
r#"","token":""#;
// then
let result = tester.io.handle_request_sync(&request).unwrap();
assert!(result.starts_with(&response), "Should return correct result. Expected: {:?}, Got: {:?}", response, result);
assert_eq!(tester.signer.requests().len(), 0);
assert_eq!(tester.miner.imported_transactions.lock().len(), 1);
}
#[test] #[test]
fn should_confirm_transaction_with_rlp() { fn should_confirm_transaction_with_rlp() {
// given // given

View File

@ -17,7 +17,7 @@
//! Parity Signer-related rpc interface. //! Parity Signer-related rpc interface.
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse}; use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken};
build_rpc_trait! { build_rpc_trait! {
/// Signer extension for confirmations rpc interface. /// Signer extension for confirmations rpc interface.
@ -31,6 +31,10 @@ build_rpc_trait! {
#[rpc(name = "signer_confirmRequest")] #[rpc(name = "signer_confirmRequest")]
fn confirm_request(&self, U256, TransactionModification, String) -> Result<ConfirmationResponse, Error>; fn confirm_request(&self, U256, TransactionModification, String) -> Result<ConfirmationResponse, Error>;
/// Confirm specific request with token.
#[rpc(name = "signer_confirmRequestWithToken")]
fn confirm_request_with_token(&self, U256, TransactionModification, String) -> Result<ConfirmationResponseWithToken, Error>;
/// Confirm specific request with already signed data. /// Confirm specific request with already signed data.
#[rpc(name = "signer_confirmRequestRaw")] #[rpc(name = "signer_confirmRequestRaw")]
fn confirm_request_raw(&self, U256, Bytes) -> Result<ConfirmationResponse, Error>; fn confirm_request_raw(&self, U256, Bytes) -> Result<ConfirmationResponse, Error>;

View File

@ -101,6 +101,15 @@ impl Serialize for ConfirmationResponse {
} }
} }
/// Confirmation response with additional token for further requests
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ConfirmationResponseWithToken {
/// Actual response
pub result: ConfirmationResponse,
/// New token
pub token: String,
}
/// Confirmation payload, i.e. the thing to be confirmed /// Confirmation payload, i.e. the thing to be confirmed
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
pub enum ConfirmationPayload { pub enum ConfirmationPayload {
@ -185,7 +194,7 @@ impl<A, B> Serialize for Either<A, B> where
mod tests { mod tests {
use std::str::FromStr; use std::str::FromStr;
use serde_json; use serde_json;
use v1::types::U256; use v1::types::{U256, H256};
use v1::helpers; use v1::helpers;
use super::*; use super::*;
@ -299,5 +308,21 @@ mod tests {
gas: None, gas: None,
}); });
} }
#[test]
fn should_serialize_confirmation_response_with_token() {
// given
let response = ConfirmationResponseWithToken {
result: ConfirmationResponse::SendTransaction(H256::default()),
token: "test-token".into(),
};
// when
let res = serde_json::to_string(&response);
let expected = r#"{"result":"0x0000000000000000000000000000000000000000000000000000000000000000","token":"test-token"}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());
}
} }

View File

@ -39,7 +39,10 @@ pub use self::bytes::Bytes;
pub use self::block::{RichBlock, Block, BlockTransactions}; pub use self::block::{RichBlock, Block, BlockTransactions};
pub use self::block_number::BlockNumber; pub use self::block_number::BlockNumber;
pub use self::call_request::CallRequest; pub use self::call_request::CallRequest;
pub use self::confirmations::{ConfirmationPayload, ConfirmationRequest, ConfirmationResponse, TransactionModification, SignRequest, DecryptRequest, Either}; pub use self::confirmations::{
ConfirmationPayload, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken,
TransactionModification, SignRequest, DecryptRequest, Either
};
pub use self::dapp_id::DappId; pub use self::dapp_id::DappId;
pub use self::filter::{Filter, FilterChanges}; pub use self::filter::{Filter, FilterChanges};
pub use self::hash::{H64, H160, H256, H512, H520, H2048}; pub use self::hash::{H64, H160, H256, H512, H520, H2048};