Merge pull request #5778 from paritytech/unlock

A CLI flag to allow fast transaction signing when account is unlocked.
This commit is contained in:
Robert Habermeier 2017-06-15 15:54:51 +02:00 committed by GitHub
commit 8d04dffe69
10 changed files with 72 additions and 12 deletions

View File

@ -26,7 +26,7 @@ use std::time::{Instant, Duration};
use util::{RwLock};
use ethstore::{
SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore,
random_string, SecretVaultRef, StoreAccountRef,
random_string, SecretVaultRef, StoreAccountRef, OpaqueSecret,
};
use ethstore::dir::MemoryDirectory;
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
@ -36,10 +36,10 @@ pub use ethstore::ethkey::Signature;
pub use ethstore::{Derivation, IndexDerivation, KeyFile};
/// Type of unlock.
#[derive(Clone)]
#[derive(Clone, PartialEq)]
enum Unlock {
/// If account is unlocked temporarily, it should be locked after first usage.
Temp,
OneTime,
/// Account unlocked permantently can always sign message.
/// Use with caution.
Perm,
@ -116,8 +116,13 @@ type AccountToken = String;
/// Account management.
/// Responsible for unlocking accounts.
pub struct AccountProvider {
/// For performance reasons some methods can re-use unlocked secrets.
unlocked_secrets: RwLock<HashMap<StoreAccountRef, OpaqueSecret>>,
/// Unlocked account data.
unlocked: RwLock<HashMap<StoreAccountRef, AccountData>>,
/// Address book.
address_book: RwLock<AddressBook>,
/// Dapps settings.
dapps_settings: RwLock<DappsSettingsStore>,
/// Accounts on disk
sstore: Box<SecretStore>,
@ -125,6 +130,9 @@ pub struct AccountProvider {
transient_sstore: EthMultiStore,
/// Accounts in hardware wallets.
hardware_store: Option<HardwareWalletManager>,
/// When unlocking account permanently we additionally keep a raw secret in memory
/// to increase the performance of transaction signing.
unlock_keep_secret: bool,
/// Disallowed accounts.
blacklisted_accounts: Vec<Address>,
}
@ -135,6 +143,8 @@ pub struct AccountProviderSettings {
pub enable_hardware_wallets: bool,
/// Use the classic chain key on the hardware wallet.
pub hardware_wallet_classic_key: bool,
/// Store raw account secret when unlocking the account permanently.
pub unlock_keep_secret: bool,
/// Disallowed accounts.
pub blacklisted_accounts: Vec<Address>,
}
@ -144,6 +154,7 @@ impl Default for AccountProviderSettings {
AccountProviderSettings {
enable_hardware_wallets: false,
hardware_wallet_classic_key: false,
unlock_keep_secret: false,
blacklisted_accounts: vec![],
}
}
@ -170,12 +181,14 @@ impl AccountProvider {
}
AccountProvider {
unlocked_secrets: RwLock::new(HashMap::new()),
unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(address_book),
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
sstore: sstore,
transient_sstore: transient_sstore(),
hardware_store: hardware_store,
unlock_keep_secret: settings.unlock_keep_secret,
blacklisted_accounts: settings.blacklisted_accounts,
}
}
@ -183,12 +196,14 @@ impl AccountProvider {
/// Creates not disk backed provider.
pub fn transient_provider() -> Self {
AccountProvider {
unlocked_secrets: RwLock::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(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
transient_sstore: transient_sstore(),
hardware_store: None,
unlock_keep_secret: false,
blacklisted_accounts: vec![],
}
}
@ -537,10 +552,7 @@ impl AccountProvider {
/// Helper method used for unlocking accounts.
fn unlock_account(&self, address: Address, password: String, unlock: Unlock) -> Result<(), Error> {
// verify password by signing dump message
// result may be discarded
let account = self.sstore.account_ref(&address)?;
let _ = self.sstore.sign(&account, &password, &Default::default())?;
// check if account is already unlocked pernamently, if it is, do nothing
let mut unlocked = self.unlocked.write();
@ -550,6 +562,16 @@ impl AccountProvider {
}
}
if self.unlock_keep_secret && unlock != Unlock::OneTime {
// verify password and get the secret
let secret = self.sstore.raw_secret(&account, &password)?;
self.unlocked_secrets.write().insert(account.clone(), secret);
} else {
// verify password by signing dump message
// result may be discarded
let _ = self.sstore.sign(&account, &password, &Default::default())?;
}
let data = AccountData {
unlock: unlock,
password: password,
@ -562,7 +584,7 @@ impl AccountProvider {
fn password(&self, account: &StoreAccountRef) -> Result<String, SignError> {
let mut unlocked = self.unlocked.write();
let data = unlocked.get(account).ok_or(SignError::NotUnlocked)?.clone();
if let Unlock::Temp = data.unlock {
if let Unlock::OneTime = data.unlock {
unlocked.remove(account).expect("data exists: so key must exist: qed");
}
if let Unlock::Timed(ref end) = data.unlock {
@ -581,7 +603,7 @@ impl AccountProvider {
/// Unlocks account temporarily (for one signing).
pub fn unlock_account_temporarily(&self, account: Address, password: String) -> Result<(), Error> {
self.unlock_account(account, password, Unlock::Temp)
self.unlock_account(account, password, Unlock::OneTime)
}
/// Unlocks account temporarily with a timeout.
@ -592,17 +614,25 @@ impl AccountProvider {
/// Checks if given account is unlocked
pub fn is_unlocked(&self, address: Address) -> bool {
let unlocked = self.unlocked.read();
let unlocked_secrets = self.unlocked_secrets.read();
self.sstore.account_ref(&address)
.map(|r| unlocked.get(&r).is_some())
.map(|r| unlocked.get(&r).is_some() || unlocked_secrets.get(&r).is_some())
.unwrap_or(false)
}
/// Signs the message. If password is not provided the account must be unlocked.
pub fn sign(&self, address: Address, password: Option<String>, message: Message) -> Result<Signature, SignError> {
let account = self.sstore.account_ref(&address)?;
match self.unlocked_secrets.read().get(&account) {
Some(secret) => {
Ok(self.sstore.sign_with_secret(&secret, &message)?)
},
None => {
let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?;
Ok(self.sstore.sign(&account, &password, &message)?)
}
}
}
/// Signs message using the derived secret. If password is not provided the account must be unlocked.
pub fn sign_derived(&self, address: &Address, password: Option<String>, derivation: Derivation, message: Message)

View File

@ -25,3 +25,4 @@ pub use self::crypto::Crypto;
pub use self::kdf::{Kdf, Pbkdf2, Scrypt, Prf};
pub use self::safe_account::SafeAccount;
pub use self::version::Version;

View File

@ -26,7 +26,7 @@ use dir::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError};
use account::SafeAccount;
use presale::PresaleWallet;
use json::{self, Uuid, OpaqueKeyFile};
use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation};
use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation, OpaqueSecret};
/// Accounts store.
pub struct EthStore {
@ -140,6 +140,10 @@ impl SimpleSecretStore for EthStore {
}
impl SecretStore for EthStore {
fn raw_secret(&self, account: &StoreAccountRef, password: &str) -> Result<OpaqueSecret, Error> {
Ok(OpaqueSecret(self.get(account)?.crypto.secret(password)?))
}
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error> {
let json_wallet = json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned()))?;
let wallet = PresaleWallet::from(json_wallet);

View File

@ -66,3 +66,6 @@ pub use self::secret_store::{
};
pub use self::random::random_string;
pub use self::parity_wordlist::random_phrase;
/// An opaque wrapper for secret.
pub struct OpaqueSecret(::ethkey::Secret);

View File

@ -20,6 +20,7 @@ use ethkey::{Address, Message, Signature, Secret, Public};
use Error;
use json::{Uuid, OpaqueKeyFile};
use bigint::hash::H256;
use OpaqueSecret;
/// Key directory reference
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
@ -88,6 +89,15 @@ pub trait SimpleSecretStore: Send + Sync {
/// Secret Store API
pub trait SecretStore: SimpleSecretStore {
/// Returns a raw opaque Secret that can be later used to sign a message.
fn raw_secret(&self, account: &StoreAccountRef, password: &str) -> Result<OpaqueSecret, Error>;
/// Signs a message with raw secret.
fn sign_with_secret(&self, secret: &OpaqueSecret, message: &Message) -> Result<Signature, Error> {
Ok(::ethkey::sign(&secret.0, message)?)
}
/// Imports presale wallet
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error>;
/// Imports existing JSON wallet

View File

@ -114,6 +114,8 @@ usage! {
or |c: &Config| otry!(c.account).keys_iterations.clone(),
flag_no_hardware_wallets: bool = false,
or |c: &Config| otry!(c.account).disable_hardware.clone(),
flag_fast_unlock: bool = false,
or |c: &Config| otry!(c.account).fast_unlock.clone(),
flag_force_ui: bool = false,
@ -424,6 +426,7 @@ struct Account {
password: Option<Vec<String>>,
keys_iterations: Option<u32>,
disable_hardware: Option<bool>,
fast_unlock: Option<bool>,
}
#[derive(Default, Debug, PartialEq, RustcDecodable)]
@ -707,6 +710,7 @@ mod tests {
flag_password: vec!["~/.safe/password.file".into()],
flag_keys_iterations: 10240u32,
flag_no_hardware_wallets: false,
flag_fast_unlock: false,
flag_force_ui: false,
flag_no_ui: false,
@ -927,6 +931,7 @@ mod tests {
password: Some(vec!["passwdfile path".into()]),
keys_iterations: None,
disable_hardware: None,
fast_unlock: None,
}),
ui: Some(Ui {
force: None,

View File

@ -99,6 +99,9 @@ Account Options:
deriving key from the password (bigger is more
secure) (default: {flag_keys_iterations}).
--no-hardware-wallets Disables hardware wallet support. (default: {flag_no_hardware_wallets})
--fast-unlock Use drasticly faster unlocking mode. This setting causes
raw secrets to be stored unprotected in memory,
so use with care. (default: {flag_fast_unlock})
UI Options:
--force-ui Enable Trusted UI WebSocket endpoint,

View File

@ -497,6 +497,7 @@ impl Configuration {
password_files: self.args.flag_password.clone(),
unlocked_accounts: to_addresses(&self.args.flag_unlock)?,
enable_hardware_wallets: !self.args.flag_no_hardware_wallets,
enable_fast_unlock: self.args.flag_fast_unlock,
};
Ok(cfg)

View File

@ -180,6 +180,7 @@ pub struct AccountsConfig {
pub password_files: Vec<String>,
pub unlocked_accounts: Vec<Address>,
pub enable_hardware_wallets: bool,
pub enable_fast_unlock: bool,
}
impl Default for AccountsConfig {
@ -190,6 +191,7 @@ impl Default for AccountsConfig {
password_files: Vec::new(),
unlocked_accounts: Vec::new(),
enable_hardware_wallets: true,
enable_fast_unlock: false,
}
}
}

View File

@ -777,6 +777,7 @@ fn prepare_account_provider(spec: &SpecType, dirs: &Directories, data_dir: &str,
let account_settings = AccountProviderSettings {
enable_hardware_wallets: cfg.enable_hardware_wallets,
hardware_wallet_classic_key: spec == &SpecType::Classic,
unlock_keep_secret: cfg.enable_fast_unlock,
blacklisted_accounts: match *spec {
SpecType::Morden | SpecType::Ropsten | SpecType::Kovan | SpecType::Dev => vec![],
_ => vec![