Merge pull request #3691 from ethcore/rotating-key
Signing transactions with rotating token
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user