Merge pull request #3691 from ethcore/rotating-key
Signing transactions with rotating token
This commit is contained in:
commit
c4406c9198
@ -23,9 +23,9 @@ use self::stores::{AddressBook, DappsSettingsStore};
|
||||
use std::fmt;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Instant, Duration};
|
||||
use util::{Mutex, RwLock};
|
||||
use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore};
|
||||
use ethstore::dir::{KeyDirectory};
|
||||
use util::RwLock;
|
||||
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore, random_string};
|
||||
use ethstore::dir::MemoryDirectory;
|
||||
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
||||
use ethjson::misc::AccountMeta;
|
||||
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
|
||||
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.
|
||||
/// Responsible for unlocking accounts.
|
||||
pub struct AccountProvider {
|
||||
unlocked: Mutex<HashMap<Address, AccountData>>,
|
||||
sstore: Box<SecretStore>,
|
||||
unlocked: RwLock<HashMap<Address, AccountData>>,
|
||||
address_book: RwLock<AddressBook>,
|
||||
dapps_settings: RwLock<DappsSettingsStore>,
|
||||
/// Accounts on disk
|
||||
sstore: Box<SecretStore>,
|
||||
/// Accounts unlocked with rolling tokens
|
||||
transient_sstore: EthMultiStore,
|
||||
}
|
||||
|
||||
impl AccountProvider {
|
||||
/// Creates new account provider.
|
||||
pub fn new(sstore: Box<SecretStore>) -> Self {
|
||||
AccountProvider {
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
|
||||
sstore: sstore,
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates not disk backed provider.
|
||||
pub fn transient_provider() -> Self {
|
||||
AccountProvider {
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::transient()),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
|
||||
sstore: Box::new(EthStore::open(Box::new(NullDir::default()))
|
||||
.expect("NullDir load always succeeds; qed"))
|
||||
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory 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.
|
||||
pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> {
|
||||
match self.sstore.sign(account, password, &Default::default()) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(SSError::InvalidPassword) => Ok(false),
|
||||
Err(e) => Err(Error::SStore(e)),
|
||||
}
|
||||
self.sstore.test_password(account, password)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Permanently removes an account.
|
||||
@ -256,7 +242,7 @@ impl AccountProvider {
|
||||
let _ = try!(self.sstore.sign(&account, &password, &Default::default()));
|
||||
|
||||
// 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 Unlock::Perm = data.unlock {
|
||||
return Ok(())
|
||||
@ -273,7 +259,7 @@ impl AccountProvider {
|
||||
}
|
||||
|
||||
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();
|
||||
if let Unlock::Temp = data.unlock {
|
||||
unlocked.remove(account).expect("data exists: so key must exist: qed");
|
||||
@ -304,7 +290,7 @@ impl AccountProvider {
|
||||
|
||||
/// Checks if given account is unlocked
|
||||
pub fn is_unlocked(&self, account: Address) -> bool {
|
||||
let unlocked = self.unlocked.lock();
|
||||
let unlocked = self.unlocked.read();
|
||||
unlocked.get(&account).is_some()
|
||||
}
|
||||
|
||||
@ -314,6 +300,48 @@ impl AccountProvider {
|
||||
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.
|
||||
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)));
|
||||
@ -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(), "test".into(), 60000).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());
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn should_set_dapps_addresses() {
|
||||
// given
|
||||
|
@ -18,7 +18,6 @@ use std::{fs, io};
|
||||
use std::path::{PathBuf, Path};
|
||||
use std::collections::HashMap;
|
||||
use time;
|
||||
use ethkey::Address;
|
||||
use {json, SafeAccount, Error};
|
||||
use json::Uuid;
|
||||
use super::KeyDirectory;
|
||||
@ -106,6 +105,11 @@ impl KeyDirectory for DiskDirectory {
|
||||
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> {
|
||||
// transform account into key file
|
||||
let keyfile: json::KeyFile = account.clone().into();
|
||||
@ -138,12 +142,12 @@ impl KeyDirectory for DiskDirectory {
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
// enumerate all entries in keystore
|
||||
// and find entry with given address
|
||||
let to_remove = try!(self.files())
|
||||
.into_iter()
|
||||
.find(|&(_, ref account)| &account.address == address);
|
||||
.find(|&(_, ref acc)| acc == account);
|
||||
|
||||
// remove it
|
||||
match to_remove {
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use ethkey::Address;
|
||||
use {SafeAccount, Error};
|
||||
use super::{KeyDirectory, DiskDirectory, DirectoryType};
|
||||
|
||||
@ -89,7 +88,11 @@ impl KeyDirectory for GethDirectory {
|
||||
self.dir.insert(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
self.dir.remove(address)
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.update(account)
|
||||
}
|
||||
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
self.dir.remove(account)
|
||||
}
|
||||
}
|
||||
|
67
ethstore/src/dir/memory.rs
Normal file
67
ethstore/src/dir/memory.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use ethkey::Address;
|
||||
use std::path::{PathBuf};
|
||||
use {SafeAccount, Error};
|
||||
|
||||
mod disk;
|
||||
mod geth;
|
||||
mod memory;
|
||||
mod parity;
|
||||
|
||||
pub enum DirectoryType {
|
||||
@ -30,10 +30,12 @@ pub enum DirectoryType {
|
||||
pub trait KeyDirectory: Send + Sync {
|
||||
fn load(&self) -> Result<Vec<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 }
|
||||
}
|
||||
|
||||
pub use self::disk::DiskDirectory;
|
||||
pub use self::geth::GethDirectory;
|
||||
pub use self::memory::MemoryDirectory;
|
||||
pub use self::parity::ParityDirectory;
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use ethkey::Address;
|
||||
use {SafeAccount, Error};
|
||||
use super::{KeyDirectory, DiskDirectory, DirectoryType};
|
||||
|
||||
@ -68,7 +67,11 @@ impl KeyDirectory for ParityDirectory {
|
||||
self.dir.insert(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
self.dir.remove(address)
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.update(account)
|
||||
}
|
||||
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
self.dir.remove(account)
|
||||
}
|
||||
}
|
||||
|
@ -16,23 +16,19 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::mem;
|
||||
use ethkey::KeyPair;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crypto::KEY_ITERATIONS;
|
||||
use random::Random;
|
||||
use ethkey::{Signature, Address, Message, Secret, Public};
|
||||
use ethkey::{Signature, Address, Message, Secret, Public, KeyPair};
|
||||
use dir::KeyDirectory;
|
||||
use account::SafeAccount;
|
||||
use {Error, SecretStore};
|
||||
use json;
|
||||
use json::Uuid;
|
||||
use parking_lot::RwLock;
|
||||
use presale::PresaleWallet;
|
||||
use import;
|
||||
use json::{self, Uuid};
|
||||
use {import, Error, SimpleSecretStore, SecretStore};
|
||||
|
||||
pub struct EthStore {
|
||||
dir: Box<KeyDirectory>,
|
||||
iterations: u32,
|
||||
cache: RwLock<BTreeMap<Address, SafeAccount>>,
|
||||
store: EthMultiStore,
|
||||
}
|
||||
|
||||
impl EthStore {
|
||||
@ -41,57 +37,46 @@ impl EthStore {
|
||||
}
|
||||
|
||||
pub fn open_with_iterations(directory: Box<KeyDirectory>, iterations: u32) -> Result<Self, Error> {
|
||||
let accounts = try!(directory.load());
|
||||
let cache = accounts.into_iter().map(|account| (account.address.clone(), account)).collect();
|
||||
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(())
|
||||
Ok(EthStore {
|
||||
store: try!(EthMultiStore::open_with_iterations(directory, iterations)),
|
||||
})
|
||||
}
|
||||
|
||||
fn get(&self, address: &Address) -> Result<SafeAccount, Error> {
|
||||
{
|
||||
let cache = self.cache.read();
|
||||
if let Some(account) = cache.get(address) {
|
||||
return Ok(account.clone())
|
||||
let mut accounts = try!(self.store.get(address)).into_iter();
|
||||
accounts.next().ok_or(Error::InvalidAccount)
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleSecretStore for EthStore {
|
||||
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error> {
|
||||
self.store.insert_account(secret, password)
|
||||
}
|
||||
try!(self.reload_accounts());
|
||||
let cache = self.cache.read();
|
||||
cache.get(address).cloned().ok_or(Error::InvalidAccount)
|
||||
|
||||
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 {
|
||||
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> {
|
||||
let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
|
||||
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));
|
||||
safe_account.address = try!(KeyPair::from_secret(secret)).address();
|
||||
let address = safe_account.address.clone();
|
||||
try!(self.save(safe_account));
|
||||
try!(self.store.import(safe_account));
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
try!(self.reload_accounts());
|
||||
Ok(self.cache.read().keys().cloned().collect())
|
||||
fn test_password(&self, address: &Address, password: &str) -> Result<bool, Error> {
|
||||
let account = try!(self.get(address));
|
||||
Ok(account.check_password(password))
|
||||
}
|
||||
|
||||
fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> {
|
||||
// change password
|
||||
fn copy_account(&self, new_store: &SimpleSecretStore, address: &Address, password: &str, new_password: &str) -> Result<(), Error> {
|
||||
let account = try!(self.get(address));
|
||||
let account = try!(account.change_password(old_password, new_password, self.iterations));
|
||||
|
||||
// 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);
|
||||
let secret = try!(account.crypto.secret(password));
|
||||
try!(new_store.insert_account(secret, new_password));
|
||||
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> {
|
||||
@ -170,23 +127,25 @@ impl SecretStore for EthStore {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// save to file
|
||||
self.save(account)
|
||||
self.store.update(old, account)
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// save to file
|
||||
self.save(account)
|
||||
self.store.update(old, account)
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -194,6 +153,288 @@ impl SecretStore for EthStore {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -50,8 +50,8 @@ mod secret_store;
|
||||
|
||||
pub use self::account::SafeAccount;
|
||||
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::presale::PresaleWallet;
|
||||
pub use self::secret_store::SecretStore;
|
||||
pub use self::random::random_phrase;
|
||||
pub use self::secret_store::{SimpleSecretStore, SecretStore};
|
||||
pub use self::random::{random_phrase, random_string};
|
||||
|
@ -51,10 +51,16 @@ pub fn random_phrase(words: usize) -> String {
|
||||
.map(|s| s.to_owned())
|
||||
.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(" ")
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
mod tests {
|
||||
use super::random_phrase;
|
||||
|
@ -18,18 +18,25 @@ use ethkey::{Address, Message, Signature, Secret, Public};
|
||||
use Error;
|
||||
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 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 remove_account(&self, account: &Address, password: &str) -> Result<(), Error>;
|
||||
|
||||
fn sign(&self, account: &Address, password: &str, message: &Message) -> Result<Signature, Error>;
|
||||
fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error>;
|
||||
fn public(&self, account: &Address, password: &str) -> Result<Public, 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 name(&self, account: &Address) -> Result<String, Error>;
|
||||
fn meta(&self, account: &Address) -> Result<String, Error>;
|
||||
|
@ -19,7 +19,7 @@ extern crate ethstore;
|
||||
|
||||
mod util;
|
||||
|
||||
use ethstore::{SecretStore, EthStore};
|
||||
use ethstore::{EthStore, SimpleSecretStore};
|
||||
use ethstore::ethkey::{Random, Generator, Secret, KeyPair, verify_address};
|
||||
use ethstore::dir::DiskDirectory;
|
||||
use util::TransientDir;
|
||||
|
@ -18,7 +18,6 @@ use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
use rand::{Rng, OsRng};
|
||||
use ethstore::dir::{KeyDirectory, DiskDirectory};
|
||||
use ethstore::ethkey::Address;
|
||||
use ethstore::{Error, SafeAccount};
|
||||
|
||||
pub fn random_dir() -> PathBuf {
|
||||
@ -64,11 +63,15 @@ impl KeyDirectory for TransientDir {
|
||||
self.dir.load()
|
||||
}
|
||||
|
||||
fn update(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.update(account)
|
||||
}
|
||||
|
||||
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, Error> {
|
||||
self.dir.insert(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), Error> {
|
||||
self.dir.remove(address)
|
||||
fn remove(&self, account: &SafeAccount) -> Result<(), Error> {
|
||||
self.dir.remove(account)
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@
|
||||
// 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::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use rlp;
|
||||
use util::{Address, H256, U256, Uint, Bytes};
|
||||
use util::bytes::ToPretty;
|
||||
@ -37,45 +39,111 @@ use v1::types::{
|
||||
|
||||
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
|
||||
{
|
||||
match payload {
|
||||
ConfirmationPayload::SendTransaction(request) => {
|
||||
sign_and_dispatch(client, miner, accounts, request, pass)
|
||||
.map(|result| result
|
||||
.map(RpcH256::from)
|
||||
.map(ConfirmationResponse::SendTransaction)
|
||||
)
|
||||
},
|
||||
ConfirmationPayload::SignTransaction(request) => {
|
||||
sign_no_dispatch(client, miner, accounts, request, pass)
|
||||
.map(|result| result
|
||||
.map(RpcRichRawTransaction::from)
|
||||
.map(ConfirmationResponse::SignTransaction)
|
||||
)
|
||||
},
|
||||
ConfirmationPayload::Signature(address, hash) => {
|
||||
signature(accounts, address, hash, pass)
|
||||
.map(|result| result
|
||||
.map(RpcH520::from)
|
||||
.map(ConfirmationResponse::Signature)
|
||||
)
|
||||
},
|
||||
ConfirmationPayload::Decrypt(address, data) => {
|
||||
decrypt(accounts, address, data, pass)
|
||||
.map(|result| result
|
||||
.map(RpcBytes)
|
||||
.map(ConfirmationResponse::Decrypt)
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn signature(accounts: &AccountProvider, address: Address, hash: H256, password: Option<String>) -> Result<Signature, Error> {
|
||||
accounts.sign(address, password.clone(), hash).map_err(|e| match password {
|
||||
Some(_) => errors::from_password_error(e),
|
||||
None => errors::from_signing_error(e),
|
||||
fn signature(accounts: &AccountProvider, address: Address, hash: H256, password: SignWith) -> Result<WithToken<Signature>, Error> {
|
||||
match password.clone() {
|
||||
SignWith::Nothing => accounts.sign(address, None, hash).map(WithToken::No),
|
||||
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> {
|
||||
accounts.decrypt(address, password.clone(), &DEFAULT_MAC, &msg)
|
||||
.map_err(|e| match password {
|
||||
Some(_) => errors::from_password_error(e),
|
||||
None => errors::from_signing_error(e),
|
||||
fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: SignWith) -> Result<WithToken<Bytes>, Error> {
|
||||
match password.clone() {
|
||||
SignWith::Nothing => accounts.decrypt(address, None, &DEFAULT_MAC, &msg).map(WithToken::No),
|
||||
SignWith::Password(pass) => accounts.decrypt(address, Some(pass), &DEFAULT_MAC, &msg).map(WithToken::No),
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
let network_id = client.signing_network_id();
|
||||
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);
|
||||
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
|
||||
|
@ -114,7 +114,7 @@ impl<C: 'static, M: 'static> Personal for PersonalClient<C, M> where C: MiningBl
|
||||
&*miner,
|
||||
&*accounts,
|
||||
request,
|
||||
Some(password)
|
||||
).map(Into::into)
|
||||
dispatch::SignWith::Password(password)
|
||||
).map(|v| v.into_value().into())
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,9 @@ use ethcore::miner::MinerService;
|
||||
|
||||
use jsonrpc_core::Error;
|
||||
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::dispatch::{self, dispatch_transaction};
|
||||
use v1::helpers::dispatch::{self, dispatch_transaction, WithToken};
|
||||
|
||||
/// Transactions confirmation (personal) rpc implementation.
|
||||
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();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
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>,
|
||||
{
|
||||
try!(self.active());
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
let result = f(&*client, &*miner, &*accounts, payload);
|
||||
// Execute
|
||||
let result = dispatch::execute(&*client, &*miner, &*accounts, payload, Some(pass));
|
||||
if let Ok(ref response) = result {
|
||||
signer.request_confirmed(id, Ok(response.clone()));
|
||||
signer.request_confirmed(id, Ok((*response).clone()));
|
||||
}
|
||||
result
|
||||
}).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> {
|
||||
try!(self.active());
|
||||
|
@ -99,7 +99,9 @@ impl<C, M> SigningQueueClient<C, M> where
|
||||
|
||||
let sender = payload.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)
|
||||
|
@ -76,7 +76,8 @@ impl<C, M> SigningUnsafeClient<C, M> where
|
||||
let accounts = take_weak!(self.accounts);
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,6 +209,53 @@ fn should_confirm_transaction_and_dispatch() {
|
||||
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]
|
||||
fn should_confirm_transaction_with_rlp() {
|
||||
// given
|
||||
|
@ -17,7 +17,7 @@
|
||||
//! Parity Signer-related rpc interface.
|
||||
use jsonrpc_core::Error;
|
||||
|
||||
use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse};
|
||||
use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken};
|
||||
|
||||
build_rpc_trait! {
|
||||
/// Signer extension for confirmations rpc interface.
|
||||
@ -31,6 +31,10 @@ build_rpc_trait! {
|
||||
#[rpc(name = "signer_confirmRequest")]
|
||||
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.
|
||||
#[rpc(name = "signer_confirmRequestRaw")]
|
||||
fn confirm_request_raw(&self, U256, Bytes) -> Result<ConfirmationResponse, Error>;
|
||||
|
@ -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
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
|
||||
pub enum ConfirmationPayload {
|
||||
@ -185,7 +194,7 @@ impl<A, B> Serialize for Either<A, B> where
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
use serde_json;
|
||||
use v1::types::U256;
|
||||
use v1::types::{U256, H256};
|
||||
use v1::helpers;
|
||||
use super::*;
|
||||
|
||||
@ -299,5 +308,21 @@ mod tests {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,10 @@ pub use self::bytes::Bytes;
|
||||
pub use self::block::{RichBlock, Block, BlockTransactions};
|
||||
pub use self::block_number::BlockNumber;
|
||||
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::filter::{Filter, FilterChanges};
|
||||
pub use self::hash::{H64, H160, H256, H512, H520, H2048};
|
||||
|
Loading…
Reference in New Issue
Block a user