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::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
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
// 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;
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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};
|
||||||
|
@ -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;
|
||||||
|
@ -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>;
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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>;
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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};
|
||||||
|
Loading…
Reference in New Issue
Block a user