diff --git a/ethcore/src/account_provider.rs b/ethcore/src/account_provider.rs index e2ccd1d83..311204bb1 100644 --- a/ethcore/src/account_provider.rs +++ b/ethcore/src/account_provider.rs @@ -20,8 +20,8 @@ use std::{fs, fmt}; use std::collections::HashMap; use std::path::PathBuf; use std::time::{Instant, Duration}; -use util::{Mutex, RwLock}; -use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore}; +use util::{Mutex, RwLock, Itertools}; +use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, SafeAccount, EthStore, EthMultiStore, random_string}; use ethstore::dir::{KeyDirectory}; use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator}; use ethjson::misc::AccountMeta; @@ -72,21 +72,35 @@ impl From for Error { #[derive(Default)] struct NullDir { - accounts: RwLock>, + accounts: RwLock>>, } impl KeyDirectory for NullDir { fn load(&self) -> Result, SSError> { - Ok(self.accounts.read().values().cloned().collect()) + Ok(self.accounts.read().values().cloned().flatten().collect()) } fn insert(&self, account: SafeAccount) -> Result { - self.accounts.write().insert(account.address.clone(), account.clone()); + self.accounts.write() + .entry(account.address.clone()) + .or_insert_with(Vec::new) + .push(account.clone()); Ok(account) } - fn remove(&self, address: &Address) -> Result<(), SSError> { - self.accounts.write().remove(address); + fn remove(&self, account: &SafeAccount) -> Result<(), SSError> { + 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(()) } } @@ -163,10 +177,12 @@ impl AddressBook { } } -fn transient_sstore() -> Box { - Box::new(EthStore::open(Box::new(NullDir::default())).expect("NullDir load always succeeds; qed"))) +fn transient_sstore() -> EthMultiStore { + EthMultiStore::open(Box::new(NullDir::default())).expect("NullDir load always succeeds; qed") } +type AccountToken = String; + /// Account management. /// Responsible for unlocking accounts. pub struct AccountProvider { @@ -175,7 +191,7 @@ pub struct AccountProvider { /// Accounts on disk sstore: Box, /// Accounts unlocked with rolling tokens - transient_sstore: Box, + transient_sstore: EthMultiStore, } impl AccountProvider { @@ -194,7 +210,7 @@ impl AccountProvider { AccountProvider { unlocked: Mutex::new(HashMap::new()), address_book: Mutex::new(AddressBook::transient()), - sstore: transient_sstore(), + sstore: Box::new(EthStore::open(Box::new(NullDir::default())).expect("NullDir load always succeeds; qed")), transient_sstore: transient_sstore(), } } @@ -285,11 +301,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 { - 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. @@ -368,6 +381,26 @@ 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. If password is not provided the account must be unlocked. pub fn decrypt(&self, account: Address, password: Option, shared_mac: &[u8], message: &[u8]) -> Result, Error> { let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account))); @@ -450,6 +483,11 @@ mod tests { assert!(ap.insert_account(kp.secret().clone(), "test").is_ok()); // when - let (_signature, token) = ap.sign_with_token(kp.address(), "test", Default::default()).unwrap(); + 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."); } } diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs index f83e5fd3a..158a7c55a 100644 --- a/ethstore/src/ethstore.rs +++ b/ethstore/src/ethstore.rs @@ -22,7 +22,7 @@ use random::Random; use ethkey::{Signature, Address, Message, Secret, Public}; use dir::KeyDirectory; use account::SafeAccount; -use {Error, SecretStore}; +use {Error, SimpleSecretStore, SecretStore}; use json; use json::UUID; use parking_lot::RwLock; @@ -30,9 +30,7 @@ use presale::PresaleWallet; use import; pub struct EthStore { - dir: Box, - iterations: u32, - cache: RwLock>, + store: EthMultiStore, } impl EthStore { @@ -41,57 +39,46 @@ impl EthStore { } pub fn open_with_iterations(directory: Box, iterations: u32) -> Result { - 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)); - - // 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 { - { - let cache = self.cache.read(); - if let Some(account) = cache.get(address) { - return Ok(account.clone()) - } - } - try!(self.reload_accounts()); - let cache = self.cache.read(); - cache.get(address).cloned().ok_or(Error::InvalidAccount) + 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 { + self.store.insert_account(secret, password) + } + + fn accounts(&self) -> Result, 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 { + let account = try!(self.get(address)); + account.sign(password, message) + } + + fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, 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 { - 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 { let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))); let wallet = PresaleWallet::from(json_wallet); @@ -105,46 +92,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.save(safe_account)); Ok(address) } - fn accounts(&self) -> Result, Error> { - try!(self.reload_accounts()); - Ok(self.cache.read().keys().cloned().collect()) - } - - fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> { - // change password + fn test_password(&self, address: &Address, password: &str) -> Result { let account = try!(self.get(address)); - let account = try!(account.change_password(old_password, new_password, self.iterations)); - - // save to file - self.save(account) + Ok(account.check_password(password)) } - fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> { + fn copy_account(&self, new_store: &SimpleSecretStore, address: &Address, password: &str, new_password: &str) -> Result<(), Error> { let account = try!(self.get(address)); - let can_remove = account.check_password(password); - - if can_remove { - try!(self.dir.remove(&account)); - let mut cache = self.cache.write(); - cache.remove(address); - Ok(()) - } else { - Err(Error::InvalidPassword) - } - } - - fn sign(&self, address: &Address, password: &str, message: &Message) -> Result { - let account = try!(self.get(address)); - account.sign(password, message) - } - - fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error> { - let account = try!(self.get(account)); - account.decrypt(password, shared_mac, message) + let secret = try!(account.crypto.secret(password)); + try!(new_store.insert_account(secret, new_password)); + Ok(()) } fn public(&self, account: &Address, password: &str) -> Result { @@ -172,7 +133,7 @@ impl SecretStore for EthStore { account.name = name; // save to file - self.save(account) + self.store.save(account) } fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error> { @@ -180,11 +141,11 @@ impl SecretStore for EthStore { account.meta = meta; // save to file - self.save(account) + self.store.save(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
{ @@ -192,7 +153,7 @@ impl SecretStore for EthStore { } fn import_geth_accounts(&self, desired: Vec
, testnet: bool) -> Result, Error> { - import::import_geth_accounts(&*self.dir, desired.into_iter().collect(), testnet) + import::import_geth_accounts(&*self.store.dir, desired.into_iter().collect(), testnet) } } @@ -210,7 +171,7 @@ impl EthMultiStore { } pub fn open_with_iterations(directory: Box, iterations: u32) -> Result { - let mut store = EthMultiStore { + let store = EthMultiStore { dir: directory, iterations: iterations, cache: Default::default(), @@ -252,7 +213,7 @@ impl EthMultiStore { } } - pub fn insert_account(&self, account: SafeAccount) -> Result<(), Error> { + fn save(&self, account: SafeAccount) -> Result<(), Error> { //save to file let account = try!(self.dir.insert(account)); @@ -263,7 +224,24 @@ impl EthMultiStore { Ok(()) } - pub fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> { +} + +impl SimpleSecretStore for EthMultiStore { + fn insert_account(&self, secret: Secret, password: &str) -> Result { + 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 accounts(&self) -> Result, 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 { @@ -294,19 +272,19 @@ impl EthMultiStore { Err(Error::InvalidPassword) } - pub fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> { + fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> { let accounts = try!(self.get(address)); for account in accounts { // First remove try!(self.remove_account(&address, old_password)); // Then insert back with new password let new_account = try!(account.change_password(old_password, new_password, self.iterations)); - try!(self.insert_account(new_account)); + try!(self.save(new_account)); } Ok(()) } - pub fn sign(&self, address: &Address, password: &str, message: &Message) -> Result { + fn sign(&self, address: &Address, password: &str, message: &Message) -> Result { let accounts = try!(self.get(address)); for account in accounts { if account.check_password(password) { @@ -317,7 +295,7 @@ impl EthMultiStore { Err(Error::InvalidPassword) } - pub fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error> { + fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error> { let accounts = try!(self.get(account)); for account in accounts { if account.check_password(password) { @@ -328,3 +306,9 @@ impl EthMultiStore { } } +#[cfg(test)] +mod tests { + fn should_have_some_tests() { + assert_eq!(true, false) + } +} diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs index f8619ff19..3fe56b7d3 100644 --- a/ethstore/src/lib.rs +++ b/ethstore/src/lib.rs @@ -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}; diff --git a/ethstore/src/random.rs b/ethstore/src/random.rs index 954ec500f..d98f2fcdf 100644 --- a/ethstore/src/random.rs +++ b/ethstore/src/random.rs @@ -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; diff --git a/ethstore/src/secret_store.rs b/ethstore/src/secret_store.rs index 06f38922b..b62189aca 100644 --- a/ethstore/src/secret_store.rs +++ b/ethstore/src/secret_store.rs @@ -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; - fn import_presale(&self, json: &[u8], password: &str) -> Result; - fn import_wallet(&self, json: &[u8], password: &str) -> Result; 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; fn decrypt(&self, account: &Address, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error>; - fn public(&self, account: &Address, password: &str) -> Result; fn accounts(&self) -> Result, Error>; +} + +pub trait SecretStore: SimpleSecretStore { + fn import_presale(&self, json: &[u8], password: &str) -> Result; + fn import_wallet(&self, json: &[u8], password: &str) -> Result; + 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; + + fn public(&self, account: &Address, password: &str) -> Result; + fn uuid(&self, account: &Address) -> Result; fn name(&self, account: &Address) -> Result; fn meta(&self, account: &Address) -> Result;