Merge pull request #5778 from paritytech/unlock
A CLI flag to allow fast transaction signing when account is unlocked.
This commit is contained in:
commit
8d04dffe69
@ -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,16 +614,24 @@ 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)?;
|
||||
let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?;
|
||||
Ok(self.sstore.sign(&account, &password, &message)?)
|
||||
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.
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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![
|
||||
|
Loading…
Reference in New Issue
Block a user