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

View File

@ -26,7 +26,7 @@ use dir::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError};
use account::SafeAccount; use account::SafeAccount;
use presale::PresaleWallet; use presale::PresaleWallet;
use json::{self, Uuid, OpaqueKeyFile}; use json::{self, Uuid, OpaqueKeyFile};
use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation}; use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation, OpaqueSecret};
/// Accounts store. /// Accounts store.
pub struct EthStore { pub struct EthStore {
@ -140,6 +140,10 @@ impl SimpleSecretStore for EthStore {
} }
impl SecretStore 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> { 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 json_wallet = 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);

View File

@ -66,3 +66,6 @@ pub use self::secret_store::{
}; };
pub use self::random::random_string; pub use self::random::random_string;
pub use self::parity_wordlist::random_phrase; 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 Error;
use json::{Uuid, OpaqueKeyFile}; use json::{Uuid, OpaqueKeyFile};
use bigint::hash::H256; use bigint::hash::H256;
use OpaqueSecret;
/// Key directory reference /// Key directory reference
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
@ -88,6 +89,15 @@ pub trait SimpleSecretStore: Send + Sync {
/// Secret Store API /// Secret Store API
pub trait SecretStore: SimpleSecretStore { 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 /// Imports presale wallet
fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error>; fn import_presale(&self, vault: SecretVaultRef, json: &[u8], password: &str) -> Result<StoreAccountRef, Error>;
/// Imports existing JSON wallet /// Imports existing JSON wallet

View File

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

View File

@ -99,6 +99,9 @@ Account Options:
deriving key from the password (bigger is more deriving key from the password (bigger is more
secure) (default: {flag_keys_iterations}). secure) (default: {flag_keys_iterations}).
--no-hardware-wallets Disables hardware wallet support. (default: {flag_no_hardware_wallets}) --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: UI Options:
--force-ui Enable Trusted UI WebSocket endpoint, --force-ui Enable Trusted UI WebSocket endpoint,

View File

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

View File

@ -180,6 +180,7 @@ pub struct AccountsConfig {
pub password_files: Vec<String>, pub password_files: Vec<String>,
pub unlocked_accounts: Vec<Address>, pub unlocked_accounts: Vec<Address>,
pub enable_hardware_wallets: bool, pub enable_hardware_wallets: bool,
pub enable_fast_unlock: bool,
} }
impl Default for AccountsConfig { impl Default for AccountsConfig {
@ -190,6 +191,7 @@ impl Default for AccountsConfig {
password_files: Vec::new(), password_files: Vec::new(),
unlocked_accounts: Vec::new(), unlocked_accounts: Vec::new(),
enable_hardware_wallets: true, 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 { let account_settings = AccountProviderSettings {
enable_hardware_wallets: cfg.enable_hardware_wallets, enable_hardware_wallets: cfg.enable_hardware_wallets,
hardware_wallet_classic_key: spec == &SpecType::Classic, hardware_wallet_classic_key: spec == &SpecType::Classic,
unlock_keep_secret: cfg.enable_fast_unlock,
blacklisted_accounts: match *spec { blacklisted_accounts: match *spec {
SpecType::Morden | SpecType::Ropsten | SpecType::Kovan | SpecType::Dev => vec![], SpecType::Morden | SpecType::Ropsten | SpecType::Kovan | SpecType::Dev => vec![],
_ => vec![ _ => vec![