diff --git a/Cargo.lock b/Cargo.lock index 8bf4de9e6..2ac272e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,7 +180,7 @@ version = "1.1.1" source = "git+https://github.com/ethcore/rust-ctrlc.git#f4927770f89eca80ec250911eea3adcbf579ac48" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -189,7 +189,7 @@ name = "daemonize" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -231,7 +231,7 @@ source = "git+https://github.com/ethcore/rust-secp256k1#b6fdd43bbcf6d46adb72a92d dependencies = [ "arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -261,8 +261,9 @@ dependencies = [ "ethcore-ipc-codegen 1.2.0", "ethcore-util 1.2.0", "ethjson 0.1.0", + "ethstore 0.1.0", "heapsize 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -390,8 +391,8 @@ dependencies = [ "igd 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", "json-tests 0.1.0", - "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.5.1 (git+https://github.com/ethcore/mio?branch=v0.5.x)", "nix 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -421,6 +422,35 @@ dependencies = [ "syntex 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ethkey" +version = "0.2.0" +dependencies = [ + "docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)", + "eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)", + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tiny-keccak 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ethstore" +version = "0.1.0" +dependencies = [ + "docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)", + "ethkey 0.2.0", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_codegen 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "syntex 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tiny-keccak 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ethsync" version = "1.2.0" @@ -439,7 +469,7 @@ dependencies = [ name = "fdlimit" version = "0.1.0" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -570,12 +600,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "json-ipc-server" version = "0.2.3" -source = "git+https://github.com/ethcore/json-ipc-server.git#b224bdbcb53cab349c278484bd3e5e22390167c9" +source = "git+https://github.com/ethcore/json-ipc-server.git#bfe16b66b2e9412d153b1ea53bc078d74037da7f" dependencies = [ "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "miow 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -632,12 +662,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "lazy_static" -version = "0.1.16" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libc" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -655,7 +685,7 @@ name = "memchr" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -692,7 +722,7 @@ version = "0.5.0" source = "git+https://github.com/carllerche/mio.git#f4aa49a9d2c4507fb33a4533d5238e0365f67c99" dependencies = [ "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "miow 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", @@ -708,7 +738,7 @@ version = "0.5.1" source = "git+https://github.com/ethcore/mio?branch=v0.5.x#1fc881771fb8c2517317b4f805d7b88235be422b" dependencies = [ "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "miow 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", @@ -724,7 +754,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "miow 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", @@ -750,7 +780,7 @@ name = "nanomsg" version = "0.5.1" source = "git+https://github.com/ethcore/nanomsg.rs.git#c40fe442c9afaea5b38009a3d992ca044dcceb00" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "nanomsg-sys 0.5.0 (git+https://github.com/ethcore/nanomsg.rs.git)", ] @@ -760,7 +790,7 @@ version = "0.5.0" source = "git+https://github.com/ethcore/nanomsg.rs.git#c40fe442c9afaea5b38009a3d992ca044dcceb00" dependencies = [ "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -770,7 +800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -781,7 +811,7 @@ version = "0.5.0-pre" source = "git+https://github.com/carllerche/nix-rust?rev=c4257f8a76#c4257f8a76b69b0d2e9a001d83e4bef67c03b23f" dependencies = [ "bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -790,7 +820,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -877,7 +907,7 @@ name = "num_cpus" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1044,7 +1074,7 @@ name = "rand" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1079,7 +1109,7 @@ name = "rocksdb" version = "0.4.5" source = "git+https://github.com/ethcore/rust-rocksdb#9140e37ce0fdb748097f85653c01b0f7e3736ea9" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)", ] @@ -1089,7 +1119,7 @@ version = "0.3.0" source = "git+https://github.com/ethcore/rust-rocksdb#9140e37ce0fdb748097f85653c01b0f7e3736ea9" dependencies = [ "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1110,7 +1140,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1121,7 +1151,7 @@ version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1245,7 +1275,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1271,7 +1301,7 @@ name = "termios" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1280,7 +1310,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1297,7 +1327,7 @@ version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index 889d759ae..294ad82c4 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -24,12 +24,13 @@ ethash = { path = "../ethash" } num_cpus = "0.2" clippy = { version = "0.0.76", optional = true} crossbeam = "0.2.9" -lazy_static = "0.1" +lazy_static = "0.2" ethcore-devtools = { path = "../devtools" } ethjson = { path = "../json" } bloomchain = "0.1" "ethcore-ipc" = { path = "../ipc/rpc" } rayon = "0.3.1" +ethstore = { path = "../ethstore" } [features] jit = ["evmjit"] diff --git a/ethcore/src/account_provider.rs b/ethcore/src/account_provider.rs new file mode 100644 index 000000000..3c79cfd52 --- /dev/null +++ b/ethcore/src/account_provider.rs @@ -0,0 +1,266 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! Account management. + +use std::fmt; +use std::sync::RwLock; +use std::collections::HashMap; +use util::{Address as H160, H256, H520}; +use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore}; +use ethstore::dir::{KeyDirectory}; +use ethstore::ethkey::{Address as SSAddress, Message as SSMessage, Secret as SSSecret, Random, Generator}; + +/// Type of unlock. +#[derive(Clone)] +enum Unlock { + /// If account is unlocked temporarily, it should be locked after first usage. + Temp, + /// Account unlocked permantently can always sign message. + /// Use with caution. + Perm, +} + +/// Data associated with account. +#[derive(Clone)] +struct AccountData { + unlock: Unlock, + password: String, +} + +/// `AccountProvider` errors. +#[derive(Debug)] +pub enum Error { + /// Returned when account is not unlocked. + NotUnlocked, + /// Returned when signing fails. + SStore(SSError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Error::NotUnlocked => write!(f, "Account is locked"), + Error::SStore(ref e) => write!(f, "{}", e), + } + } +} + +impl From for Error { + fn from(e: SSError) -> Self { + Error::SStore(e) + } +} + +macro_rules! impl_bridge_type { + ($name: ident, $size: expr, $core: ident, $store: ident) => { + /// Primitive + pub struct $name([u8; $size]); + + impl From<[u8; $size]> for $name { + fn from(s: [u8; $size]) -> Self { + $name(s) + } + } + + impl From<$core> for $name { + fn from(s: $core) -> Self { + $name(s.0) + } + } + + impl From<$store> for $name { + fn from(s: $store) -> Self { + $name(s.into()) + } + } + + impl Into<$core> for $name { + fn into(self) -> $core { + $core(self.0) + } + } + + impl Into<$store> for $name { + fn into(self) -> $store { + $store::from(self.0) + } + } + } +} + +impl_bridge_type!(Secret, 32, H256, SSSecret); +impl_bridge_type!(Message, 32, H256, SSMessage); +impl_bridge_type!(Address, 20, H160, SSAddress); + + +struct NullDir; + +impl KeyDirectory for NullDir { + fn load(&self) -> Result, SSError> { + Ok(vec![]) + } + + fn insert(&self, _account: SafeAccount) -> Result<(), SSError> { + Ok(()) + } + + fn remove(&self, _address: &SSAddress) -> Result<(), SSError> { + Ok(()) + } +} + +/// Account management. +/// Responsible for unlocking accounts. +pub struct AccountProvider { + unlocked: RwLock>, + sstore: Box, +} + +impl AccountProvider { + /// Creates new account provider. + pub fn new(sstore: Box) -> Self { + AccountProvider { + unlocked: RwLock::new(HashMap::new()), + sstore: sstore, + } + } + + /// Creates not disk backed provider. + pub fn transient_provider() -> Self { + AccountProvider { + unlocked: RwLock::new(HashMap::new()), + sstore: Box::new(EthStore::open(Box::new(NullDir)).unwrap()) + } + } + + /// Creates new random account. + pub fn new_account(&self, password: &str) -> Result { + let secret = Random.generate().unwrap().secret().clone(); + let address = try!(self.sstore.insert_account(secret, password)); + Ok(Address::from(address).into()) + } + + /// Inserts new account into underlying store. + /// Does not unlock account! + pub fn insert_account(&self, secret: S, password: &str) -> Result where Secret: From { + let s = Secret::from(secret); + let address = try!(self.sstore.insert_account(s.into(), password)); + Ok(Address::from(address).into()) + } + + /// Returns addresses of all accounts. + pub fn accounts(&self) -> Vec { + self.sstore.accounts().into_iter().map(|a| H160(a.into())).collect() + } + + /// Helper method used for unlocking accounts. + fn unlock_account(&self, account: A, password: String, unlock: Unlock) -> Result<(), Error> where Address: From { + let a = Address::from(account); + let account = a.into(); + // verify password by signing dump message + // result may be discarded + let _ = try!(self.sstore.sign(&account, &password, &Default::default())); + + // check if account is already unlocked pernamently, if it is, do nothing + { + let unlocked = self.unlocked.read().unwrap(); + if let Some(data) = unlocked.get(&account) { + if let Unlock::Perm = data.unlock { + return Ok(()) + } + } + } + + let data = AccountData { + unlock: unlock, + password: password, + }; + + let mut unlocked = self.unlocked.write().unwrap(); + unlocked.insert(account, data); + Ok(()) + } + + /// Unlocks account permanently. + pub fn unlock_account_permanently(&self, account: A, password: String) -> Result<(), Error> where Address: From { + self.unlock_account(account, password, Unlock::Perm) + } + + /// Unlocks account temporarily (for one signing). + pub fn unlock_account_temporarily(&self, account: A, password: String) -> Result<(), Error> where Address: From { + self.unlock_account(account, password, Unlock::Temp) + } + + /// Signs the message. Account must be unlocked. + pub fn sign(&self, account: A, message: M) -> Result where Address: From, Message: From { + let account = Address::from(account).into(); + let message = Message::from(message).into(); + + let data = { + let unlocked = self.unlocked.read().unwrap(); + try!(unlocked.get(&account).ok_or(Error::NotUnlocked)).clone() + }; + + if let Unlock::Temp = data.unlock { + let mut unlocked = self.unlocked.write().unwrap(); + unlocked.remove(&account).expect("data exists: so key must exist: qed"); + } + + let signature = try!(self.sstore.sign(&account, &data.password, &message)); + Ok(H520(signature.into())) + } + + /// Unlocks an account, signs the message, and locks it again. + pub fn sign_with_password(&self, account: A, password: String, message: M) -> Result where Address: From, Message: From { + let account = Address::from(account).into(); + let message = Message::from(message).into(); + let signature = try!(self.sstore.sign(&account, &password, &message)); + Ok(H520(signature.into())) + } +} + +#[cfg(test)] +mod tests { + use super::AccountProvider; + use ethstore::SecretStore; + use ethstore::ethkey::{Generator, Random}; + + #[test] + fn unlock_account_temp() { + let kp = Random.generate().unwrap(); + let ap = AccountProvider::transient_provider(); + assert!(ap.insert_account(kp.secret().clone(), "test").is_ok()); + assert!(ap.unlock_account_temporarily(kp.address(), "test1".into()).is_err()); + assert!(ap.unlock_account_temporarily(kp.address(), "test".into()).is_ok()); + assert!(ap.sign(kp.address(), [0u8; 32]).is_ok()); + assert!(ap.sign(kp.address(), [0u8; 32]).is_err()); + } + + #[test] + fn unlock_account_perm() { + let kp = Random.generate().unwrap(); + let ap = AccountProvider::transient_provider(); + assert!(ap.insert_account(kp.secret().clone(), "test").is_ok()); + assert!(ap.unlock_account_permanently(kp.address(), "test1".into()).is_err()); + assert!(ap.unlock_account_permanently(kp.address(), "test".into()).is_ok()); + assert!(ap.sign(kp.address(), [0u8; 32]).is_ok()); + assert!(ap.sign(kp.address(), [0u8; 32]).is_ok()); + assert!(ap.unlock_account_temporarily(kp.address(), "test".into()).is_ok()); + assert!(ap.sign(kp.address(), [0u8; 32]).is_ok()); + assert!(ap.sign(kp.address(), [0u8; 32]).is_ok()); + } +} diff --git a/ethcore/src/basic_authority.rs b/ethcore/src/basic_authority.rs index c732ce711..e63f229cf 100644 --- a/ethcore/src/basic_authority.rs +++ b/ethcore/src/basic_authority.rs @@ -17,7 +17,7 @@ //! A blockchain engine that supports a basic, non-BFT proof-of-authority. use common::*; -use util::keys::store::AccountProvider; +use account_provider::AccountProvider; use block::*; use spec::{CommonParams, Spec}; use engine::*; @@ -105,15 +105,13 @@ impl Engine for BasicAuthority { /// be returned. fn generate_seal(&self, block: &ExecutedBlock, accounts: Option<&AccountProvider>) -> Option> { if let Some(ap) = accounts { - // check to see if author is contained in self.our_params.authorities - if self.our_params.authorities.contains(block.header().author()) { - if let Ok(secret) = ap.account_secret(block.header().author()) { - return Some(block.header().author_seal(&secret)); - } else { - trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable"); - } + let header = block.header(); + let message = header.bare_hash(); + // account should be pernamently unlocked, otherwise sealing will fail + if let Ok(signature) = ap.sign(*block.header().author(), message) { + return Some(vec![encode(&signature).to_vec()]); } else { - trace!(target: "basicauthority", "generate_seal: FAIL: block author {} isn't one of the authorized accounts {:?}", block.header().author(), self.our_params.authorities); + trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable"); } } else { trace!(target: "basicauthority", "generate_seal: FAIL: accounts not provided"); @@ -176,16 +174,6 @@ impl Header { pub fn signature(&self) -> H520 { decode(&self.seal()[0]) } - - /// Generate a seal for the block with the given `secret`. - pub fn author_seal(&self, secret: &Secret) -> Vec { - vec![encode(&ec::sign(secret, &self.bare_hash()).unwrap_or(Signature::new())).to_vec()] - } - - /// Set the nonce and mix hash fields of the header. - pub fn sign(&mut self, secret: &Secret) { - self.seal = self.author_seal(secret); - } } /// Create a new test chain spec with `BasicAuthority` consensus engine. @@ -197,7 +185,7 @@ mod tests { use common::*; use block::*; use tests::helpers::*; - use util::keys::{TestAccountProvider, TestAccount}; + use account_provider::AccountProvider; #[test] fn has_valid_metadata() { @@ -251,24 +239,11 @@ mod tests { } } - #[test] - fn can_do_signature_verification() { - let secret = "".sha3(); - let addr = KeyPair::from_secret("".sha3()).unwrap().address(); - - let engine = new_test_authority().engine; - let mut header: Header = Header::default(); - header.set_author(addr); - header.sign(&secret); - - assert!(engine.verify_block_unordered(&header, None).is_ok()); - } - #[test] fn can_generate_seal() { - let addr = KeyPair::from_secret("".sha3()).unwrap().address(); - let accounts = hash_map![addr => TestAccount{unlocked: true, password: Default::default(), secret: "".sha3()}]; - let tap = TestAccountProvider::new(accounts); + let tap = AccountProvider::transient_provider(); + let addr = tap.insert_account("".sha3(), "").unwrap(); + tap.unlock_account_permanently(addr, "".into()).unwrap(); let spec = new_test_authority(); let engine = &spec.engine; @@ -278,10 +253,9 @@ mod tests { spec.ensure_db_good(db.as_hashdb_mut()); let last_hashes = vec![genesis_header.hash()]; let vm_factory = Default::default(); - let b = OpenBlock::new(engine.deref(), &vm_factory, false, db, &genesis_header, last_hashes, addr.clone(), 3141562.into(), vec![]).unwrap(); + let b = OpenBlock::new(engine.deref(), &vm_factory, false, db, &genesis_header, last_hashes, addr, 3141562.into(), vec![]).unwrap(); let b = b.close_and_lock(); let seal = engine.generate_seal(b.block(), Some(&tap)).unwrap(); - assert!(b.try_seal(engine.deref(), seal).is_ok()); } } diff --git a/ethcore/src/engine.rs b/ethcore/src/engine.rs index ed482db89..98ebac40e 100644 --- a/ethcore/src/engine.rs +++ b/ethcore/src/engine.rs @@ -17,7 +17,7 @@ //! Consensus engine specification use common::*; -use util::keys::store::AccountProvider; +use account_provider::AccountProvider; use block::ExecutedBlock; use spec::CommonParams; use evm::Schedule; diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index f69048a95..9919ec62a 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -91,10 +91,12 @@ extern crate ethjson; extern crate bloomchain; #[macro_use] extern crate ethcore_ipc as ipc; extern crate rayon; +pub extern crate ethstore; #[cfg(test)] extern crate ethcore_devtools as devtools; #[cfg(feature = "jit" )] extern crate evmjit; +pub mod account_provider; pub mod basic_authority; pub mod block; pub mod block_queue; @@ -140,4 +142,4 @@ mod json_tests; pub use types::*; pub use evm::get_info; -pub use executive::contract_address; \ No newline at end of file +pub use executive::contract_address; diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index c5dff80d0..735ad5cf4 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -18,7 +18,7 @@ use rayon::prelude::*; use std::sync::atomic::AtomicBool; use util::*; -use util::keys::store::{AccountProvider}; +use account_provider::AccountProvider; use views::{BlockView, HeaderView}; use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics}; use block::{ClosedBlock, IsBlock}; diff --git a/ethcore/src/types/transaction.rs b/ethcore/src/types/transaction.rs index a6b4c1781..a8e1c66e4 100644 --- a/ethcore/src/types/transaction.rs +++ b/ethcore/src/types/transaction.rs @@ -140,8 +140,13 @@ impl Transaction { /// Signs the transaction as coming from `sender`. pub fn sign(self, secret: &Secret) -> SignedTransaction { - let sig = ec::sign(secret, &self.hash()); - let (r, s, v) = sig.unwrap().to_rsv(); + let sig = ec::sign(secret, &self.hash()).unwrap(); + self.with_signature(sig) + } + + /// Signs the transaction with signature. + pub fn with_signature(self, sig: H520) -> SignedTransaction { + let (r, s, v) = sig.to_rsv(); SignedTransaction { unsigned: self, r: r, diff --git a/ethkey/.gitignore b/ethkey/.gitignore new file mode 100644 index 000000000..c6262ea6a --- /dev/null +++ b/ethkey/.gitignore @@ -0,0 +1,2 @@ +target +*.swp diff --git a/ethkey/.travis.yml b/ethkey/.travis.yml new file mode 100644 index 000000000..e1d6f9b13 --- /dev/null +++ b/ethkey/.travis.yml @@ -0,0 +1,23 @@ +sudo: false +language: rust +branches: + only: + - master +matrix: + fast_finish: false + include: + - rust: stable + - rust: beta + - rust: nightly +after_success: | + [ $TRAVIS_BRANCH = master ] && + [ $TRAVIS_PULL_REQUEST = false ] && + [ $TRAVIS_RUST_VERSION = stable ] && + cargo doc --no-deps --verbose && + echo '' > target/doc/index.html && + pip install --user ghp-import && + /home/travis/.local/bin/ghp-import -n target/doc && + git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages +env: + global: + - secure: LBkFAH5fAhzHRP7kYQZnOCavOuPS2vEDv49KGfTsY/7MmW0De4c0sz0a3/0IdFqqIMLYLV2uMO86S0p6FBaq6/GIdobLsGZZ3cFReYFI+vb8sylYF/+D/aQ/UOjpEOD8HP6G3YmV5buSyL8uiPlmYbqwBAe4z6ELEbh/16gRuIqQLYQtpYPxMCD3tZzSux81b45K2khETZ7E+ap3LUG3rFTXxjEgx9leIZlVY+Qk4U5D9gFnJnjmxDPyIqzn2dORnw5jcpp3eSUEvSvSgjz4TAVg7Gw789jDl2dyr26U1wp1E5bB9AqZVYOb4l8vcQ6QiHrCvu7Wgl32O6XYkwMjDaDUB68bm5MTsUrwDWgKGx4xeurIBil5doHFlCGZ98RrzPxdgoCd6hCI459dA8jEwdXAfOkZ80RycZlryHCwn68x3dlnJoqVyg8viYo6H6G0GdH/dIhuwbnLDdWZsODehN8eJEy9KKQ4tPp+PjBcgKm1Wz5MzKFSIwfFInic7hjTVXGozHSvgvXJE0BI2bPbjVNCdZa5kGAAUAhBNXyTn7PbC7hYbmwAalzaOIjoYcdQLmUEz2J2gSOK8xW2gMU0Z2I+IylA0oh8xB/r2Q5sqLHT3LPLdzoETsyzaQjWFcFdXdsbbcG59DnFC9s2Jq7KqeODp6EJG4cw0ofKpBuDRes= diff --git a/ethkey/Cargo.toml b/ethkey/Cargo.toml new file mode 100644 index 000000000..08461e865 --- /dev/null +++ b/ethkey/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ethkey" +version = "0.2.0" +authors = ["debris "] + +[dependencies] +rand = "0.3.14" +lazy_static = "0.2.1" +tiny-keccak = "1.0" +eth-secp256k1 = { git = "https://github.com/ethcore/rust-secp256k1" } +rustc-serialize = "0.3" +docopt = { version = "0.6", optional = true } + +[features] +default = ["cli"] +cli = ["docopt"] + +[[bin]] +name = "ethkey" +path = "src/bin/main.rs" +doc = false diff --git a/ethkey/README.md b/ethkey/README.md new file mode 100644 index 000000000..1a588cd48 --- /dev/null +++ b/ethkey/README.md @@ -0,0 +1,150 @@ +# ethkey + +[![Build Status][travis-image]][travis-url] + +[travis-image]: https://travis-ci.org/ethcore/ethkey.svg?branch=master +[travis-url]: https://travis-ci.org/ethcore/ethkey + +Ethereum keys generator. + +[Documentation](http://ethcore.github.io/ethkey/ethkey/index.html) + +### Usage + +``` +Ethereum keys generator. + Copyright 2016 Ethcore (UK) Limited + +Usage: + ethkey info [options] + ethkey generate random [options] + ethkey generate prefix [options] + ethkey generate brain [options] + ethkey sign + ethkey verify + ethkey [-h | --help] + +Options: + -h, --help Display this message and exit. + -s, --secret Display only the secret. + -p, --public Display only the public. + -a, --address Display only the address. + +Commands: + info Display public and address of the secret. + generate Generates new ethereum key. + random Random generation. + prefix Random generation, but address must start with a prefix + brain Generate new key from string seed. + sign Sign message using secret. + verify Verify signer of the signature. +``` + +### Examples + +#### `info ` +*Display info about private key.* + +- `` - ethereum secret, 32 bytes long + +``` +ethkey info 17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55 +``` + +``` +secret: 17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55 +public: 689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124 +address: 26d1ec50b4e62c1d1a40d16e7cacc6a6580757d5 +``` + +-- + +#### `generate brain ` +*Generate new brain-wallet keypair using 16384 iterations.* + +- `` - brain-wallet seed, any string + + +``` +ethkey generate brain "this is sparta" +``` + +``` +secret: 17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55 +public: 689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124 +address: 26d1ec50b4e62c1d1a40d16e7cacc6a6580757d5 +``` + +-- + +#### `generate random` +*Generate new keypair randomly.* + +``` +ethkey generate random +``` + +``` +secret: 7d29fab185a33e2cd955812397354c472d2b84615b645aa135ff539f6b0d70d5 +public: 35f222d88b80151857a2877826d940104887376a94c1cbd2c8c7c192eb701df88a18a4ecb8b05b1466c5b3706042027b5e079fe3a3683e66d822b0e047aa3418 +address: a8fa5dd30a87bb9e3288d604eb74949c515ab66e +``` + +-- + +#### `generate prefix ` +*Generate new keypair randomly with address starting with prefix.* + +- `` - desired address prefix, 0 - 32 bytes long. +- `` - maximum number of tries before generation is assumed to be a failure. + +``` +ethkey generate prefix ff 1000 +``` + +``` +secret: 2075b1d9c124ea673de7273758ed6de14802a9da8a73ceb74533d7c312ff6acd +public: 48dbce4508566a05509980a5dd1335599fcdac6f9858ba67018cecb9f09b8c4066dc4c18ae2722112fd4d9ac36d626793fffffb26071dfeb0c2300df994bd173 +address: fff7e25dff2aa60f61f9d98130c8646a01f31649 +``` + +-- + +#### `sign ` +*Sign a message with a secret.* + +- `` - ethereum secret, 32 bytes long +- `` - message to sign, 32 bytes long + +``` +ethkey sign 17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55 bd50b7370c3f96733b31744c6c45079e7ae6c8d299613246d28ebcef507ec987 +``` + +``` +c1878cf60417151c766a712653d26ef350c8c75393458b7a9be715f053215af63dfd3b02c2ae65a8677917a8efa3172acb71cb90196e42106953ea0363c5aaf200 +``` + +-- + +#### `verify ` +*Verify the signature.* + +- `` - ethereum public, 64 bytes long +- `` - message signature, 65 bytes long +- `` - message, 32 bytes long + +``` +ethkey verify 689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124 c1878cf60417151c766a712653d26ef350c8c75393458b7a9be715f053215af63dfd3b02c2ae65a8677917a8efa3172acb71cb90196e42106953ea0363c5aaf200 bd50b7370c3f96733b31744c6c45079e7ae6c8d299613246d28ebcef507ec987 +``` + +``` +true +``` + + +# Ethcore toolchain +*this project is a part of the ethcore toolchain* + +- [**ethkey**](https://github.com/ethcore/ethkey) - Ethereum keys generator and signer. +- [**ethstore**](https://github.com/ethcore/ethstore) - Ethereum key management. +- [**ethabi**](https://github.com/ethcore/ethabi) - Ethereum function calls encoding. diff --git a/ethkey/src/bin/ethkey.rs b/ethkey/src/bin/ethkey.rs new file mode 100644 index 000000000..a7661c42f --- /dev/null +++ b/ethkey/src/bin/ethkey.rs @@ -0,0 +1,274 @@ +extern crate docopt; +extern crate rustc_serialize; +extern crate ethkey; + +use std::str::FromStr; +use std::{env, fmt, process}; +use std::num::ParseIntError; +use docopt::Docopt; +use rustc_serialize::hex::{FromHex, FromHexError}; +use ethkey::{KeyPair, Random, Brain, Prefix, Error as EthkeyError, Generator, Secret, Message, Public, Signature, sign, verify}; + +pub const USAGE: &'static str = r#" +Ethereum keys generator. + Copyright 2016 Ethcore (UK) Limited + +Usage: + ethkey info [options] + ethkey generate random [options] + ethkey generate prefix [options] + ethkey generate brain [options] + ethkey sign + ethkey verify + ethkey [-h | --help] + +Options: + -h, --help Display this message and exit. + -s, --secret Display only the secret. + -p, --public Display only the public. + -a, --address Display only the address. + +Commands: + info Display public and address of the secret. + generate Generates new ethereum key. + random Random generation. + prefix Random generation, but address must start with a prefix + brain Generate new key from string seed. + sign Sign message using secret. + verify Verify signer of the signature. +"#; + +#[derive(Debug, RustcDecodable)] +struct Args { + cmd_info: bool, + cmd_generate: bool, + cmd_random: bool, + cmd_prefix: bool, + cmd_brain: bool, + cmd_sign: bool, + cmd_verify: bool, + arg_prefix: String, + arg_iterations: String, + arg_seed: String, + arg_secret: String, + arg_message: String, + arg_public: String, + arg_signature: String, + flag_secret: bool, + flag_public: bool, + flag_address: bool, +} + +#[derive(Debug)] +enum Error { + Ethkey(EthkeyError), + FromHex(FromHexError), + ParseInt(ParseIntError), +} + +impl From for Error { + fn from(err: EthkeyError) -> Self { + Error::Ethkey(err) + } +} + +impl From for Error { + fn from(err: FromHexError) -> Self { + Error::FromHex(err) + } +} + +impl From for Error { + fn from(err: ParseIntError) -> Self { + Error::ParseInt(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Error::Ethkey(ref e) => write!(f, "{}", e), + Error::FromHex(ref e) => write!(f, "{}", e), + Error::ParseInt(ref e) => write!(f, "{}", e), + } + } +} + +enum DisplayMode { + KeyPair, + Secret, + Public, + Address, +} + +impl DisplayMode { + fn new(args: &Args) -> Self { + if args.flag_secret { + DisplayMode::Secret + } else if args.flag_public { + DisplayMode::Public + } else if args.flag_address { + DisplayMode::Address + } else { + DisplayMode::KeyPair + } + } +} + +fn main() { + match execute(env::args()) { + Ok(ok) => println!("{}", ok), + Err(err) => { + println!("{}", err); + process::exit(1); + }, + } +} + +fn display(keypair: KeyPair, mode: DisplayMode) -> String { + match mode { + DisplayMode::KeyPair => format!("{}", keypair), + DisplayMode::Secret => format!("{}", keypair.secret()), + DisplayMode::Public => format!("{}", keypair.public()), + DisplayMode::Address => format!("{}", keypair.address()), + } +} + +fn execute(command: I) -> Result where I: IntoIterator, S: AsRef { + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.argv(command).decode()) + .unwrap_or_else(|e| e.exit()); + + return if args.cmd_info { + let display_mode = DisplayMode::new(&args); + let secret = try!(Secret::from_str(&args.arg_secret)); + let keypair = try!(KeyPair::from_secret(secret)); + Ok(display(keypair, display_mode)) + } else if args.cmd_generate { + let display_mode = DisplayMode::new(&args); + let keypair = if args.cmd_random { + Random.generate() + } else if args.cmd_prefix { + let prefix = try!(args.arg_prefix.from_hex()); + let iterations = try!(usize::from_str_radix(&args.arg_iterations, 10)); + Prefix::new(prefix, iterations).generate() + } else if args.cmd_brain { + Brain::new(args.arg_seed).generate() + } else { + unreachable!(); + }; + Ok(display(try!(keypair), display_mode)) + } else if args.cmd_sign { + let secret = try!(Secret::from_str(&args.arg_secret)); + let message = try!(Message::from_str(&args.arg_message)); + let signature = try!(sign(&secret, &message)); + Ok(format!("{}", signature)) + } else if args.cmd_verify { + let public = try!(Public::from_str(&args.arg_public)); + let signature = try!(Signature::from_str(&args.arg_signature)); + let message = try!(Message::from_str(&args.arg_message)); + let ok = try!(verify(&public, &signature, &message)); + Ok(format!("{}", ok)) + } else { + unreachable!(); + } +} + +#[cfg(test)] +mod tests { + use super::execute; + + #[test] + fn info() { + let command = vec!["ethkey", "info", "17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = +"secret: 17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55 +public: 689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124 +address: 26d1ec50b4e62c1d1a40d16e7cacc6a6580757d5".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn brain() { + let command = vec!["ethkey", "generate", "brain", "this is sparta"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = +"secret: 17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55 +public: 689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124 +address: 26d1ec50b4e62c1d1a40d16e7cacc6a6580757d5".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn secret() { + let command = vec!["ethkey", "generate", "brain", "this is sparta", "--secret"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = "17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn public() { + let command = vec!["ethkey", "generate", "brain", "this is sparta", "--public"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = "689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn address() { + let command = vec!["ethkey", "generate", "brain", "this is sparta", "--address"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = "26d1ec50b4e62c1d1a40d16e7cacc6a6580757d5".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn sign() { + let command = vec!["ethkey", "sign", "17d08f5fe8c77af811caa0c9a187e668ce3b74a99acc3f6d976f075fa8e0be55", "bd50b7370c3f96733b31744c6c45079e7ae6c8d299613246d28ebcef507ec987"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = "c1878cf60417151c766a712653d26ef350c8c75393458b7a9be715f053215af63dfd3b02c2ae65a8677917a8efa3172acb71cb90196e42106953ea0363c5aaf200".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn verify_valid() { + let command = vec!["ethkey", "verify", "689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124", "c1878cf60417151c766a712653d26ef350c8c75393458b7a9be715f053215af63dfd3b02c2ae65a8677917a8efa3172acb71cb90196e42106953ea0363c5aaf200", "bd50b7370c3f96733b31744c6c45079e7ae6c8d299613246d28ebcef507ec987"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = "true".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } + + #[test] + fn verify_invalid() { + let command = vec!["ethkey", "verify", "689268c0ff57a20cd299fa60d3fb374862aff565b20b5f1767906a99e6e09f3ff04ca2b2a5cd22f62941db103c0356df1a8ed20ce322cab2483db67685afd124", "c1878cf60417151c766a712653d26ef350c8c75393458b7a9be715f053215af63dfd3b02c2ae65a8677917a8efa3172acb71cb90196e42106953ea0363c5aaf200", "bd50b7370c3f96733b31744c6c45079e7ae6c8d299613246d28ebcef507ec986"] + .into_iter() + .map(Into::into) + .collect::>(); + + let expected = "false".to_owned(); + assert_eq!(execute(command).unwrap(), expected); + } +} diff --git a/ethkey/src/bin/main.rs b/ethkey/src/bin/main.rs new file mode 100644 index 000000000..404136b62 --- /dev/null +++ b/ethkey/src/bin/main.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "cli")] +include!("ethkey.rs"); + +#[cfg(not(feature = "cli"))] +fn main() {} diff --git a/ethkey/src/brain.rs b/ethkey/src/brain.rs new file mode 100644 index 000000000..c7fffe3bf --- /dev/null +++ b/ethkey/src/brain.rs @@ -0,0 +1,46 @@ +use keccak::Keccak256; +use super::{KeyPair, Error, Generator, Secret}; + +/// Simple brainwallet. +pub struct Brain(String); + +impl Brain { + pub fn new(s: String) -> Self { + Brain(s) + } +} + +impl Generator for Brain { + fn generate(self) -> Result { + let seed = self.0; + let mut secret = seed.bytes().collect::>().keccak256(); + + let mut i = 0; + loop { + secret = secret.keccak256(); + + match i > 16384 { + false => i += 1, + true => { + let result = KeyPair::from_secret(Secret::from(secret.clone())); + if result.is_ok() { + return result + } + }, + } + } + } +} + +#[cfg(test)] +mod tests { + use {Brain, Generator}; + + #[test] + fn test_brain() { + let words = "this is sparta!".to_owned(); + let first_keypair = Brain(words.clone()).generate().unwrap(); + let second_keypair = Brain(words.clone()).generate().unwrap(); + assert_eq!(first_keypair.secret(), second_keypair.secret()); + } +} diff --git a/ethkey/src/error.rs b/ethkey/src/error.rs new file mode 100644 index 000000000..f75f264f5 --- /dev/null +++ b/ethkey/src/error.rs @@ -0,0 +1,53 @@ +use std::fmt; + +#[derive(Debug)] +/// Crypto error +pub enum Error { + /// Invalid secret key + InvalidSecret, + /// Invalid public key + InvalidPublic, + /// Invalid address + InvalidAddress, + /// Invalid EC signature + InvalidSignature, + /// Invalid AES message + InvalidMessage, + /// IO Error + Io(::std::io::Error), + /// Custom + Custom(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let msg = match *self { + Error::InvalidSecret => "Invalid secret key".into(), + Error::InvalidPublic => "Invalid public key".into(), + Error::InvalidAddress => "Invalid address".into(), + Error::InvalidSignature => "Invalid EC signature".into(), + Error::InvalidMessage => "Invalid AES message".into(), + Error::Io(ref err) => format!("I/O error: {}", err), + Error::Custom(ref s) => s.clone(), + }; + + f.write_fmt(format_args!("Crypto error ({})", msg)) + } +} + +impl From<::secp256k1::Error> for Error { + fn from(e: ::secp256k1::Error) -> Error { + match e { + ::secp256k1::Error::InvalidMessage => Error::InvalidMessage, + ::secp256k1::Error::InvalidPublicKey => Error::InvalidPublic, + ::secp256k1::Error::InvalidSecretKey => Error::InvalidSecret, + _ => Error::InvalidSignature, + } + } +} + +impl From<::std::io::Error> for Error { + fn from(err: ::std::io::Error) -> Error { + Error::Io(err) + } +} diff --git a/ethkey/src/keccak.rs b/ethkey/src/keccak.rs new file mode 100644 index 000000000..3423f19fe --- /dev/null +++ b/ethkey/src/keccak.rs @@ -0,0 +1,15 @@ +use tiny_keccak::Keccak; + +pub trait Keccak256 { + fn keccak256(&self) -> T where T: Sized; +} + +impl Keccak256<[u8; 32]> for [u8] { + fn keccak256(&self) -> [u8; 32] { + let mut keccak = Keccak::new_keccak256(); + let mut result = [0u8; 32]; + keccak.update(self); + keccak.finalize(&mut result); + result + } +} diff --git a/ethkey/src/keypair.rs b/ethkey/src/keypair.rs new file mode 100644 index 000000000..dcfb67460 --- /dev/null +++ b/ethkey/src/keypair.rs @@ -0,0 +1,91 @@ +use std::fmt; +use secp256k1::key; +use rustc_serialize::hex::ToHex; +use keccak::Keccak256; +use super::{Secret, Public, Address, SECP256K1, Error}; + +/// secp256k1 key pair +pub struct KeyPair { + secret: Secret, + public: Public, +} + +impl fmt::Display for KeyPair { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + try!(writeln!(f, "secret: {}", self.secret.to_hex())); + try!(writeln!(f, "public: {}", self.public.to_hex())); + write!(f, "address: {}", self.address().to_hex()) + } +} + +impl KeyPair { + /// Create a pair from secret key + pub fn from_secret(secret: Secret) -> Result { + let context = &SECP256K1; + let s: key::SecretKey = try!(key::SecretKey::from_slice(context, &secret[..])); + let pub_key = try!(key::PublicKey::from_secret_key(context, &s)); + let serialized = pub_key.serialize_vec(context, false); + + let mut public = Public::default(); + public.copy_from_slice(&serialized[1..65]); + + let keypair = KeyPair { + secret: secret, + public: public, + }; + + Ok(keypair) + } + + pub fn from_keypair(sec: key::SecretKey, publ: key::PublicKey) -> Self { + let context = &SECP256K1; + let serialized = publ.serialize_vec(context, false); + let mut secret = Secret::default(); + secret.copy_from_slice(&sec[0..32]); + let mut public = Public::default(); + public.copy_from_slice(&serialized[1..65]); + + KeyPair { + secret: secret, + public: public, + } + } + + pub fn secret(&self) -> &Secret { + &self.secret + } + + pub fn public(&self) -> &Public { + &self.public + } + + pub fn address(&self) -> Address { + let hash = self.public.keccak256(); + let mut result = Address::default(); + result.copy_from_slice(&hash[12..]); + result + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use {KeyPair, Secret}; + + #[test] + fn from_secret() { + let secret = Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap(); + let _ = KeyPair::from_secret(secret).unwrap(); + } + + #[test] + fn keypair_display() { + let expected = +"secret: a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65 +public: 8ce0db0b0359ffc5866ba61903cc2518c3675ef2cf380a7e54bde7ea20e6fa1ab45b7617346cd11b7610001ee6ae5b0155c41cad9527cbcdff44ec67848943a4 +address: 5b073e9233944b5e729e46d618f0d8edf3d9c34a".to_owned(); + let secret = Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap(); + let kp = KeyPair::from_secret(secret).unwrap(); + assert_eq!(format!("{}", kp), expected); + } +} diff --git a/ethkey/src/lib.rs b/ethkey/src/lib.rs new file mode 100644 index 000000000..218abbbf3 --- /dev/null +++ b/ethkey/src/lib.rs @@ -0,0 +1,33 @@ +extern crate rand; +#[macro_use] +extern crate lazy_static; +extern crate tiny_keccak; +extern crate secp256k1; +extern crate rustc_serialize; + +mod brain; +mod error; +mod keypair; +mod keccak; +mod prefix; +mod primitive; +mod random; +mod signature; + +lazy_static! { + static ref SECP256K1: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); +} + +/// Generates new keypair. +pub trait Generator { + /// Should be called to generate new keypair. + fn generate(self) -> Result; +} + +pub use self::brain::Brain; +pub use self::error::Error; +pub use self::keypair::KeyPair; +pub use self::primitive::{Secret, Public, Address, Message}; +pub use self::prefix::Prefix; +pub use self::random::Random; +pub use self::signature::{sign, verify, Signature}; diff --git a/ethkey/src/prefix.rs b/ethkey/src/prefix.rs new file mode 100644 index 000000000..8d7f18cfa --- /dev/null +++ b/ethkey/src/prefix.rs @@ -0,0 +1,41 @@ +use super::{Random, Generator, KeyPair, Error}; + +/// Tries to find keypair with address starting with given prefix. +pub struct Prefix { + prefix: Vec, + iterations: usize, +} + +impl Prefix { + pub fn new(prefix: Vec, iterations: usize) -> Self { + Prefix { + prefix: prefix, + iterations: iterations, + } + } +} + +impl Generator for Prefix { + fn generate(self) -> Result { + for _ in 0..self.iterations { + let keypair = try!(Random.generate()); + if keypair.address().starts_with(&self.prefix) { + return Ok(keypair) + } + } + + Err(Error::Custom("Could not find keypair".into())) + } +} + +#[cfg(test)] +mod tests { + use {Generator, Prefix}; + + #[test] + fn prefix_generator() { + let prefix = vec![0xffu8]; + let keypair = Prefix::new(prefix.clone(), usize::max_value()).generate().unwrap(); + assert!(keypair.address().starts_with(&prefix)); + } +} diff --git a/ethkey/src/primitive.rs b/ethkey/src/primitive.rs new file mode 100644 index 000000000..11a614ded --- /dev/null +++ b/ethkey/src/primitive.rs @@ -0,0 +1,122 @@ +use std::ops::{Deref, DerefMut}; +use std::{fmt, cmp, hash}; +use std::str::FromStr; +use rustc_serialize::hex::{ToHex, FromHex}; +use Error; + +macro_rules! impl_primitive { + ($name: ident, $size: expr, $err: expr) => { + + #[repr(C)] + #[derive(Eq)] + pub struct $name([u8; $size]); + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self.to_hex()) + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self.to_hex()) + } + } + + impl FromStr for $name { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.from_hex() { + Ok(ref hex) if hex.len() == $size => { + let mut res = $name::default(); + res.copy_from_slice(hex); + Ok(res) + }, + _ => Err($err) + } + } + } + + impl PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + let self_ref: &[u8] = &self.0; + let other_ref: &[u8] = &other.0; + self_ref == other_ref + } + } + + impl PartialOrd for $name { + fn partial_cmp(&self, other: &Self) -> Option { + let self_ref: &[u8] = &self.0; + let other_ref: &[u8] = &other.0; + self_ref.partial_cmp(other_ref) + } + } + + impl Ord for $name { + fn cmp(&self, other: &Self) -> cmp::Ordering { + let self_ref: &[u8] = &self.0; + let other_ref: &[u8] = &other.0; + self_ref.cmp(other_ref) + } + } + + impl Clone for $name { + fn clone(&self) -> Self { + let mut res = Self::default(); + res.copy_from_slice(&self.0); + res + } + } + + impl Default for $name { + fn default() -> Self { + $name([0u8; $size]) + } + } + + impl From<[u8; $size]> for $name { + fn from(s: [u8; $size]) -> Self { + $name(s) + } + } + + impl Into<[u8; $size]> for $name { + fn into(self) -> [u8; $size] { + self.0 + } + } + + impl hash::Hash for $name { + fn hash(&self, state: &mut H) where H: hash::Hasher { + let self_ref: &[u8] = &self.0; + self_ref.hash(state) + } + } + + impl Deref for $name { + type Target = [u8; $size]; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for $name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + } +} + +impl_primitive!(Address, 20, Error::InvalidAddress); +impl_primitive!(Secret, 32, Error::InvalidSecret); +impl_primitive!(Message, 32, Error::InvalidMessage); +impl_primitive!(Public, 64, Error::InvalidPublic); + +#[cfg(test)] +mod tests { + +} diff --git a/ethkey/src/random.rs b/ethkey/src/random.rs new file mode 100644 index 000000000..aa3abb838 --- /dev/null +++ b/ethkey/src/random.rs @@ -0,0 +1,16 @@ +use rand::os::OsRng; +use super::{Generator, KeyPair, Error, SECP256K1}; + +/// Randomly generates new keypair. +pub struct Random; + +impl Generator for Random { + fn generate(self) -> Result { + let context = &SECP256K1; + let mut rng = try!(OsRng::new()); + let (sec, publ) = try!(context.generate_keypair(&mut rng)); + + Ok(KeyPair::from_keypair(sec, publ)) + } +} + diff --git a/ethkey/src/signature.rs b/ethkey/src/signature.rs new file mode 100644 index 000000000..f34baa04f --- /dev/null +++ b/ethkey/src/signature.rs @@ -0,0 +1,158 @@ +use std::ops::{Deref, DerefMut}; +use std::{mem, fmt}; +use std::str::FromStr; +use secp256k1::{Message as SecpMessage, RecoverableSignature, RecoveryId, Error as SecpError}; +use secp256k1::key::{SecretKey, PublicKey}; +use rustc_serialize::hex::{ToHex, FromHex}; +use {Secret, Public, SECP256K1, Error, Message}; + +#[repr(C)] +#[derive(Eq)] +pub struct Signature([u8; 65]); + +impl Signature { + /// Get a slice into the 'r' portion of the data. + pub fn r(&self) -> &[u8] { + &self.0[0..32] + } + + /// Get a slice into the 's' portion of the data. + pub fn s(&self) -> &[u8] { + &self.0[32..64] + } + + /// Get the recovery byte. + pub fn v(&self) -> u8 { + self.0[64] + } +} + +// manual implementation large arrays don't have trait impls by default. +// remove when integer generics exist +impl ::std::cmp::PartialEq for Signature { + fn eq(&self, other: &Self) -> bool { + &self.0[..] == &other.0[..] + } +} + +// also manual for the same reason, but the pretty printing might be useful. +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + f.debug_struct("Signature") + .field("r", &self.0[0..32].to_hex()) + .field("s", &self.0[32..64].to_hex()) + .field("v", &self.0[64..65].to_hex()) + .finish() + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self.to_hex()) + } +} + +impl FromStr for Signature { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.from_hex() { + Ok(ref hex) if hex.len() == 65 => { + let mut data = [0; 65]; + data.copy_from_slice(&hex[0..65]); + Ok(Signature(data)) + }, + _ => Err(Error::InvalidSignature) + } + } +} + +impl Default for Signature { + fn default() -> Self { + Signature([0; 65]) + } +} + +impl From<[u8; 65]> for Signature { + fn from(s: [u8; 65]) -> Self { + Signature(s) + } +} + +impl Into<[u8; 65]> for Signature { + fn into(self) -> [u8; 65] { + self.0 + } +} + +impl Deref for Signature { + type Target = [u8; 65]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Signature { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub fn sign(secret: &Secret, message: &Message) -> Result { + let context = &SECP256K1; + // no way to create from raw byte array. + let sec: &SecretKey = unsafe { mem::transmute(secret) }; + let s = try!(context.sign_recoverable(&try!(SecpMessage::from_slice(&message[..])), sec)); + let (rec_id, data) = s.serialize_compact(context); + let mut data_arr = [0; 65]; + + // no need to check if s is low, it always is + data_arr[0..64].copy_from_slice(&data[0..64]); + data_arr[64] = rec_id.to_i32() as u8; + Ok(Signature(data_arr)) +} + +pub fn verify(public: &Public, signature: &Signature, message: &Message) -> Result { + let context = &SECP256K1; + let rsig = try!(RecoverableSignature::from_compact(context, &signature[0..64], try!(RecoveryId::from_i32(signature[64] as i32)))); + let sig = rsig.to_standard(context); + + let pdata: [u8; 65] = { + let mut temp = [4u8; 65]; + temp[1..65].copy_from_slice(public.deref()); + temp + }; + + let publ = try!(PublicKey::from_slice(context, &pdata)); + match context.verify(&try!(SecpMessage::from_slice(&message[..])), &sig, &publ) { + Ok(_) => Ok(true), + Err(SecpError::IncorrectSignature) => Ok(false), + Err(x) => Err(Error::from(x)) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use {Generator, Random, Message}; + use super::{sign, verify, Signature}; + + #[test] + fn signature_to_and_from_str() { + let keypair = Random.generate().unwrap(); + let message = Message::default(); + let signature = sign(keypair.secret(), &message).unwrap(); + let string = format!("{}", signature); + let deserialized = Signature::from_str(&string).unwrap(); + assert_eq!(signature, deserialized); + } + + #[test] + fn sign_and_verify() { + let keypair = Random.generate().unwrap(); + let message = Message::default(); + let signature = sign(keypair.secret(), &message).unwrap(); + assert!(verify(keypair.public(), &signature, &message).unwrap()); + } +} diff --git a/ethstore/.editorconfig b/ethstore/.editorconfig new file mode 100644 index 000000000..0ac22f073 --- /dev/null +++ b/ethstore/.editorconfig @@ -0,0 +1,11 @@ +root = true +[*] +indent_style=tab +indent_size=tab +tab_width=4 +end_of_line=lf +charset=utf-8 +trim_trailing_whitespace=true +max_line_length=120 +insert_final_newline=true + diff --git a/ethstore/.gitignore b/ethstore/.gitignore new file mode 100644 index 000000000..c6262ea6a --- /dev/null +++ b/ethstore/.gitignore @@ -0,0 +1,2 @@ +target +*.swp diff --git a/ethstore/.travis.yml b/ethstore/.travis.yml new file mode 100644 index 000000000..8113e488e --- /dev/null +++ b/ethstore/.travis.yml @@ -0,0 +1,23 @@ +sudo: false +language: rust +branches: + only: + - master +matrix: + fast_finish: false + include: + - rust: stable + - rust: beta + - rust: nightly +after_success: | + [ $TRAVIS_BRANCH = master ] && + [ $TRAVIS_PULL_REQUEST = false ] && + [ $TRAVIS_RUST_VERSION = stable ] && + cargo doc --no-deps --verbose && + echo '' > target/doc/index.html && + pip install --user ghp-import && + /home/travis/.local/bin/ghp-import -n target/doc && + git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages +env: + global: + - secure: C4l7WR0jS84WNmd3MvmpPXQz4wRh4CLDS6bP3BqSHXadz8FPKejtMJZscLYAk5kIkDcVsTAYb88RsEFRrYOA4wkS6vhZBtryYRaJ68MlkyEU/77SYwm86rkQINIDw65O73dUD5LbWWCUoYkenGu26u/UnfayHfJBAyKw5IHkVKf6eDqu7E8ojKSEOXbWgBHjq6uixI8IESb15UjIE0AQ1Od+6cqhsz/caPhTMT3CJGjoCoVGWChwWSQZ+Ppb+xB83C/1h58UVwE9sZEyIPKwVP6socnHPmtR+VEUI6a7YIsOk6ZadKLtyy4523w4HqHNx1/dYjmsknbGpkF4D0DRp5L3D4t4J6URCkJIHfSRrBF5l2QbLMMuSf+KWMWuFOrOF5DBryobRKAVmIL5AjfvFsxtBNzYLPyVBs0ntbPuN5WeUPhadam00za9Z1ZvOUJxfNfyy9R67u6FdD9xkw2m/9hO7KJLDeZ4TSCRFrzfl/7WQprfjCwhZ+reKPgHH0Ufy1/Kh/WEuEBfZDa+z3mWWHlslqH2uBPH3+pvhzdVQGLB/5GZdJNeg/nJYJDCqHyWUKxkw+OMSvI0J8W0GiHV4TuY9V3p+rYjU2Zj69u3/xO/IvKrFtB9xdeJMrLiFQ2cD5vgzQOLCKo80f53NitUjdVSoWrY/NcYopBU4VHZMlk= diff --git a/ethstore/Cargo.toml b/ethstore/Cargo.toml new file mode 100644 index 000000000..41504154d --- /dev/null +++ b/ethstore/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "ethstore" +version = "0.1.0" +authors = ["debris "] +build = "build.rs" + +[dependencies] +libc = "0.2.11" +rand = "0.3.14" +ethkey = { path = "../ethkey" } +serde = "0.7" +serde_json = "0.7" +serde_macros = { version = "0.7", optional = true } +rustc-serialize = "0.3" +rust-crypto = "0.2.36" +tiny-keccak = "1.0" +docopt = { version = "0.6", optional = true } + +[build-dependencies] +serde_codegen = { version = "0.7", optional = true } +syntex = "0.33.0" + +[features] +default = ["cli", "serde_codegen"] +nightly = ["serde_macros"] +cli = ["docopt"] + +[[bin]] +name = "ethstore" +doc = false diff --git a/ethstore/README.md b/ethstore/README.md new file mode 100644 index 000000000..aba4911bf --- /dev/null +++ b/ethstore/README.md @@ -0,0 +1,162 @@ +# ethstore + +[![Build Status][travis-image]][travis-url] + +[travis-image]: https://travis-ci.org/ethcore/ethstore.svg?branch=master +[travis-url]: https://travis-ci.org/ethcore/ethstore + +Ethereum key management. + +[Documentation](http://ethcore.github.io/ethstore/ethstore/index.html) + +### Usage + +``` +Ethereum key management. + Copyright 2016 Ethcore (UK) Limited + +Usage: + ethstore insert [--dir DIR] + ethstore change-pwd
[--dir DIR] + ethstore list [--dir DIR] + ethstore import [--src DIR] [--dir DIR] + ethstore remove
[--dir DIR] + ethstore sign
[--dir DIR] + ethstore [-h | --help] + +Options: + -h, --help Display this message and exit. + --dir DIR Specify the secret store directory. It may be either + parity, parity-test, geth, geth-test + or a path [default: parity]. + --src DIR Specify import source. It may be either + parity, parity-test, get, geth-test + or a path [default: geth]. + +Commands: + insert Save account with password. + change-pwd Change account password. + list List accounts. + import Import accounts from src. + remove Remove account. + sign Sign message. +``` + +### Examples + +#### `insert [--dir DIR]` +*Encrypt secret with a password and save it in secret store.* + +- `` - ethereum secret, 32 bytes long +- `` - account password, any string +- `[--dir DIR]` - secret store directory, It may be either parity, parity-test, geth, geth-test or a path. default: parity + +``` +ethstore insert 7d29fab185a33e2cd955812397354c472d2b84615b645aa135ff539f6b0d70d5 "this is sparta" +``` + +``` +a8fa5dd30a87bb9e3288d604eb74949c515ab66e +``` + +-- + +``` +ethstore insert `ethkey generate random -s` "this is sparta" +``` + +``` +24edfff680d536a5f6fe862d36df6f8f6f40f115 +``` + +-- + +#### `change-pwd
[--dir DIR]` +*Change account password.* + +- `
` - ethereum address, 20 bytes long +- `` - old account password, any string +- `` - new account password, any string +- `[--dir DIR]` - secret store directory, It may be either parity, parity-test, geth, geth-test or a path. default: parity + +``` +ethstore change-pwd a8fa5dd30a87bb9e3288d604eb74949c515ab66e "this is sparta" "hello world" +``` + +``` +true +``` + +-- + +#### `list [--dir DIR]` +*List secret store accounts.* + +- `[--dir DIR]` - secret store directory, It may be either parity, parity-test, geth, geth-test or a path. default: parity + +``` +ethstore list +``` + +``` + 0: 24edfff680d536a5f6fe862d36df6f8f6f40f115 + 1: 6edddfc6349aff20bc6467ccf276c5b52487f7a8 + 2: e6a3d25a7cb7cd21cb720df5b5e8afd154af1bbb +``` + +-- + +#### `import [--src DIR] [--dir DIR]` +*Import accounts from src.* + +- `[--src DIR]` - secret store directory, It may be either parity, parity-test, geth, geth-test or a path. default: geth +- `[--dir DIR]` - secret store directory, It may be either parity, parity-test, geth, geth-test or a path. default: parity + +``` + 0: e6a3d25a7cb7cd21cb720df5b5e8afd154af1bbb + 1: 6edddfc6349aff20bc6467ccf276c5b52487f7a8 +``` + +-- + +#### `remove
[--dir DIR]` +*Remove account from secret store.* + +- `
` - ethereum address, 20 bytes long +- `` - account password, any string +- `[--dir DIR]` - secret store directory, It may be either parity, parity-test, geth, geth-test or a path. default: parity + +``` +ethstore remove a8fa5dd30a87bb9e3288d604eb74949c515ab66e "hello world" +``` + +``` +true +``` + +-- + +#### `sign
[--dir DIR]` +*Sign message with account's secret.* + +- `
` - ethereum address, 20 bytes long +- `` - account password, any string +- `` - message to sign, 32 bytes long +- `[--dir DIR]` - secret store directory, It may be either parity, parity-test, geth, geth-test or a path. default: parity + +``` +ethstore sign 24edfff680d536a5f6fe862d36df6f8f6f40f115 "this is sparta" 7d29fab185a33e2cd955812397354c472d2b84615b645aa135ff539f6b0d70d5 +``` + +``` +c6649f9555232d90ff716d7e552a744c5af771574425a74860e12f763479eb1b708c1f3a7dc0a0a7f7a81e0a0ca88c6deacf469222bb3d9c5bf0847f98bae54901 +``` + +-- + +# Ethcore toolchain +*this project is a part of the ethcore toolchain* + +- [**ethkey**](https://github.com/ethcore/ethkey) - Ethereum keys generator and signer. +- [**ethstore**](https://github.com/ethcore/ethstore) - Ethereum key management. +- [**ethabi**](https://github.com/ethcore/ethabi) - Ethereum function calls encoding. diff --git a/ethstore/build.rs b/ethstore/build.rs new file mode 100644 index 000000000..9113e596d --- /dev/null +++ b/ethstore/build.rs @@ -0,0 +1,29 @@ +#[cfg(not(feature = "serde_macros"))] +mod inner { + extern crate syntex; + extern crate serde_codegen; + + use std::env; + use std::path::Path; + + pub fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + + let src = Path::new("src/json/mod.rs.in"); + let dst = Path::new(&out_dir).join("mod.rs"); + + let mut registry = syntex::Registry::new(); + + serde_codegen::register(&mut registry); + registry.expand("", &src, &dst).unwrap(); + } +} + +#[cfg(feature = "serde_macros")] +mod inner { + pub fn main() {} +} + +fn main() { + inner::main(); +} diff --git a/ethstore/src/account/cipher.rs b/ethstore/src/account/cipher.rs new file mode 100644 index 000000000..55b3c1de8 --- /dev/null +++ b/ethstore/src/account/cipher.rs @@ -0,0 +1,43 @@ +use json; + +#[derive(Debug, PartialEq, Clone)] +pub struct Aes128Ctr { + pub iv: [u8; 16], +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Cipher { + Aes128Ctr(Aes128Ctr), +} + +impl From for Aes128Ctr { + fn from(json: json::Aes128Ctr) -> Self { + Aes128Ctr { + iv: json.iv.into() + } + } +} + +impl Into for Aes128Ctr { + fn into(self) -> json::Aes128Ctr { + json::Aes128Ctr { + iv: From::from(self.iv) + } + } +} + +impl From for Cipher { + fn from(json: json::Cipher) -> Self { + match json { + json::Cipher::Aes128Ctr(params) => Cipher::Aes128Ctr(From::from(params)), + } + } +} + +impl Into for Cipher { + fn into(self) -> json::Cipher { + match self { + Cipher::Aes128Ctr(params) => json::Cipher::Aes128Ctr(params.into()), + } + } +} diff --git a/ethstore/src/account/kdf.rs b/ethstore/src/account/kdf.rs new file mode 100644 index 000000000..f8d67dac8 --- /dev/null +++ b/ethstore/src/account/kdf.rs @@ -0,0 +1,109 @@ +use json; + +#[derive(Debug, PartialEq, Clone)] +pub enum Prf { + HmacSha256, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Pbkdf2 { + pub c: u32, + pub dklen: u32, + pub prf: Prf, + pub salt: [u8; 32], +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Scrypt { + pub dklen: u32, + pub p: u32, + pub n: u32, + pub r: u32, + pub salt: [u8; 32], +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Kdf { + Pbkdf2(Pbkdf2), + Scrypt(Scrypt), +} + +impl From for Prf { + fn from(json: json::Prf) -> Self { + match json { + json::Prf::HmacSha256 => Prf::HmacSha256, + } + } +} + +impl Into for Prf { + fn into(self) -> json::Prf { + match self { + Prf::HmacSha256 => json::Prf::HmacSha256, + } + } +} + +impl From for Pbkdf2 { + fn from(json: json::Pbkdf2) -> Self { + Pbkdf2 { + c: json.c, + dklen: json.dklen, + prf: From::from(json.prf), + salt: json.salt.into(), + } + } +} + +impl Into for Pbkdf2 { + fn into(self) -> json::Pbkdf2 { + json::Pbkdf2 { + c: self.c, + dklen: self.dklen, + prf: self.prf.into(), + salt: From::from(self.salt), + } + } +} + +impl From for Scrypt { + fn from(json: json::Scrypt) -> Self { + Scrypt { + dklen: json.dklen, + p: json.p, + n: json.n, + r: json.r, + salt: json.salt.into(), + } + } +} + +impl Into for Scrypt { + fn into(self) -> json::Scrypt { + json::Scrypt { + dklen: self.dklen, + p: self.p, + n: self.n, + r: self.r, + salt: From::from(self.salt), + } + } +} + +impl From for Kdf { + fn from(json: json::Kdf) -> Self { + match json { + json::Kdf::Pbkdf2(params) => Kdf::Pbkdf2(From::from(params)), + json::Kdf::Scrypt(params) => Kdf::Scrypt(From::from(params)), + } + } +} + +impl Into for Kdf { + fn into(self) -> json::Kdf { + match self { + Kdf::Pbkdf2(params) => json::Kdf::Pbkdf2(params.into()), + Kdf::Scrypt(params) => json::Kdf::Scrypt(params.into()), + } + } +} diff --git a/ethstore/src/account/mod.rs b/ethstore/src/account/mod.rs new file mode 100644 index 000000000..b97526991 --- /dev/null +++ b/ethstore/src/account/mod.rs @@ -0,0 +1,9 @@ +mod cipher; +mod kdf; +mod safe_account; +mod version; + +pub use self::cipher::{Cipher, Aes128Ctr}; +pub use self::kdf::{Kdf, Pbkdf2, Scrypt, Prf}; +pub use self::safe_account::{SafeAccount, Crypto}; +pub use self::version::Version; diff --git a/ethstore/src/account/safe_account.rs b/ethstore/src/account/safe_account.rs new file mode 100644 index 000000000..934bfd03d --- /dev/null +++ b/ethstore/src/account/safe_account.rs @@ -0,0 +1,200 @@ +use std::ops::{Deref, DerefMut}; +use ethkey::{KeyPair, sign, Address, Secret, Signature, Message}; +use {json, Error, crypto}; +use crypto::Keccak256; +use random::Random; +use account::{Version, Cipher, Kdf, Aes128Ctr, Pbkdf2, Prf}; + +#[derive(Debug, PartialEq, Clone)] +pub struct Crypto { + pub cipher: Cipher, + pub ciphertext: [u8; 32], + pub kdf: Kdf, + pub mac: [u8; 32], +} + +#[derive(Debug, PartialEq, Clone)] +pub struct SafeAccount { + pub id: [u8; 16], + pub version: Version, + pub address: Address, + pub crypto: Crypto, +} + +impl From for Crypto { + fn from(json: json::Crypto) -> Self { + Crypto { + cipher: From::from(json.cipher), + ciphertext: json.ciphertext.into(), + kdf: From::from(json.kdf), + mac: json.mac.into(), + } + } +} + +impl Into for Crypto { + fn into(self) -> json::Crypto { + json::Crypto { + cipher: self.cipher.into(), + ciphertext: From::from(self.ciphertext), + kdf: self.kdf.into(), + mac: From::from(self.mac), + } + } +} + +impl From for SafeAccount { + fn from(json: json::KeyFile) -> Self { + SafeAccount { + id: json.id.into(), + version: From::from(json.version), + address: From::from(json.address), //json.address.into(), + crypto: From::from(json.crypto), + } + } +} + +impl Into for SafeAccount { + fn into(self) -> json::KeyFile { + json::KeyFile { + id: From::from(self.id), + version: self.version.into(), + address: self.address.into(), //From::from(self.address), + crypto: self.crypto.into(), + } + } +} + +impl Crypto { + pub fn create(secret: &Secret, password: &str, iterations: u32) -> Self { + let salt: [u8; 32] = Random::random(); + let iv: [u8; 16] = Random::random(); + + // two parts of derived key + // DK = [ DK[0..15] DK[16..31] ] = [derived_left_bits, derived_right_bits] + let (derived_left_bits, derived_right_bits) = crypto::derive_key_iterations(password, &salt, iterations); + + let mut ciphertext = [0u8; 32]; + + // aes-128-ctr with initial vector of iv + crypto::aes::encrypt(&derived_left_bits, &iv, secret.deref(), &mut ciphertext); + + // KECCAK(DK[16..31] ++ ), where DK[16..31] - derived_right_bits + let mac = crypto::derive_mac(&derived_right_bits, &ciphertext).keccak256(); + + Crypto { + cipher: Cipher::Aes128Ctr(Aes128Ctr { + iv: iv, + }), + ciphertext: ciphertext, + kdf: Kdf::Pbkdf2(Pbkdf2 { + dklen: crypto::KEY_LENGTH as u32, + salt: salt, + c: iterations, + prf: Prf::HmacSha256, + }), + mac: mac, + } + } + + pub fn secret(&self, password: &str) -> Result { + let (derived_left_bits, derived_right_bits) = match self.kdf { + Kdf::Pbkdf2(ref params) => crypto::derive_key_iterations(password, ¶ms.salt, params.c), + Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r), + }; + + let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256(); + + if mac != self.mac { + return Err(Error::InvalidPassword); + } + + let mut secret = Secret::default(); + + match self.cipher { + Cipher::Aes128Ctr(ref params) => { + crypto::aes::decrypt(&derived_left_bits, ¶ms.iv, &self.ciphertext, secret.deref_mut()) + }, + } + + Ok(secret) + } +} + +impl SafeAccount { + pub fn create(keypair: &KeyPair, id: [u8; 16], password: &str, iterations: u32) -> Self { + SafeAccount { + id: id, + version: Version::V3, + crypto: Crypto::create(keypair.secret(), password, iterations), + address: keypair.address(), + } + } + + pub fn sign(&self, password: &str, message: &Message) -> Result { + let secret = try!(self.crypto.secret(password)); + sign(&secret, message).map_err(From::from) + } + + pub fn change_password(&self, old_password: &str, new_password: &str, iterations: u32) -> Result { + let secret = try!(self.crypto.secret(old_password)); + let result = SafeAccount { + id: self.id.clone(), + version: self.version.clone(), + crypto: Crypto::create(&secret, new_password, iterations), + address: self.address.clone(), + }; + Ok(result) + } + + pub fn check_password(&self, password: &str) -> bool { + self.crypto.secret(password).is_ok() + } +} + +#[cfg(test)] +mod tests { + use ethkey::{Generator, Random, verify, Message}; + use super::{Crypto, SafeAccount}; + + #[test] + fn crypto_create() { + let keypair = Random.generate().unwrap(); + let crypto = Crypto::create(keypair.secret(), "this is sparta", 10240); + let secret = crypto.secret("this is sparta").unwrap(); + assert_eq!(keypair.secret(), &secret); + } + + #[test] + #[should_panic] + fn crypto_invalid_password() { + let keypair = Random.generate().unwrap(); + let crypto = Crypto::create(keypair.secret(), "this is sparta", 10240); + let _ = crypto.secret("this is sparta!").unwrap(); + } + + #[test] + fn sign_and_verify() { + let keypair = Random.generate().unwrap(); + let password = "hello world"; + let message = Message::default(); + let account = SafeAccount::create(&keypair, [0u8; 16], password, 10240); + let signature = account.sign(password, &message).unwrap(); + assert!(verify(keypair.public(), &signature, &message).unwrap()); + } + + #[test] + fn change_password() { + let keypair = Random.generate().unwrap(); + let first_password = "hello world"; + let sec_password = "this is sparta"; + let i = 10240; + let message = Message::default(); + let account = SafeAccount::create(&keypair, [0u8; 16], first_password, i); + let new_account = account.change_password(first_password, sec_password, i).unwrap(); + assert!(account.sign(first_password, &message).is_ok()); + assert!(account.sign(sec_password, &message).is_err()); + assert!(new_account.sign(first_password, &message).is_err()); + assert!(new_account.sign(sec_password, &message).is_ok()); + } +} diff --git a/ethstore/src/account/version.rs b/ethstore/src/account/version.rs new file mode 100644 index 000000000..570ebd981 --- /dev/null +++ b/ethstore/src/account/version.rs @@ -0,0 +1,22 @@ +use json; + +#[derive(Debug, PartialEq, Clone)] +pub enum Version { + V3, +} + +impl From for Version { + fn from(json: json::Version) -> Self { + match json { + json::Version::V3 => Version::V3, + } + } +} + +impl Into for Version { + fn into(self) -> json::Version { + match self { + Version::V3 => json::Version::V3, + } + } +} diff --git a/ethstore/src/bin/ethstore.rs b/ethstore/src/bin/ethstore.rs new file mode 100644 index 000000000..e53acfeb2 --- /dev/null +++ b/ethstore/src/bin/ethstore.rs @@ -0,0 +1,128 @@ +extern crate rustc_serialize; +extern crate docopt; +extern crate ethstore; + +use std::{env, process}; +use std::ops::Deref; +use std::str::FromStr; +use docopt::Docopt; +use ethstore::ethkey::{Secret, Address, Message}; +use ethstore::dir::{KeyDirectory, ParityDirectory, DiskDirectory, GethDirectory, DirectoryType}; +use ethstore::{EthStore, SecretStore, import_accounts, Error}; + +pub const USAGE: &'static str = r#" +Ethereum key management. + Copyright 2016 Ethcore (UK) Limited + +Usage: + ethstore insert [--dir DIR] + ethstore change-pwd
[--dir DIR] + ethstore list [--dir DIR] + ethstore import [--src DIR] [--dir DIR] + ethstore remove
[--dir DIR] + ethstore sign
[--dir DIR] + ethstore [-h | --help] + +Options: + -h, --help Display this message and exit. + --dir DIR Specify the secret store directory. It may be either + parity, parity-test, geth, geth-test + or a path [default: parity]. + --src DIR Specify import source. It may be either + parity, parity-test, get, geth-test + or a path [default: geth]. + +Commands: + insert Save account with password. + change-pwd Change password. + list List accounts. + import Import accounts from src. + remove Remove account. + sign Sign message. +"#; + +#[derive(Debug, RustcDecodable)] +struct Args { + cmd_insert: bool, + cmd_change_pwd: bool, + cmd_list: bool, + cmd_import: bool, + cmd_remove: bool, + cmd_sign: bool, + arg_secret: String, + arg_password: String, + arg_old_pwd: String, + arg_new_pwd: String, + arg_address: String, + arg_message: String, + flag_src: String, + flag_dir: String, +} + +fn main() { + match execute(env::args()) { + Ok(result) => println!("{}", result), + Err(err) => { + println!("{}", err); + process::exit(1); + } + } +} + +fn key_dir(location: &str) -> Result, Error> { + let dir: Box = match location { + "parity" => Box::new(try!(ParityDirectory::create(DirectoryType::Main))), + "parity-test" => Box::new(try!(ParityDirectory::create(DirectoryType::Testnet))), + "geth" => Box::new(try!(GethDirectory::create(DirectoryType::Main))), + "geth-test" => Box::new(try!(GethDirectory::create(DirectoryType::Testnet))), + path => Box::new(try!(DiskDirectory::create(path))), + }; + + Ok(dir) +} + +fn format_accounts(accounts: &[Address]) -> String { + accounts.iter() + .enumerate() + .map(|(i, a)| format!("{:2}: {}", i, a)) + .collect::>() + .join("\n") +} + +fn execute(command: I) -> Result where I: IntoIterator, S: AsRef { + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.argv(command).decode()) + .unwrap_or_else(|e| e.exit()); + + let store = try!(EthStore::open(try!(key_dir(&args.flag_dir)))); + + return if args.cmd_insert { + let secret = try!(Secret::from_str(&args.arg_secret)); + let address = try!(store.insert_account(secret, &args.arg_password)); + Ok(format!("{}", address)) + } else if args.cmd_change_pwd { + let address = try!(Address::from_str(&args.arg_address)); + let ok = store.change_password(&address, &args.arg_old_pwd, &args.arg_new_pwd).is_ok(); + Ok(format!("{}", ok)) + } else if args.cmd_list { + let accounts = store.accounts(); + Ok(format_accounts(&accounts)) + } else if args.cmd_import { + let src = try!(key_dir(&args.flag_src)); + let dst = try!(key_dir(&args.flag_dir)); + let accounts = try!(import_accounts(src.deref(), dst.deref())); + Ok(format_accounts(&accounts)) + } else if args.cmd_remove { + let address = try!(Address::from_str(&args.arg_address)); + let ok = store.remove_account(&address, &args.arg_password).is_ok(); + Ok(format!("{}", ok)) + } else if args.cmd_sign { + let address = try!(Address::from_str(&args.arg_address)); + let message = try!(Message::from_str(&args.arg_message)); + let signature = try!(store.sign(&address, &args.arg_password, &message)); + Ok(format!("{}", signature)) + } else { + unreachable!(); + } +} + diff --git a/ethstore/src/bin/main.rs b/ethstore/src/bin/main.rs new file mode 100644 index 000000000..8bed9a946 --- /dev/null +++ b/ethstore/src/bin/main.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "cli")] +include!("ethstore.rs"); + +#[cfg(not(feature = "cli"))] +fn main() {} diff --git a/ethstore/src/crypto.rs b/ethstore/src/crypto.rs new file mode 100644 index 000000000..05ebc4042 --- /dev/null +++ b/ethstore/src/crypto.rs @@ -0,0 +1,69 @@ +use tiny_keccak::Keccak; +use rcrypto::pbkdf2::pbkdf2; +use rcrypto::scrypt::{scrypt, ScryptParams}; +use rcrypto::sha2::Sha256; +use rcrypto::hmac::Hmac; + +pub const KEY_LENGTH: usize = 32; +pub const KEY_ITERATIONS: usize = 10240; +pub const KEY_LENGTH_AES: usize = KEY_LENGTH / 2; + +pub fn derive_key_iterations(password: &str, salt: &[u8; 32], c: u32) -> (Vec, Vec) { + let mut h_mac = Hmac::new(Sha256::new(), password.as_bytes()); + let mut derived_key = vec![0u8; KEY_LENGTH]; + pbkdf2(&mut h_mac, salt, c, &mut derived_key); + let derived_right_bits = &derived_key[0..KEY_LENGTH_AES]; + let derived_left_bits = &derived_key[KEY_LENGTH_AES..KEY_LENGTH]; + (derived_right_bits.to_vec(), derived_left_bits.to_vec()) +} + +pub fn derive_key_scrypt(password: &str, salt: &[u8; 32], n: u32, p: u32, r: u32) -> (Vec, Vec) { + let mut derived_key = vec![0u8; KEY_LENGTH]; + let scrypt_params = ScryptParams::new(n.trailing_zeros() as u8, r, p); + scrypt(password.as_bytes(), salt, &scrypt_params, &mut derived_key); + let derived_right_bits = &derived_key[0..KEY_LENGTH_AES]; + let derived_left_bits = &derived_key[KEY_LENGTH_AES..KEY_LENGTH]; + (derived_right_bits.to_vec(), derived_left_bits.to_vec()) +} + +pub fn derive_mac(derived_left_bits: &[u8], cipher_text: &[u8]) -> Vec { + let mut mac = vec![0u8; KEY_LENGTH_AES + cipher_text.len()]; + mac[0..KEY_LENGTH_AES].copy_from_slice(derived_left_bits); + mac[KEY_LENGTH_AES..cipher_text.len() + KEY_LENGTH_AES].copy_from_slice(cipher_text); + mac +} + +pub trait Keccak256 { + fn keccak256(&self) -> T where T: Sized; +} + +impl Keccak256<[u8; 32]> for [u8] { + fn keccak256(&self) -> [u8; 32] { + let mut keccak = Keccak::new_keccak256(); + let mut result = [0u8; 32]; + keccak.update(self); + keccak.finalize(&mut result); + result + } +} + +/// AES encryption +pub mod aes { + use rcrypto::blockmodes::CtrMode; + use rcrypto::aessafe::AesSafe128Encryptor; + use rcrypto::symmetriccipher::{Encryptor, Decryptor}; + use rcrypto::buffer::{RefReadBuffer, RefWriteBuffer}; + + /// Encrypt a message + pub fn encrypt(k: &[u8], iv: &[u8], plain: &[u8], dest: &mut [u8]) { + let mut encryptor = CtrMode::new(AesSafe128Encryptor::new(k), iv.to_vec()); + encryptor.encrypt(&mut RefReadBuffer::new(plain), &mut RefWriteBuffer::new(dest), true).expect("Invalid length or padding"); + } + + /// Decrypt a message + pub fn decrypt(k: &[u8], iv: &[u8], encrypted: &[u8], dest: &mut [u8]) { + let mut encryptor = CtrMode::new(AesSafe128Encryptor::new(k), iv.to_vec()); + encryptor.decrypt(&mut RefReadBuffer::new(encrypted), &mut RefWriteBuffer::new(dest), true).expect("Invalid length or padding"); + } +} + diff --git a/ethstore/src/dir/disk.rs b/ethstore/src/dir/disk.rs new file mode 100644 index 000000000..2f6f39657 --- /dev/null +++ b/ethstore/src/dir/disk.rs @@ -0,0 +1,109 @@ +use std::{fs, ffi, io}; +use std::path::{PathBuf, Path}; +use std::collections::HashMap; +use ethkey::Address; +use {libc, json, SafeAccount, Error}; +use super::KeyDirectory; + +#[cfg(not(windows))] +fn restrict_permissions_to_owner(file_path: &Path) -> Result<(), i32> { + let cstr = ffi::CString::new(file_path.to_str().unwrap()).unwrap(); + match unsafe { libc::chmod(cstr.as_ptr(), libc::S_IWUSR | libc::S_IRUSR) } { + 0 => Ok(()), + x => Err(x), + } +} + +#[cfg(windows)] +fn restrict_permissions_to_owner(file_path: &Path) -> Result<(), i32> { + Ok(()) +} + +pub struct DiskDirectory { + path: PathBuf, +} + +impl DiskDirectory { + pub fn create

(path: P) -> Result where P: AsRef { + try!(fs::create_dir_all(&path)); + Ok(Self::at(path)) + } + + pub fn at

(path: P) -> Self where P: AsRef { + DiskDirectory { + path: path.as_ref().to_path_buf(), + } + } + + /// all accounts found in keys directory + fn files(&self) -> Result, Error> { + // it's not done using one iterator cause + // there is an issue with rustc and it takes tooo much time to compile + let paths = try!(fs::read_dir(&self.path)) + .flat_map(Result::ok) + .filter(|entry| { + let metadata = entry.metadata(); + metadata.is_ok() && !metadata.unwrap().is_dir() + }) + .map(|entry| entry.path()) + .collect::>(); + + let files: Result, _> = paths.iter() + .map(fs::File::open) + .collect(); + + let files = try!(files); + + let accounts = files.into_iter() + .map(json::KeyFile::load) + .zip(paths.into_iter()) + .filter_map(|(file, path)| file.ok().map(|file| (path, SafeAccount::from(file)))) + .collect(); + + Ok(accounts) + } +} + +impl KeyDirectory for DiskDirectory { + fn load(&self) -> Result, Error> { + let accounts = try!(self.files()) + .into_iter() + .map(|(_, account)| account) + .collect(); + Ok(accounts) + } + + fn insert(&self, account: SafeAccount) -> Result<(), Error> { + // transform account into key file + let keyfile: json::KeyFile = account.into(); + + // build file path + let mut keyfile_path = self.path.clone(); + keyfile_path.push(format!("{}", keyfile.id)); + + // save the file + let mut file = try!(fs::File::create(&keyfile_path)); + try!(keyfile.write(&mut file).map_err(|e| Error::Custom(format!("{:?}", e)))); + + if let Err(_) = restrict_permissions_to_owner(&keyfile_path) { + fs::remove_file(&keyfile_path).expect("Expected to remove recently created file"); + return Err(Error::Io(io::Error::last_os_error())); + } + + Ok(()) + } + + fn remove(&self, address: &Address) -> Result<(), Error> { + // enumerate all entries in keystore + // and find entry with given address + let to_remove = try!(self.files()) + .into_iter() + .find(|&(_, ref account)| &account.address == address); + + // remove it + match to_remove { + None => Err(Error::InvalidAccount), + Some((path, _)) => fs::remove_file(path).map_err(From::from) + } + } +} diff --git a/ethstore/src/dir/geth.rs b/ethstore/src/dir/geth.rs new file mode 100644 index 000000000..9f8850aad --- /dev/null +++ b/ethstore/src/dir/geth.rs @@ -0,0 +1,79 @@ +use std::env; +use std::path::PathBuf; +use ethkey::Address; +use {SafeAccount, Error}; +use super::{KeyDirectory, DiskDirectory, DirectoryType}; + +#[cfg(target_os = "macos")] +fn geth_dir_path() -> PathBuf { + let mut home = env::home_dir().expect("Failed to get home dir"); + home.push("Library"); + home.push("Ethereum"); + home +} + +#[cfg(windows)] +/// Default path for ethereum installation on Windows +pub fn geth_dir_path() -> PathBuf { + let mut home = env::home_dir().expect("Failed to get home dir"); + home.push("AppData"); + home.push("Roaming"); + home.push("Ethereum"); + home +} + +#[cfg(not(any(target_os = "macos", windows)))] +/// Default path for ethereum installation on posix system which is not Mac OS +pub fn geth_dir_path() -> PathBuf { + let mut home = env::home_dir().expect("Failed to get home dir"); + home.push(".ethereum"); + home +} + +fn geth_keystore(t: DirectoryType) -> PathBuf { + let mut dir = geth_dir_path(); + match t { + DirectoryType::Testnet => { + dir.push("testnet"); + dir.push("keystore"); + }, + DirectoryType::Main => { + dir.push("keystore"); + } + } + dir +} + +pub struct GethDirectory { + dir: DiskDirectory, +} + +impl GethDirectory { + pub fn create(t: DirectoryType) -> Result { + let result = GethDirectory { + dir: try!(DiskDirectory::create(geth_keystore(t))), + }; + + Ok(result) + } + + pub fn open(t: DirectoryType) -> Self { + GethDirectory { + dir: DiskDirectory::at(geth_keystore(t)), + } + } +} + +impl KeyDirectory for GethDirectory { + fn load(&self) -> Result, Error> { + self.dir.load() + } + + fn insert(&self, account: SafeAccount) -> Result<(), Error> { + self.dir.insert(account) + } + + fn remove(&self, address: &Address) -> Result<(), Error> { + self.dir.remove(address) + } +} diff --git a/ethstore/src/dir/mod.rs b/ethstore/src/dir/mod.rs new file mode 100644 index 000000000..a5537b56c --- /dev/null +++ b/ethstore/src/dir/mod.rs @@ -0,0 +1,21 @@ +use ethkey::Address; +use {SafeAccount, Error}; + +mod disk; +mod geth; +mod parity; + +pub enum DirectoryType { + Testnet, + Main, +} + +pub trait KeyDirectory: Send + Sync { + fn load(&self) -> Result, Error>; + fn insert(&self, account: SafeAccount) -> Result<(), Error>; + fn remove(&self, address: &Address) -> Result<(), Error>; +} + +pub use self::disk::DiskDirectory; +pub use self::geth::GethDirectory; +pub use self::parity::ParityDirectory; diff --git a/ethstore/src/dir/parity.rs b/ethstore/src/dir/parity.rs new file mode 100644 index 000000000..6f11d5ae6 --- /dev/null +++ b/ethstore/src/dir/parity.rs @@ -0,0 +1,58 @@ +use std::env; +use std::path::PathBuf; +use ethkey::Address; +use {SafeAccount, Error}; +use super::{KeyDirectory, DiskDirectory, DirectoryType}; + +fn parity_dir_path() -> PathBuf { + let mut home = env::home_dir().expect("Failed to get home dir"); + home.push(".parity"); + home +} + +fn parity_keystore(t: DirectoryType) -> PathBuf { + let mut dir = parity_dir_path(); + match t { + DirectoryType::Testnet => { + dir.push("testnet_keys"); + }, + DirectoryType::Main => { + dir.push("keys"); + } + } + dir +} + +pub struct ParityDirectory { + dir: DiskDirectory, +} + +impl ParityDirectory { + pub fn create(t: DirectoryType) -> Result { + let result = ParityDirectory { + dir: try!(DiskDirectory::create(parity_keystore(t))), + }; + + Ok(result) + } + + pub fn open(t: DirectoryType) -> Self { + ParityDirectory { + dir: DiskDirectory::at(parity_keystore(t)), + } + } +} + +impl KeyDirectory for ParityDirectory { + fn load(&self) -> Result, Error> { + self.dir.load() + } + + fn insert(&self, account: SafeAccount) -> Result<(), Error> { + self.dir.insert(account) + } + + fn remove(&self, address: &Address) -> Result<(), Error> { + self.dir.remove(address) + } +} diff --git a/ethstore/src/error.rs b/ethstore/src/error.rs new file mode 100644 index 000000000..537bfe008 --- /dev/null +++ b/ethstore/src/error.rs @@ -0,0 +1,42 @@ +use std::fmt; +use std::io::Error as IoError; +use ethkey::Error as EthKeyError; + +#[derive(Debug)] +pub enum Error { + Io(IoError), + InvalidPassword, + InvalidSecret, + InvalidAccount, + CreationFailed, + EthKey(EthKeyError), + Custom(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let s = match *self { + Error::Io(ref err) => format!("{}", err), + Error::InvalidPassword => "Invalid password".into(), + Error::InvalidSecret => "Invalid secret".into(), + Error::InvalidAccount => "Invalid account".into(), + Error::CreationFailed => "Account creation failed".into(), + Error::EthKey(ref err) => format!("{}", err), + Error::Custom(ref s) => s.clone(), + }; + + write!(f, "{}", s) + } +} + +impl From for Error { + fn from(err: IoError) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: EthKeyError) -> Self { + Error::EthKey(err) + } +} diff --git a/ethstore/src/ethkey.rs b/ethstore/src/ethkey.rs new file mode 100644 index 000000000..d77fa7a52 --- /dev/null +++ b/ethstore/src/ethkey.rs @@ -0,0 +1,17 @@ +//! ethkey reexport to make documentation look pretty. +pub use _ethkey::{Address, Message, Signature, Public, Secret, Generator, sign, verify, Error, KeyPair, Random, Prefix}; +use json; + +impl Into for Address { + fn into(self) -> json::H160 { + let a: [u8; 20] = self.into(); + From::from(a) + } +} + +impl From for Address { + fn from(json: json::H160) -> Self { + let a: [u8; 20] = json.into(); + From::from(a) + } +} diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs new file mode 100644 index 000000000..103ff179d --- /dev/null +++ b/ethstore/src/ethstore.rs @@ -0,0 +1,92 @@ +use std::collections::BTreeMap; +use std::sync::RwLock; +use ethkey::KeyPair; +use crypto::KEY_ITERATIONS; +use random::Random; +use ethkey::{Signature, Address, Message, Secret}; +use dir::KeyDirectory; +use account::SafeAccount; +use {Error, SecretStore}; + +pub struct EthStore { + dir: Box, + iterations: u32, + cache: RwLock>, +} + +impl EthStore { + pub fn open(directory: Box) -> Result { + Self::open_with_iterations(directory, KEY_ITERATIONS as u32) + } + + 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 + try!(self.dir.insert(account.clone())); + + // update cache + let mut cache = self.cache.write().unwrap(); + cache.insert(account.address.clone(), account); + Ok(()) + } +} + +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); + let address = account.address.clone(); + try!(self.save(account)); + Ok(address) + } + + fn accounts(&self) -> Vec

{ + self.cache.read().unwrap().keys().cloned().collect() + } + + fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> { + // change password + let account = { + let cache = self.cache.read().unwrap(); + let account = try!(cache.get(address).ok_or(Error::InvalidAccount)); + try!(account.change_password(old_password, new_password, self.iterations)) + }; + + // save to file + self.save(account) + } + + fn remove_account(&self, address: &Address, password: &str) -> Result<(), Error> { + let can_remove = { + let cache = self.cache.read().unwrap(); + let account = try!(cache.get(address).ok_or(Error::InvalidAccount)); + account.check_password(password) + }; + + if can_remove { + try!(self.dir.remove(address)); + let mut cache = self.cache.write().unwrap(); + cache.remove(address); + Ok(()) + } else { + Err(Error::InvalidPassword) + } + } + + fn sign(&self, account: &Address, password: &str, message: &Message) -> Result { + let cache = self.cache.read().unwrap(); + let account = try!(cache.get(account).ok_or(Error::InvalidAccount)); + account.sign(password, message) + } +} diff --git a/ethstore/src/import.rs b/ethstore/src/import.rs new file mode 100644 index 000000000..94bb37925 --- /dev/null +++ b/ethstore/src/import.rs @@ -0,0 +1,12 @@ +use ethkey::Address; +use dir::KeyDirectory; +use Error; + +pub fn import_accounts(src: &KeyDirectory, dst: &KeyDirectory) -> Result, Error> { + let accounts = try!(src.load()); + accounts.into_iter().map(|a| { + let address = a.address.clone(); + try!(dst.insert(a)); + Ok(address) + }).collect() +} diff --git a/ethstore/src/json/cipher.rs b/ethstore/src/json/cipher.rs new file mode 100644 index 000000000..22fed9ee0 --- /dev/null +++ b/ethstore/src/json/cipher.rs @@ -0,0 +1,75 @@ +use serde::{Serialize, Serializer, Deserialize, Deserializer, Error as SerdeError}; +use serde::de::Visitor; +use super::{Error, H128}; + +#[derive(Debug, PartialEq)] +pub enum CipherSer { + Aes128Ctr, +} + +impl Serialize for CipherSer { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + match *self { + CipherSer::Aes128Ctr => serializer.serialize_str("aes-128-ctr"), + } + } +} + +impl Deserialize for CipherSer { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + deserializer.deserialize(CipherSerVisitor) + } +} + +struct CipherSerVisitor; + +impl Visitor for CipherSerVisitor { + type Value = CipherSer; + + fn visit_str(&mut self, value: &str) -> Result where E: SerdeError { + match value { + "aes-128-ctr" => Ok(CipherSer::Aes128Ctr), + _ => Err(SerdeError::custom(Error::UnsupportedCipher)) + } + } + + fn visit_string(&mut self, value: String) -> Result where E: SerdeError { + self.visit_str(value.as_ref()) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Aes128Ctr { + pub iv: H128, +} + +#[derive(Debug, PartialEq)] +pub enum CipherSerParams { + Aes128Ctr(Aes128Ctr), +} + +impl Serialize for CipherSerParams { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + match *self { + CipherSerParams::Aes128Ctr(ref params) => params.serialize(serializer), + } + } +} + +impl Deserialize for CipherSerParams { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + Aes128Ctr::deserialize(deserializer) + .map(CipherSerParams::Aes128Ctr) + .map_err(|_| Error::InvalidCipherParams) + .map_err(SerdeError::custom) + } +} + +#[derive(Debug, PartialEq)] +pub enum Cipher { + Aes128Ctr(Aes128Ctr), +} diff --git a/ethstore/src/json/crypto.rs b/ethstore/src/json/crypto.rs new file mode 100644 index 000000000..c11078e71 --- /dev/null +++ b/ethstore/src/json/crypto.rs @@ -0,0 +1,184 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer, Error}; +use serde::de::{Visitor, MapVisitor}; +use serde::ser; +use super::{Cipher, CipherSer, CipherSerParams, Kdf, KdfSer, KdfSerParams, H256}; + +#[derive(Debug, PartialEq)] +pub struct Crypto { + pub cipher: Cipher, + pub ciphertext: H256, + pub kdf: Kdf, + pub mac: H256, +} + +enum CryptoField { + Cipher, + CipherParams, + CipherText, + Kdf, + KdfParams, + Mac, +} + +impl Deserialize for CryptoField { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer + { + deserializer.deserialize(CryptoFieldVisitor) + } +} + +struct CryptoFieldVisitor; + +impl Visitor for CryptoFieldVisitor { + type Value = CryptoField; + + fn visit_str(&mut self, value: &str) -> Result + where E: Error + { + match value { + "cipher" => Ok(CryptoField::Cipher), + "cipherparams" => Ok(CryptoField::CipherParams), + "ciphertext" => Ok(CryptoField::CipherText), + "kdf" => Ok(CryptoField::Kdf), + "kdfparams" => Ok(CryptoField::KdfParams), + "mac" => Ok(CryptoField::Mac), + _ => Err(Error::custom(format!("Unknown field: '{}'", value))), + } + } +} + +impl Deserialize for Crypto { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer + { + static FIELDS: &'static [&'static str] = &["id", "version", "crypto", "Crypto", "address"]; + deserializer.deserialize_struct("Crypto", FIELDS, CryptoVisitor) + } +} + +struct CryptoVisitor; + +impl Visitor for CryptoVisitor { + type Value = Crypto; + + fn visit_map(&mut self, mut visitor: V) -> Result + where V: MapVisitor + { + let mut cipher = None; + let mut cipherparams = None; + let mut ciphertext = None; + let mut kdf = None; + let mut kdfparams = None; + let mut mac = None; + + loop { + match try!(visitor.visit_key()) { + Some(CryptoField::Cipher) => { cipher = Some(try!(visitor.visit_value())); } + Some(CryptoField::CipherParams) => { cipherparams = Some(try!(visitor.visit_value())); } + Some(CryptoField::CipherText) => { ciphertext = Some(try!(visitor.visit_value())); } + Some(CryptoField::Kdf) => { kdf = Some(try!(visitor.visit_value())); } + Some(CryptoField::KdfParams) => { kdfparams = Some(try!(visitor.visit_value())); } + Some(CryptoField::Mac) => { mac = Some(try!(visitor.visit_value())); } + None => { break; } + } + } + + let cipher = match (cipher, cipherparams) { + (Some(CipherSer::Aes128Ctr), Some(CipherSerParams::Aes128Ctr(params))) => Cipher::Aes128Ctr(params), + (None, _) => return Err(Error::missing_field("cipher")), + (Some(_), None) => return Err(Error::missing_field("cipherparams")), + }; + + let ciphertext = match ciphertext { + Some(ciphertext) => ciphertext, + None => try!(visitor.missing_field("ciphertext")), + }; + + let kdf = match (kdf, kdfparams) { + (Some(KdfSer::Pbkdf2), Some(KdfSerParams::Pbkdf2(params))) => Kdf::Pbkdf2(params), + (Some(KdfSer::Scrypt), Some(KdfSerParams::Scrypt(params))) => Kdf::Scrypt(params), + (Some(_), Some(_)) => return Err(Error::custom("Invalid cipherparams")), + (None, _) => return Err(Error::missing_field("kdf")), + (Some(_), None) => return Err(Error::missing_field("kdfparams")), + }; + + let mac = match mac { + Some(mac) => mac, + None => try!(visitor.missing_field("mac")), + }; + + try!(visitor.end()); + + let result = Crypto { + cipher: cipher, + ciphertext: ciphertext, + kdf: kdf, + mac: mac, + }; + + Ok(result) + } +} + +impl Serialize for Crypto { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer + { + serializer.serialize_struct("Crypto", CryptoMapVisitor { + value: self, + state: 0, + }) + } +} + +struct CryptoMapVisitor<'a> { + value: &'a Crypto, + state: u8, +} + +impl<'a> ser::MapVisitor for CryptoMapVisitor<'a> { + fn visit(&mut self, serializer: &mut S) -> Result, S::Error> + where S: Serializer + { + match self.state { + 0 => { + self.state += 1; + match self.value.cipher { + Cipher::Aes128Ctr(_) => Ok(Some(try!(serializer.serialize_struct_elt("cipher", &CipherSer::Aes128Ctr)))), + } + }, + 1 => { + self.state += 1; + match self.value.cipher { + Cipher::Aes128Ctr(ref params) => Ok(Some(try!(serializer.serialize_struct_elt("cipherparams", params)))), + } + }, + 2 => { + self.state += 1; + Ok(Some(try!(serializer.serialize_struct_elt("ciphertext", &self.value.ciphertext)))) + }, + 3 => { + self.state += 1; + match self.value.kdf { + Kdf::Pbkdf2(_) => Ok(Some(try!(serializer.serialize_struct_elt("kdf", &KdfSer::Pbkdf2)))), + Kdf::Scrypt(_) => Ok(Some(try!(serializer.serialize_struct_elt("kdf", &KdfSer::Scrypt)))), + } + }, + 4 => { + self.state += 1; + match self.value.kdf { + Kdf::Pbkdf2(ref params) => Ok(Some(try!(serializer.serialize_struct_elt("kdfparams", params)))), + Kdf::Scrypt(ref params) => Ok(Some(try!(serializer.serialize_struct_elt("kdfparams", params)))), + } + }, + 5 => { + self.state += 1; + Ok(Some(try!(serializer.serialize_struct_elt("mac", &self.value.mac)))) + }, + _ => { + Ok(None) + } + } + } +} diff --git a/ethstore/src/json/error.rs b/ethstore/src/json/error.rs new file mode 100644 index 000000000..6700ee103 --- /dev/null +++ b/ethstore/src/json/error.rs @@ -0,0 +1,34 @@ +use std::fmt; + +#[derive(Debug, PartialEq)] +pub enum Error { + UnsupportedCipher, + InvalidCipherParams, + UnsupportedKdf, + InvalidUUID, + UnsupportedVersion, + InvalidCiphertext, + InvalidH256, + InvalidPrf, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Error::InvalidUUID => write!(f, "Invalid UUID"), + Error::UnsupportedVersion => write!(f, "Unsupported version"), + Error::UnsupportedKdf => write!(f, "Unsupported kdf"), + Error::InvalidCiphertext => write!(f, "Invalid ciphertext"), + Error::UnsupportedCipher => write!(f, "Unsupported cipher"), + Error::InvalidCipherParams => write!(f, "Invalid cipher params"), + Error::InvalidH256 => write!(f, "Invalid hash"), + Error::InvalidPrf => write!(f, "Invalid prf"), + } + } +} + +impl Into for Error { + fn into(self) -> String { + format!("{}", self) + } +} diff --git a/ethstore/src/json/hash.rs b/ethstore/src/json/hash.rs new file mode 100644 index 000000000..32d8a974e --- /dev/null +++ b/ethstore/src/json/hash.rs @@ -0,0 +1,71 @@ +use std::str::FromStr; +use rustc_serialize::hex::{FromHex, ToHex}; +use serde::{Serialize, Serializer, Deserialize, Deserializer, Error as SerdeError}; +use serde::de::Visitor; +use super::Error; + +macro_rules! impl_hash { + ($name: ident, $size: expr) => { + #[derive(Debug, PartialEq)] + pub struct $name([u8; $size]); + + impl Serialize for $name { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + serializer.serialize_str(&self.0.to_hex()) + } + } + + impl Deserialize for $name { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + struct HashVisitor; + + impl Visitor for HashVisitor { + type Value = $name; + + fn visit_str(&mut self, value: &str) -> Result where E: SerdeError { + FromStr::from_str(value).map_err(SerdeError::custom) + } + + fn visit_string(&mut self, value: String) -> Result where E: SerdeError { + self.visit_str(value.as_ref()) + } + } + + deserializer.deserialize(HashVisitor) + } + } + + impl FromStr for $name { + type Err = Error; + + fn from_str(value: &str) -> Result { + match value.from_hex() { + Ok(ref hex) if hex.len() == $size => { + let mut hash = [0u8; $size]; + hash.clone_from_slice(hex); + Ok($name(hash)) + } + _ => Err(Error::InvalidH256), + } + } + } + + impl From<[u8; $size]> for $name { + fn from(bytes: [u8; $size]) -> Self { + $name(bytes) + } + } + + impl Into<[u8; $size]> for $name { + fn into(self) -> [u8; $size] { + self.0 + } + } + } +} + +impl_hash!(H128, 16); +impl_hash!(H160, 20); +impl_hash!(H256, 32); diff --git a/ethstore/src/json/id.rs b/ethstore/src/json/id.rs new file mode 100644 index 000000000..e6020ddcb --- /dev/null +++ b/ethstore/src/json/id.rs @@ -0,0 +1,129 @@ +//! Universaly unique identifier. +use std::str::FromStr; +use std::fmt; +use rustc_serialize::hex::{ToHex, FromHex}; +use serde::{Deserialize, Serialize, Deserializer, Serializer, Error as SerdeError}; +use serde::de::Visitor; +use super::Error; + +/// Universaly unique identifier. +#[derive(Debug, PartialEq)] +pub struct UUID([u8; 16]); + +impl From<[u8; 16]> for UUID { + fn from(uuid: [u8; 16]) -> Self { + UUID(uuid) + } +} + +impl<'a> Into for &'a UUID { + fn into(self) -> String { + let d1 = &self.0[0..4]; + let d2 = &self.0[4..6]; + let d3 = &self.0[6..8]; + let d4 = &self.0[8..10]; + let d5 = &self.0[10..16]; + [d1, d2, d3, d4, d5].into_iter().map(|d| d.to_hex()).collect::>().join("-") + } +} + +impl Into for UUID { + fn into(self) -> String { + Into::into(&self) + } +} + +impl Into<[u8; 16]> for UUID { + fn into(self) -> [u8; 16] { + self.0 + } +} + +impl fmt::Display for UUID { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let s: String = (self as &UUID).into(); + write!(f, "{}", s) + } +} + +fn copy_into(from: &str, into: &mut [u8]) -> Result<(), Error> { + let from = try!(from.from_hex().map_err(|_| Error::InvalidUUID)); + + if from.len() != into.len() { + return Err(Error::InvalidUUID); + } + + into.copy_from_slice(&from); + Ok(()) +} + +impl FromStr for UUID { + type Err = Error; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split("-").collect(); + + if parts.len() != 5 { + return Err(Error::InvalidUUID); + } + + let mut uuid = [0u8; 16]; + + try!(copy_into(parts[0], &mut uuid[0..4])); + try!(copy_into(parts[1], &mut uuid[4..6])); + try!(copy_into(parts[2], &mut uuid[6..8])); + try!(copy_into(parts[3], &mut uuid[8..10])); + try!(copy_into(parts[4], &mut uuid[10..16])); + + Ok(UUID(uuid)) + } +} + +impl Serialize for UUID { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + let s: String = self.into(); + serializer.serialize_str(&s) + } +} + +impl Deserialize for UUID { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + deserializer.deserialize(UUIDVisitor) + } +} + +struct UUIDVisitor; + +impl Visitor for UUIDVisitor { + type Value = UUID; + + fn visit_str(&mut self, value: &str) -> Result where E: SerdeError { + UUID::from_str(value).map_err(SerdeError::custom) + } + + fn visit_string(&mut self, value: String) -> Result where E: SerdeError { + self.visit_str(value.as_ref()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use super::UUID; + + #[test] + fn uuid_from_str() { + let uuid = UUID::from_str("3198bc9c-6672-5ab3-d995-4942343ae5b6").unwrap(); + assert_eq!(uuid, UUID::from([0x31, 0x98, 0xbc, 0x9c, 0x66, 0x72, 0x5a, 0xb3, 0xd9, 0x95, 0x49, 0x42, 0x34, 0x3a, 0xe5, 0xb6])); + } + + #[test] + fn uuid_from_and_to_str() { + let from = "3198bc9c-6672-5ab3-d995-4942343ae5b6"; + let uuid = UUID::from_str(from).unwrap(); + let to: String = uuid.into(); + assert_eq!(from, &to); + } +} diff --git a/ethstore/src/json/kdf.rs b/ethstore/src/json/kdf.rs new file mode 100644 index 000000000..c00fc8bd3 --- /dev/null +++ b/ethstore/src/json/kdf.rs @@ -0,0 +1,133 @@ +use serde::{Serialize, Serializer, Deserialize, Deserializer, Error as SerdeError}; +use serde::de::Visitor; +use serde_json::{Value, value}; +use super::{Error, H256}; + +#[derive(Debug, PartialEq)] +pub enum KdfSer { + Pbkdf2, + Scrypt, +} + +impl Serialize for KdfSer { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + match *self { + KdfSer::Pbkdf2 => serializer.serialize_str("pbkdf2"), + KdfSer::Scrypt => serializer.serialize_str("scrypt"), + } + } +} + +impl Deserialize for KdfSer { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + deserializer.deserialize(KdfSerVisitor) + } +} + +struct KdfSerVisitor; + +impl Visitor for KdfSerVisitor { + type Value = KdfSer; + + fn visit_str(&mut self, value: &str) -> Result where E: SerdeError { + match value { + "pbkdf2" => Ok(KdfSer::Pbkdf2), + "scrypt" => Ok(KdfSer::Scrypt), + _ => Err(SerdeError::custom(Error::UnsupportedKdf)) + } + } + + fn visit_string(&mut self, value: String) -> Result where E: SerdeError { + self.visit_str(value.as_ref()) + } +} + +#[derive(Debug, PartialEq)] +pub enum Prf { + HmacSha256, +} + +impl Serialize for Prf { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + match *self { + Prf::HmacSha256 => serializer.serialize_str("hmac-sha256"), + } + } +} + +impl Deserialize for Prf { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + deserializer.deserialize(PrfVisitor) + } +} + +struct PrfVisitor; + +impl Visitor for PrfVisitor { + type Value = Prf; + + fn visit_str(&mut self, value: &str) -> Result where E: SerdeError { + match value { + "hmac-sha256" => Ok(Prf::HmacSha256), + _ => Err(SerdeError::custom(Error::InvalidPrf)), + } + } + + fn visit_string(&mut self, value: String) -> Result where E: SerdeError { + self.visit_str(value.as_ref()) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Pbkdf2 { + pub c: u32, + pub dklen: u32, + pub prf: Prf, + pub salt: H256, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Scrypt { + pub dklen: u32, + pub p: u32, + pub n: u32, + pub r: u32, + pub salt: H256, +} + +#[derive(Debug, PartialEq)] +pub enum KdfSerParams { + Pbkdf2(Pbkdf2), + Scrypt(Scrypt), +} + +impl Serialize for KdfSerParams { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + match *self { + KdfSerParams::Pbkdf2(ref params) => params.serialize(serializer), + KdfSerParams::Scrypt(ref params) => params.serialize(serializer), + } + } +} + +impl Deserialize for KdfSerParams { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + let v = try!(Value::deserialize(deserializer)); + + Deserialize::deserialize(&mut value::Deserializer::new(v.clone())).map(KdfSerParams::Pbkdf2) + .or_else(|_| Deserialize::deserialize(&mut value::Deserializer::new(v)).map(KdfSerParams::Scrypt)) + .map_err(|e| D::Error::custom(format!("{}", e))) + } +} + +#[derive(Debug, PartialEq)] +pub enum Kdf { + Pbkdf2(Pbkdf2), + Scrypt(Scrypt), +} diff --git a/ethstore/src/json/key_file.rs b/ethstore/src/json/key_file.rs new file mode 100644 index 000000000..eae5e58ae --- /dev/null +++ b/ethstore/src/json/key_file.rs @@ -0,0 +1,255 @@ +use std::io::{Read, Write}; +use serde::{Deserialize, Deserializer, Error}; +use serde::de::{Visitor, MapVisitor}; +use serde_json; +use super::{UUID, Version, Crypto, H160}; + +#[derive(Debug, PartialEq, Serialize)] +pub struct KeyFile { + pub id: UUID, + pub version: Version, + pub crypto: Crypto, + pub address: H160, +} + +enum KeyFileField { + ID, + Version, + Crypto, + Address, +} + +impl Deserialize for KeyFileField { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer + { + deserializer.deserialize(KeyFileFieldVisitor) + } +} + +struct KeyFileFieldVisitor; + +impl Visitor for KeyFileFieldVisitor { + type Value = KeyFileField; + + fn visit_str(&mut self, value: &str) -> Result + where E: Error + { + match value { + "id" => Ok(KeyFileField::ID), + "version" => Ok(KeyFileField::Version), + "crypto" => Ok(KeyFileField::Crypto), + "Crypto" => Ok(KeyFileField::Crypto), + "address" => Ok(KeyFileField::Address), + _ => Err(Error::custom(format!("Unknown field: '{}'", value))), + } + } +} + +impl Deserialize for KeyFile { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer + { + static FIELDS: &'static [&'static str] = &["id", "version", "crypto", "Crypto", "address"]; + deserializer.deserialize_struct("KeyFile", FIELDS, KeyFileVisitor) + } +} + +struct KeyFileVisitor; + +impl Visitor for KeyFileVisitor { + type Value = KeyFile; + + fn visit_map(&mut self, mut visitor: V) -> Result + where V: MapVisitor + { + let mut id = None; + let mut version = None; + let mut crypto = None; + let mut address = None; + + loop { + match try!(visitor.visit_key()) { + Some(KeyFileField::ID) => { id = Some(try!(visitor.visit_value())); } + Some(KeyFileField::Version) => { version = Some(try!(visitor.visit_value())); } + Some(KeyFileField::Crypto) => { crypto = Some(try!(visitor.visit_value())); } + Some(KeyFileField::Address) => { address = Some(try!(visitor.visit_value())); } + None => { break; } + } + } + + let id = match id { + Some(id) => id, + None => try!(visitor.missing_field("id")), + }; + + let version = match version { + Some(version) => version, + None => try!(visitor.missing_field("version")), + }; + + let crypto = match crypto { + Some(crypto) => crypto, + None => try!(visitor.missing_field("crypto")), + }; + + let address = match address { + Some(address) => address, + None => try!(visitor.missing_field("address")), + }; + + try!(visitor.end()); + + let result = KeyFile { + id: id, + version: version, + crypto: crypto, + address: address, + }; + + Ok(result) + } +} + +impl KeyFile { + pub fn load(reader: R) -> Result where R: Read { + serde_json::from_reader(reader) + } + + pub fn write(&self, writer: &mut W) -> Result<(), serde_json::Error> where W: Write { + serde_json::to_writer(writer, self) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use serde_json; + use json::{KeyFile, UUID, Version, Crypto, Cipher, Aes128Ctr, Kdf, Scrypt, H128, H160, H256}; + + #[test] + fn basic_keyfile() { + let json = r#" + { + "address": "6edddfc6349aff20bc6467ccf276c5b52487f7a8", + "crypto": { + "cipher": "aes-128-ctr", + "ciphertext": "7203da0676d141b138cd7f8e1a4365f59cc1aa6978dc5443f364ca943d7cb4bc", + "cipherparams": { + "iv": "b5a7ec855ec9e2c405371356855fec83" + }, + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "1e8642fdf1f87172492c1412fc62f8db75d796cdfa9c53c3f2b11e44a2a1b209" + }, + "mac": "46325c5d4e8c991ad2683d525c7854da387138b6ca45068985aa4959fa2b8c8f" + }, + "id": "8777d9f6-7860-4b9b-88b7-0b57ee6b3a73", + "version": 3 + }"#; + + let expected = KeyFile { + id: UUID::from_str("8777d9f6-7860-4b9b-88b7-0b57ee6b3a73").unwrap(), + version: Version::V3, + address: H160::from_str("6edddfc6349aff20bc6467ccf276c5b52487f7a8").unwrap(), + crypto: Crypto { + cipher: Cipher::Aes128Ctr(Aes128Ctr { + iv: H128::from_str("b5a7ec855ec9e2c405371356855fec83").unwrap(), + }), + ciphertext: H256::from_str("7203da0676d141b138cd7f8e1a4365f59cc1aa6978dc5443f364ca943d7cb4bc").unwrap(), + kdf: Kdf::Scrypt(Scrypt { + n: 262144, + dklen: 32, + p: 1, + r: 8, + salt: H256::from_str("1e8642fdf1f87172492c1412fc62f8db75d796cdfa9c53c3f2b11e44a2a1b209").unwrap(), + }), + mac: H256::from_str("46325c5d4e8c991ad2683d525c7854da387138b6ca45068985aa4959fa2b8c8f").unwrap(), + }, + }; + + let keyfile: KeyFile = serde_json::from_str(json).unwrap(); + assert_eq!(keyfile, expected); + } + + #[test] + fn capital_crypto_keyfile() { + let json = r#" + { + "address": "6edddfc6349aff20bc6467ccf276c5b52487f7a8", + "Crypto": { + "cipher": "aes-128-ctr", + "ciphertext": "7203da0676d141b138cd7f8e1a4365f59cc1aa6978dc5443f364ca943d7cb4bc", + "cipherparams": { + "iv": "b5a7ec855ec9e2c405371356855fec83" + }, + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "1e8642fdf1f87172492c1412fc62f8db75d796cdfa9c53c3f2b11e44a2a1b209" + }, + "mac": "46325c5d4e8c991ad2683d525c7854da387138b6ca45068985aa4959fa2b8c8f" + }, + "id": "8777d9f6-7860-4b9b-88b7-0b57ee6b3a73", + "version": 3 + }"#; + + let expected = KeyFile { + id: UUID::from_str("8777d9f6-7860-4b9b-88b7-0b57ee6b3a73").unwrap(), + version: Version::V3, + address: H160::from_str("6edddfc6349aff20bc6467ccf276c5b52487f7a8").unwrap(), + crypto: Crypto { + cipher: Cipher::Aes128Ctr(Aes128Ctr { + iv: H128::from_str("b5a7ec855ec9e2c405371356855fec83").unwrap(), + }), + ciphertext: H256::from_str("7203da0676d141b138cd7f8e1a4365f59cc1aa6978dc5443f364ca943d7cb4bc").unwrap(), + kdf: Kdf::Scrypt(Scrypt { + n: 262144, + dklen: 32, + p: 1, + r: 8, + salt: H256::from_str("1e8642fdf1f87172492c1412fc62f8db75d796cdfa9c53c3f2b11e44a2a1b209").unwrap(), + }), + mac: H256::from_str("46325c5d4e8c991ad2683d525c7854da387138b6ca45068985aa4959fa2b8c8f").unwrap(), + }, + }; + + let keyfile: KeyFile = serde_json::from_str(json).unwrap(); + assert_eq!(keyfile, expected); + } + + #[test] + fn to_and_from_json() { + let file = KeyFile { + id: UUID::from_str("8777d9f6-7860-4b9b-88b7-0b57ee6b3a73").unwrap(), + version: Version::V3, + address: H160::from_str("6edddfc6349aff20bc6467ccf276c5b52487f7a8").unwrap(), + crypto: Crypto { + cipher: Cipher::Aes128Ctr(Aes128Ctr { + iv: H128::from_str("b5a7ec855ec9e2c405371356855fec83").unwrap(), + }), + ciphertext: H256::from_str("7203da0676d141b138cd7f8e1a4365f59cc1aa6978dc5443f364ca943d7cb4bc").unwrap(), + kdf: Kdf::Scrypt(Scrypt { + n: 262144, + dklen: 32, + p: 1, + r: 8, + salt: H256::from_str("1e8642fdf1f87172492c1412fc62f8db75d796cdfa9c53c3f2b11e44a2a1b209").unwrap(), + }), + mac: H256::from_str("46325c5d4e8c991ad2683d525c7854da387138b6ca45068985aa4959fa2b8c8f").unwrap(), + }, + }; + + let serialized = serde_json::to_string(&file).unwrap(); + let deserialized = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(file, deserialized); + } +} diff --git a/ethstore/src/json/mod.rs b/ethstore/src/json/mod.rs new file mode 100644 index 000000000..dd069332b --- /dev/null +++ b/ethstore/src/json/mod.rs @@ -0,0 +1,8 @@ +//! Contract interface specification. + +#[cfg(feature = "serde_macros")] +include!("mod.rs.in"); + +#[cfg(not(feature = "serde_macros"))] +include!(concat!(env!("OUT_DIR"), "/mod.rs")); + diff --git a/ethstore/src/json/mod.rs.in b/ethstore/src/json/mod.rs.in new file mode 100644 index 000000000..c4a67a287 --- /dev/null +++ b/ethstore/src/json/mod.rs.in @@ -0,0 +1,18 @@ +mod cipher; +mod crypto; +mod error; +mod hash; +mod id; +mod kdf; +mod key_file; +mod version; + +pub use self::cipher::{Cipher, CipherSer, CipherSerParams, Aes128Ctr}; +pub use self::crypto::Crypto; +pub use self::error::Error; +pub use self::hash::{H128, H160, H256}; +pub use self::id::UUID; +pub use self::kdf::{Kdf, KdfSer, Prf, Pbkdf2, Scrypt, KdfSerParams}; +pub use self::key_file::KeyFile; +pub use self::version::Version; + diff --git a/ethstore/src/json/version.rs b/ethstore/src/json/version.rs new file mode 100644 index 000000000..3b4105a79 --- /dev/null +++ b/ethstore/src/json/version.rs @@ -0,0 +1,38 @@ +use serde::{Serialize, Serializer, Deserialize, Deserializer, Error as SerdeError}; +use serde::de::Visitor; +use super::Error; + +#[derive(Debug, PartialEq)] +pub enum Version { + V3, +} + +impl Serialize for Version { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> + where S: Serializer { + match *self { + Version::V3 => serializer.serialize_u64(3) + } + } +} + +impl Deserialize for Version { + fn deserialize(deserializer: &mut D) -> Result + where D: Deserializer { + deserializer.deserialize(VersionVisitor) + } +} + +struct VersionVisitor; + +impl Visitor for VersionVisitor { + type Value = Version; + + fn visit_u64(&mut self, value: u64) -> Result where E: SerdeError { + match value { + 3 => Ok(Version::V3), + _ => Err(SerdeError::custom(Error::UnsupportedVersion)) + } + } +} + diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs new file mode 100644 index 000000000..62932fc16 --- /dev/null +++ b/ethstore/src/lib.rs @@ -0,0 +1,32 @@ +#![cfg_attr(feature="nightly", feature(custom_derive, plugin))] +#![cfg_attr(feature="nightly", plugin(serde_macros))] + +extern crate libc; +extern crate rand; +extern crate serde; +extern crate serde_json; +extern crate rustc_serialize; +extern crate crypto as rcrypto; +extern crate tiny_keccak; +// reexport it nicely +extern crate ethkey as _ethkey; + +pub mod dir; +pub mod ethkey; + +mod account; +mod json; +mod crypto; + +mod error; +mod ethstore; +mod import; +mod random; +mod secret_store; + +pub use self::account::SafeAccount; +pub use self::error::Error; +pub use self::ethstore::EthStore; +pub use self::import::import_accounts; +pub use self::secret_store::SecretStore; + diff --git a/ethstore/src/random.rs b/ethstore/src/random.rs new file mode 100644 index 000000000..2693d4fcf --- /dev/null +++ b/ethstore/src/random.rs @@ -0,0 +1,23 @@ +use rand::{Rng, OsRng}; + +pub trait Random { + fn random() -> Self where Self: Sized; +} + +impl Random for [u8; 16] { + fn random() -> Self { + let mut result = [0u8; 16]; + let mut rng = OsRng::new().unwrap(); + rng.fill_bytes(&mut result); + result + } +} + +impl Random for [u8; 32] { + fn random() -> Self { + let mut result = [0u8; 32]; + let mut rng = OsRng::new().unwrap(); + rng.fill_bytes(&mut result); + result + } +} diff --git a/ethstore/src/secret_store.rs b/ethstore/src/secret_store.rs new file mode 100644 index 000000000..620b8d9b9 --- /dev/null +++ b/ethstore/src/secret_store.rs @@ -0,0 +1,15 @@ +use ethkey::{Address, Message, Signature, Secret}; +use Error; + +pub trait SecretStore: Send + Sync { + fn insert_account(&self, secret: Secret, password: &str) -> Result; + + fn accounts(&self) -> Vec
; + + 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; +} + diff --git a/ethstore/tests/api.rs b/ethstore/tests/api.rs new file mode 100644 index 000000000..a29b143db --- /dev/null +++ b/ethstore/tests/api.rs @@ -0,0 +1,72 @@ +extern crate rand; +extern crate ethstore; + +mod util; + +use ethstore::{SecretStore, EthStore}; +use ethstore::ethkey::{Random, Generator, Secret}; +use util::TransientDir; + +#[test] +fn secret_store_create() { + let dir = TransientDir::create().unwrap(); + let _ = EthStore::open(Box::new(dir)).unwrap(); +} + +#[test] +#[should_panic] +fn secret_store_open_not_existing() { + let dir = TransientDir::open(); + let _ = EthStore::open(Box::new(dir)).unwrap(); +} + +fn random_secret() -> Secret { + Random.generate().unwrap().secret().clone() +} + +#[test] +fn secret_store_create_account() { + let dir = TransientDir::create().unwrap(); + let store = EthStore::open(Box::new(dir)).unwrap(); + assert_eq!(store.accounts().len(), 0); + assert!(store.insert_account(random_secret(), "").is_ok()); + assert_eq!(store.accounts().len(), 1); + assert!(store.insert_account(random_secret(), "").is_ok()); + assert_eq!(store.accounts().len(), 2); +} + +#[test] +fn secret_store_sign() { + let dir = TransientDir::create().unwrap(); + let store = EthStore::open(Box::new(dir)).unwrap(); + assert!(store.insert_account(random_secret(), "").is_ok()); + let accounts = store.accounts(); + assert_eq!(accounts.len(), 1); + assert!(store.sign(&accounts[0], "", &Default::default()).is_ok()); + assert!(store.sign(&accounts[0], "1", &Default::default()).is_err()); +} + +#[test] +fn secret_store_change_password() { + let dir = TransientDir::create().unwrap(); + let store = EthStore::open(Box::new(dir)).unwrap(); + assert!(store.insert_account(random_secret(), "").is_ok()); + let accounts = store.accounts(); + assert_eq!(accounts.len(), 1); + assert!(store.sign(&accounts[0], "", &Default::default()).is_ok()); + assert!(store.change_password(&accounts[0], "", "1").is_ok()); + assert!(store.sign(&accounts[0], "", &Default::default()).is_err()); + assert!(store.sign(&accounts[0], "1", &Default::default()).is_ok()); +} + +#[test] +fn secret_store_remove_account() { + let dir = TransientDir::create().unwrap(); + let store = EthStore::open(Box::new(dir)).unwrap(); + assert!(store.insert_account(random_secret(), "").is_ok()); + let accounts = store.accounts(); + assert_eq!(accounts.len(), 1); + assert!(store.remove_account(&accounts[0], "").is_ok()); + assert_eq!(store.accounts().len(), 0); + assert!(store.remove_account(&accounts[0], "").is_err()); +} diff --git a/ethstore/tests/cli.rs b/ethstore/tests/cli.rs new file mode 100644 index 000000000..e69de29bb diff --git a/ethstore/tests/util/mod.rs b/ethstore/tests/util/mod.rs new file mode 100644 index 000000000..03bdb8af1 --- /dev/null +++ b/ethstore/tests/util/mod.rs @@ -0,0 +1,3 @@ +mod transient_dir; + +pub use self::transient_dir::TransientDir; diff --git a/ethstore/tests/util/transient_dir.rs b/ethstore/tests/util/transient_dir.rs new file mode 100644 index 000000000..b29a5d775 --- /dev/null +++ b/ethstore/tests/util/transient_dir.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; +use std::{env, fs}; +use rand::{Rng, OsRng}; +use ethstore::dir::{KeyDirectory, DiskDirectory}; +use ethstore::ethkey::Address; +use ethstore::{Error, SafeAccount}; + +pub fn random_dir() -> PathBuf { + let mut rng = OsRng::new().unwrap(); + let mut dir = env::temp_dir(); + dir.push(format!("{:x}-{:x}", rng.next_u64(), rng.next_u64())); + dir +} + +pub struct TransientDir { + dir: DiskDirectory, + path: PathBuf, +} + +impl TransientDir { + pub fn create() -> Result { + let path = random_dir(); + let result = TransientDir { + dir: try!(DiskDirectory::create(&path)), + path: path, + }; + + Ok(result) + } + + pub fn open() -> Self { + let path = random_dir(); + TransientDir { + dir: DiskDirectory::at(&path), + path: path, + } + } +} + +impl Drop for TransientDir { + fn drop(&mut self) { + fs::remove_dir_all(&self.path).expect("Expected to remove temp dir"); + } +} + +impl KeyDirectory for TransientDir { + fn load(&self) -> Result, Error> { + self.dir.load() + } + + fn insert(&self, account: SafeAccount) -> Result<(), Error> { + self.dir.insert(account) + } + + fn remove(&self, address: &Address) -> Result<(), Error> { + self.dir.remove(address) + } +} diff --git a/parity/configuration.rs b/parity/configuration.rs index 3aff2ee27..7c42f06e1 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -24,7 +24,7 @@ use docopt::Docopt; use die::*; use util::*; -use util::keys::store::{ImportKeySet, AccountService}; +use ethcore::account_provider::AccountProvider; use util::network_settings::NetworkSettings; use ethcore::client::{append_path, get_db_path, ClientConfig, Switch, VMType}; use ethcore::ethereum; @@ -249,7 +249,10 @@ impl Configuration { sync_config } - pub fn account_service(&self) -> AccountService { + pub fn account_service(&self) -> AccountProvider { + use ethcore::ethstore::{import_accounts, EthStore}; + use ethcore::ethstore::dir::{GethDirectory, DirectoryType, DiskDirectory}; + // Secret Store let passwords = self.args.flag_password.iter().flat_map(|filename| { BufReader::new(&File::open(filename).unwrap_or_else(|_| die!("{} Unable to read password file. Ensure it exists and permissions are correct.", filename))) @@ -258,18 +261,30 @@ impl Configuration { .collect::>() .into_iter() }).collect::>(); - let import_keys = match (self.args.flag_no_import_keys, self.args.flag_testnet) { - (true, _) => ImportKeySet::None, - (false, false) => ImportKeySet::Legacy, - (false, true) => ImportKeySet::LegacyTestnet, - }; - let account_service = AccountService::with_security(Path::new(&self.keys_path()), self.keys_iterations(), import_keys); + + if !self.args.flag_no_import_keys { + let dir_type = match self.args.flag_testnet { + true => DirectoryType::Testnet, + false => DirectoryType::Main, + }; + + let from = GethDirectory::open(dir_type); + let to = DiskDirectory::create(self.keys_path()).unwrap(); + if let Err(e) = import_accounts(&from, &to) { + warn!("Could not import accounts {}", e); + } + } + + let dir = Box::new(DiskDirectory::create(self.keys_path()).unwrap()); + let iterations = self.keys_iterations(); + let account_service = AccountProvider::new(Box::new(EthStore::open_with_iterations(dir, iterations).unwrap())); + if let Some(ref unlocks) = self.args.flag_unlock { for d in unlocks.split(',') { let a = Address::from_str(clean_0x(d)).unwrap_or_else(|_| { die!("{}: Invalid address for --unlock. Must be 40 hex characters, without the 0x at the beginning.", d) }); - if passwords.iter().find(|p| account_service.unlock_account_no_expire(&a, p).is_ok()).is_none() { + if passwords.iter().find(|p| account_service.unlock_account_permanently(a, (*p).clone()).is_ok()).is_none() { die!("No password given to unlock account {}. Pass the password using `--password`.", a); } } diff --git a/parity/io_handler.rs b/parity/io_handler.rs index 2460cfec7..ee6d6130f 100644 --- a/parity/io_handler.rs +++ b/parity/io_handler.rs @@ -18,20 +18,17 @@ use std::sync::Arc; use ethcore::client::Client; use ethcore::service::{NetSyncMessage, SyncMessage}; use ethsync::EthSync; -use util::keys::store::AccountService; +use ethcore::account_provider::AccountProvider; use util::{TimerToken, IoHandler, IoContext, NetworkService, NetworkIoMessage}; use informant::Informant; const INFO_TIMER: TimerToken = 0; -const ACCOUNT_TICK_TIMER: TimerToken = 10; -const ACCOUNT_TICK_MS: u64 = 60000; - pub struct ClientIoHandler { pub client: Arc, pub sync: Arc, - pub accounts: Arc, + pub accounts: Arc, pub info: Informant, pub network: Arc>, } @@ -39,13 +36,11 @@ pub struct ClientIoHandler { impl IoHandler for ClientIoHandler { fn initialize(&self, io: &IoContext) { io.register_timer(INFO_TIMER, 5000).expect("Error registering timer"); - io.register_timer(ACCOUNT_TICK_TIMER, ACCOUNT_TICK_MS).expect("Error registering account timer"); } fn timeout(&self, _io: &IoContext, timer: TimerToken) { match timer { INFO_TIMER => { self.info.tick(&self.client, Some(&self.sync)); } - ACCOUNT_TICK_TIMER => { self.accounts.tick(); }, _ => {} } } diff --git a/parity/main.rs b/parity/main.rs index cfa44f6ee..9680f8a03 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -478,9 +478,15 @@ fn execute_signer(conf: Configuration) { } fn execute_account_cli(conf: Configuration) { - use util::keys::store::SecretStore; + use ethcore::ethstore::{SecretStore, EthStore, import_accounts}; + use ethcore::ethstore::dir::DiskDirectory; + use ethcore::account_provider::AccountProvider; use rpassword::read_password; - let mut secret_store = SecretStore::with_security(Path::new(&conf.keys_path()), conf.keys_iterations()); + + let dir = Box::new(DiskDirectory::create(conf.keys_path()).unwrap()); + let iterations = conf.keys_iterations(); + let secret_store = AccountProvider::new(Box::new(EthStore::open_with_iterations(dir, iterations).unwrap())); + if conf.args.cmd_new { println!("Please note that password is NOT RECOVERABLE."); print!("Type password: "); @@ -498,15 +504,22 @@ fn execute_account_cli(conf: Configuration) { println!("{:?}", new_address); return; } + if conf.args.cmd_list { println!("Known addresses:"); - for &(addr, _) in &secret_store.accounts().unwrap() { + for addr in &secret_store.accounts() { println!("{:?}", addr); } return; } + if conf.args.cmd_import { - let imported = util::keys::import_keys_paths(&mut secret_store, &conf.args.arg_path).unwrap(); + let to = DiskDirectory::create(conf.keys_path()).unwrap(); + let mut imported = 0; + for path in &conf.args.arg_path { + let from = DiskDirectory::at(path); + imported += import_accounts(&from, &to).unwrap_or_else(|e| die!("Could not import accounts {}", e)).len(); + } println!("Imported {} keys", imported); } } diff --git a/parity/rpc_apis.rs b/parity/rpc_apis.rs index feefc89c0..bf21181a1 100644 --- a/parity/rpc_apis.rs +++ b/parity/rpc_apis.rs @@ -23,7 +23,7 @@ use ethsync::EthSync; use ethcore::miner::{Miner, ExternalMiner}; use ethcore::client::Client; use util::RotatingLogger; -use util::keys::store::AccountService; +use ethcore::account_provider::AccountProvider; use util::network_settings::NetworkSettings; #[cfg(feature="rpc")] @@ -83,7 +83,7 @@ pub struct Dependencies { pub signer_queue: Arc, pub client: Arc, pub sync: Arc, - pub secret_store: Arc, + pub secret_store: Arc, pub miner: Arc, pub external_miner: Arc, pub logger: Arc, diff --git a/rpc/src/v1/impls/eth.rs b/rpc/src/v1/impls/eth.rs index 2c070cab9..e1f1ba6df 100644 --- a/rpc/src/v1/impls/eth.rs +++ b/rpc/src/v1/impls/eth.rs @@ -26,7 +26,7 @@ use jsonrpc_core::*; use util::numbers::*; use util::sha3::*; use util::rlp::{encode, decode, UntrustedRlp, View}; -use util::keys::store::AccountProvider; +use ethcore::account_provider::AccountProvider; use ethcore::client::{MiningBlockChainClient, BlockID, TransactionID, UncleID}; use ethcore::block::IsBlock; use ethcore::views::*; @@ -41,32 +41,30 @@ use v1::impls::{dispatch_transaction, error_codes}; use serde; /// Eth rpc implementation. -pub struct EthClient where +pub struct EthClient where C: MiningBlockChainClient, S: SyncProvider, - A: AccountProvider, M: MinerService, EM: ExternalMinerService { client: Weak, sync: Weak, - accounts: Weak, + accounts: Weak, miner: Weak, external_miner: Arc, seed_compute: Mutex, allow_pending_receipt_query: bool, } -impl EthClient where +impl EthClient where C: MiningBlockChainClient, S: SyncProvider, - A: AccountProvider, M: MinerService, EM: ExternalMinerService { /// Creates new EthClient. - pub fn new(client: &Arc, sync: &Arc, accounts: &Arc, miner: &Arc, em: &Arc, allow_pending_receipt_query: bool) - -> EthClient { + pub fn new(client: &Arc, sync: &Arc, accounts: &Arc, miner: &Arc, em: &Arc, allow_pending_receipt_query: bool) + -> EthClient { EthClient { client: Arc::downgrade(client), sync: Arc::downgrade(sync), @@ -236,10 +234,9 @@ fn no_work_err() -> Error { } } -impl Eth for EthClient where +impl Eth for EthClient where C: MiningBlockChainClient + 'static, S: SyncProvider + 'static, - A: AccountProvider + 'static, M: MinerService + 'static, EM: ExternalMinerService + 'static { @@ -306,10 +303,7 @@ impl Eth for EthClient where fn accounts(&self, _: Params) -> Result { let store = take_weak!(self.accounts); - match store.accounts() { - Ok(account_list) => to_value(&account_list), - Err(_) => Err(Error::internal_error()) - } + to_value(&store.accounts()) } fn block_number(&self, params: Params) -> Result { diff --git a/rpc/src/v1/impls/eth_signing.rs b/rpc/src/v1/impls/eth_signing.rs index 9a045d7a1..ad0bb035a 100644 --- a/rpc/src/v1/impls/eth_signing.rs +++ b/rpc/src/v1/impls/eth_signing.rs @@ -21,12 +21,11 @@ use jsonrpc_core::*; use ethcore::miner::MinerService; use ethcore::client::MiningBlockChainClient; use util::numbers::*; -use util::keys::store::AccountProvider; +use ethcore::account_provider::AccountProvider; use v1::helpers::{SigningQueue, ConfirmationsQueue}; use v1::traits::EthSigning; use v1::types::{TransactionRequest, Bytes}; -use v1::impls::{sign_and_dispatch, error_codes}; - +use v1::impls::sign_and_dispatch; /// Implementation of functions that require signing when no trusted signer is used. pub struct EthSigningQueueClient { @@ -79,22 +78,20 @@ impl EthSigning for EthSigningQueueClient { } /// Implementation of functions that require signing when no trusted signer is used. -pub struct EthSigningUnsafeClient where +pub struct EthSigningUnsafeClient where C: MiningBlockChainClient, - A: AccountProvider, M: MinerService { client: Weak, - accounts: Weak, + accounts: Weak, miner: Weak, } -impl EthSigningUnsafeClient where +impl EthSigningUnsafeClient where C: MiningBlockChainClient, - A: AccountProvider, M: MinerService { /// Creates new EthClient. - pub fn new(client: &Arc, accounts: &Arc, miner: &Arc) + pub fn new(client: &Arc, accounts: &Arc, miner: &Arc) -> Self { EthSigningUnsafeClient { client: Arc::downgrade(client), @@ -104,35 +101,24 @@ impl EthSigningUnsafeClient where } } -impl EthSigning for EthSigningUnsafeClient where +impl EthSigning for EthSigningUnsafeClient where C: MiningBlockChainClient + 'static, - A: AccountProvider + 'static, M: MinerService + 'static { fn sign(&self, params: Params) -> Result { from_params::<(Address, H256)>(params).and_then(|(addr, msg)| { - take_weak!(self.accounts).sign(&addr, &msg) - .map(|v| to_value(&v)) - .unwrap_or_else(|e| Err(account_locked(format!("Error: {:?}", e)))) + to_value(&take_weak!(self.accounts).sign(addr, msg).unwrap_or(H520::zero())) }) } fn send_transaction(&self, params: Params) -> Result { from_params::<(TransactionRequest, )>(params) .and_then(|(request, )| { - let accounts = take_weak!(self.accounts); - match accounts.account_secret(&request.from) { - Ok(secret) => sign_and_dispatch(&*take_weak!(self.client), &*take_weak!(self.miner), request, secret), - Err(e) => Err(account_locked(format!("Error: {:?}", e))), + let sender = request.from; + match sign_and_dispatch(&*take_weak!(self.client), &*take_weak!(self.miner), request, &*take_weak!(self.accounts), sender) { + Ok(hash) => to_value(&hash), + _ => to_value(&H256::zero()), } }) } } - -fn account_locked(data: String) -> Error { - Error { - code: ErrorCode::ServerError(error_codes::ACCOUNT_LOCKED), - message: "Your account is locked. Unlock the account via CLI, personal_unlockAccount or use Trusted Signer.".into(), - data: Some(Value::String(data)), - } -} diff --git a/rpc/src/v1/impls/mod.rs b/rpc/src/v1/impls/mod.rs index f69fa25ce..efca09091 100644 --- a/rpc/src/v1/impls/mod.rs +++ b/rpc/src/v1/impls/mod.rs @@ -58,6 +58,7 @@ use ethcore::error::Error as EthcoreError; use ethcore::miner::{AccountDetails, MinerService}; use ethcore::client::MiningBlockChainClient; use ethcore::transaction::{Action, SignedTransaction, Transaction}; +use ethcore::account_provider::{AccountProvider, Error as AccountError}; use util::numbers::*; use util::rlp::encode; use util::bytes::ToPretty; @@ -88,29 +89,58 @@ fn dispatch_transaction(client: &C, miner: &M, signed_transaction: SignedT .and_then(|_| to_value(&hash)) } -fn sign_and_dispatch(client: &C, miner: &M, request: TransactionRequest, secret: H256) -> Result +fn prepare_transaction(client: &C, miner: &M, request: TransactionRequest) -> Transaction where C: MiningBlockChainClient, M: MinerService { + Transaction { + nonce: request.nonce + .or_else(|| miner + .last_nonce(&request.from) + .map(|nonce| nonce + U256::one())) + .unwrap_or_else(|| client.latest_nonce(&request.from)), + + action: request.to.map_or(Action::Create, Action::Call), + gas: request.gas.unwrap_or_else(|| miner.sensible_gas_limit()), + gas_price: request.gas_price.unwrap_or_else(|| miner.sensible_gas_price()), + value: request.value.unwrap_or_else(U256::zero), + data: request.data.map_or_else(Vec::new, |b| b.to_vec()), + } +} + +fn unlock_sign_and_dispatch(client: &C, miner: &M, request: TransactionRequest, account_provider: &AccountProvider, address: Address, password: String) -> Result where C: MiningBlockChainClient, M: MinerService { let signed_transaction = { - Transaction { - nonce: request.nonce - .or_else(|| miner - .last_nonce(&request.from) - .map(|nonce| nonce + U256::one())) - .unwrap_or_else(|| client.latest_nonce(&request.from)), - - action: request.to.map_or(Action::Create, Action::Call), - gas: request.gas.unwrap_or_else(|| miner.sensible_gas_limit()), - gas_price: request.gas_price.unwrap_or_else(|| miner.sensible_gas_price()), - value: request.value.unwrap_or_else(U256::zero), - data: request.data.map_or_else(Vec::new, |b| b.to_vec()), - }.sign(&secret) + let t = prepare_transaction(client, miner, request); + let hash = t.hash(); + let signature = try!(account_provider.sign_with_password(address, password, hash).map_err(signing_error)); + t.with_signature(signature) }; trace!(target: "miner", "send_transaction: dispatching tx: {}", encode(&signed_transaction).to_vec().pretty()); dispatch_transaction(&*client, &*miner, signed_transaction) } +fn sign_and_dispatch(client: &C, miner: &M, request: TransactionRequest, account_provider: &AccountProvider, address: Address) -> Result + where C: MiningBlockChainClient, M: MinerService { + + let signed_transaction = { + let t = prepare_transaction(client, miner, request); + let hash = t.hash(); + let signature = try!(account_provider.sign(address, hash).map_err(signing_error)); + t.with_signature(signature) + }; + + trace!(target: "miner", "send_transaction: dispatching tx: {}", encode(&signed_transaction).to_vec().pretty()); + dispatch_transaction(&*client, &*miner, signed_transaction) +} + +fn signing_error(error: AccountError) -> Error { + Error { + code: ErrorCode::ServerError(error_codes::ACCOUNT_LOCKED), + message: "Your account is locked. Unlock the account via CLI, personal_unlockAccount or use Trusted Signer.".into(), + data: Some(Value::String(format!("{:?}", error))), + } +} + fn transaction_error(error: EthcoreError) -> Error { use ethcore::error::TransactionError::*; diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index 4f9e307cb..38191ed90 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -19,25 +19,23 @@ use std::sync::{Arc, Weak}; use jsonrpc_core::*; use v1::traits::Personal; use v1::types::TransactionRequest; -use v1::impls::sign_and_dispatch; -use util::keys::store::AccountProvider; +use v1::impls::unlock_sign_and_dispatch; +use ethcore::account_provider::AccountProvider; use util::numbers::*; use ethcore::client::MiningBlockChainClient; use ethcore::miner::MinerService; /// Account management (personal) rpc implementation. -pub struct PersonalClient - where A: AccountProvider, C: MiningBlockChainClient, M: MinerService { - accounts: Weak, +pub struct PersonalClient where C: MiningBlockChainClient, M: MinerService { + accounts: Weak, client: Weak, miner: Weak, signer_port: Option, } -impl PersonalClient - where A: AccountProvider, C: MiningBlockChainClient, M: MinerService { +impl PersonalClient where C: MiningBlockChainClient, M: MinerService { /// Creates new PersonalClient - pub fn new(store: &Arc, client: &Arc, miner: &Arc, signer_port: Option) -> Self { + pub fn new(store: &Arc, client: &Arc, miner: &Arc, signer_port: Option) -> Self { PersonalClient { accounts: Arc::downgrade(store), client: Arc::downgrade(client), @@ -47,8 +45,7 @@ impl PersonalClient } } -impl Personal for PersonalClient - where A: AccountProvider, C: MiningBlockChainClient, M: MinerService { +impl Personal for PersonalClient where C: MiningBlockChainClient, M: MinerService { fn signer_enabled(&self, _: Params) -> Result { self.signer_port @@ -58,10 +55,7 @@ impl Personal for PersonalClient fn accounts(&self, _: Params) -> Result { let store = take_weak!(self.accounts); - match store.accounts() { - Ok(account_list) => to_value(&account_list), - Err(_) => Err(Error::internal_error()) - } + to_value(&store.accounts()) } fn new_account(&self, params: Params) -> Result { @@ -80,7 +74,7 @@ impl Personal for PersonalClient from_params::<(Address, String, u64)>(params).and_then( |(account, account_pass, _)|{ let store = take_weak!(self.accounts); - match store.unlock_account_temp(&account, &account_pass) { + match store.unlock_account_temporarily(account, account_pass) { Ok(_) => Ok(Value::Bool(true)), Err(_) => Ok(Value::Bool(false)), } @@ -90,10 +84,12 @@ impl Personal for PersonalClient fn sign_and_send_transaction(&self, params: Params) -> Result { from_params::<(TransactionRequest, String)>(params) .and_then(|(request, password)| { + let sender = request.from; let accounts = take_weak!(self.accounts); - match accounts.locked_account_secret(&request.from, &password) { - Ok(secret) => sign_and_dispatch(&*take_weak!(self.client), &*take_weak!(self.miner), request, secret), - Err(_) => to_value(&H256::zero()), + + match unlock_sign_and_dispatch(&*take_weak!(self.client), &*take_weak!(self.miner), request, &*accounts, sender, password) { + Ok(hash) => to_value(&hash), + _ => to_value(&H256::zero()), } }) } diff --git a/rpc/src/v1/impls/personal_signer.rs b/rpc/src/v1/impls/personal_signer.rs index 0c1fcb7f1..89f15c787 100644 --- a/rpc/src/v1/impls/personal_signer.rs +++ b/rpc/src/v1/impls/personal_signer.rs @@ -20,27 +20,25 @@ use std::sync::{Arc, Weak}; use jsonrpc_core::*; use v1::traits::PersonalSigner; use v1::types::TransactionModification; -use v1::impls::sign_and_dispatch; +use v1::impls::unlock_sign_and_dispatch; use v1::helpers::{SigningQueue, ConfirmationsQueue}; -use util::keys::store::AccountProvider; +use ethcore::account_provider::AccountProvider; use util::numbers::*; use ethcore::client::MiningBlockChainClient; use ethcore::miner::MinerService; /// Transactions confirmation (personal) rpc implementation. -pub struct SignerClient - where A: AccountProvider, C: MiningBlockChainClient, M: MinerService { +pub struct SignerClient where C: MiningBlockChainClient, M: MinerService { queue: Weak, - accounts: Weak, + accounts: Weak, client: Weak, miner: Weak, } -impl SignerClient - where A: AccountProvider, C: MiningBlockChainClient, M: MinerService { +impl SignerClient where C: MiningBlockChainClient, M: MinerService { /// Create new instance of signer client. - pub fn new(store: &Arc, client: &Arc, miner: &Arc, queue: &Arc) -> Self { + pub fn new(store: &Arc, client: &Arc, miner: &Arc, queue: &Arc) -> Self { SignerClient { queue: Arc::downgrade(queue), accounts: Arc::downgrade(store), @@ -50,8 +48,7 @@ impl SignerClient } } -impl PersonalSigner for SignerClient - where A: AccountProvider, C: MiningBlockChainClient, M: MinerService { +impl PersonalSigner for SignerClient where C: MiningBlockChainClient, M: MinerService { fn transactions_to_confirm(&self, _params: Params) -> Result { let queue = take_weak!(self.queue); @@ -71,13 +68,15 @@ impl PersonalSigner for SignerClient { - let res = sign_and_dispatch(&*client, &*miner, request, secret); - queue.request_confirmed(id, res.clone()); - Some(res) + + let sender = request.from; + + match unlock_sign_and_dispatch(&*client, &*miner, request, &*accounts, sender, pass) { + Ok(hash) => { + queue.request_confirmed(id, Ok(hash.clone())); + Some(to_value(&hash)) }, - Err(_) => None + _ => None } }) .unwrap_or_else(|| { diff --git a/rpc/src/v1/tests/eth.rs b/rpc/src/v1/tests/eth.rs index 5d7c72ac0..457a9164a 100644 --- a/rpc/src/v1/tests/eth.rs +++ b/rpc/src/v1/tests/eth.rs @@ -15,7 +15,6 @@ // along with Parity. If not, see . //! rpc integration tests. -use std::collections::HashMap; use std::sync::Arc; use std::str::FromStr; @@ -26,12 +25,11 @@ use ethcore::block::Block; use ethcore::views::BlockView; use ethcore::ethereum; use ethcore::miner::{MinerService, ExternalMiner, Miner}; +use ethcore::account_provider::AccountProvider; use devtools::RandomTempPath; use util::Hashable; use util::io::IoChannel; -use util::hash::{Address, H256}; -use util::numbers::U256; -use util::keys::{AccountProvider, TestAccount, TestAccountProvider}; +use util::{U256, H256}; use jsonrpc_core::IoHandler; use ethjson::blockchain::BlockChain; @@ -39,11 +37,8 @@ use v1::traits::eth::{Eth, EthSigning}; use v1::impls::{EthClient, EthSigningUnsafeClient}; use v1::tests::helpers::{TestSyncProvider, Config}; -fn account_provider() -> Arc { - let mut accounts = HashMap::new(); - accounts.insert(Address::from(1), TestAccount::new("test")); - let ap = TestAccountProvider::new(accounts); - Arc::new(ap) +fn account_provider() -> Arc { + Arc::new(AccountProvider::transient_provider()) } fn sync_provider() -> Arc { @@ -70,7 +65,7 @@ fn make_spec(chain: &BlockChain) -> Spec { struct EthTester { client: Arc, _miner: Arc, - accounts: Arc, + accounts: Arc, handler: IoHandler, } @@ -226,15 +221,10 @@ const TRANSACTION_COUNT_SPEC: &'static [u8] = br#"{ fn eth_transaction_count() { use util::crypto::Secret; - let address = Address::from_str("faa34835af5c2ea724333018a515fbb7d5bc0b33").unwrap(); let secret = Secret::from_str("8a283037bb19c4fed7b1c569e40c7dcff366165eb869110a1b11532963eb9cb2").unwrap(); - let tester = EthTester::from_spec_provider(|| Spec::load(TRANSACTION_COUNT_SPEC)); - tester.accounts.accounts.write().unwrap().insert(address, TestAccount { - unlocked: false, - password: "123".into(), - secret: secret - }); + let address = tester.accounts.insert_account(secret, "").unwrap(); + tester.accounts.unlock_account_permanently(address, "".into()).unwrap(); let req_before = r#"{ "jsonrpc": "2.0", diff --git a/rpc/src/v1/tests/mocked/eth.rs b/rpc/src/v1/tests/mocked/eth.rs index 2fcd8a266..29c10a92b 100644 --- a/rpc/src/v1/tests/mocked/eth.rs +++ b/rpc/src/v1/tests/mocked/eth.rs @@ -20,7 +20,7 @@ use std::sync::{Arc, RwLock}; use jsonrpc_core::IoHandler; use util::hash::{Address, H256, FixedHash}; use util::numbers::{Uint, U256}; -use util::keys::{AccountProvider, TestAccount, TestAccountProvider}; +use ethcore::account_provider::AccountProvider; use ethcore::client::{TestBlockChainClient, EachBlockWith, Executed, TransactionID}; use ethcore::log_entry::{LocalizedLogEntry, LogEntry}; use ethcore::receipt::LocalizedReceipt; @@ -36,11 +36,8 @@ fn blockchain_client() -> Arc { Arc::new(client) } -fn accounts_provider() -> Arc { - let mut accounts = HashMap::new(); - accounts.insert(Address::from(1), TestAccount::new("test")); - let ap = TestAccountProvider::new(accounts); - Arc::new(ap) +fn accounts_provider() -> Arc { + Arc::new(AccountProvider::transient_provider()) } fn sync_provider() -> Arc { @@ -57,7 +54,7 @@ fn miner_service() -> Arc { struct EthTester { pub client: Arc, pub sync: Arc, - pub accounts_provider: Arc, + pub accounts_provider: Arc, miner: Arc, hashrates: Arc>>, pub io: IoHandler, @@ -169,8 +166,9 @@ fn rpc_eth_sign() { let tester = EthTester::default(); let account = tester.accounts_provider.new_account("abcd").unwrap(); + tester.accounts_provider.unlock_account_permanently(account, "abcd".into()).unwrap(); let message = H256::from("0x0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f"); - let signed = tester.accounts_provider.sign(&account, &message).unwrap(); + let signed = tester.accounts_provider.sign(account, message).unwrap(); let req = r#"{ "jsonrpc": "2.0", @@ -233,10 +231,13 @@ fn rpc_eth_gas_price() { #[test] fn rpc_eth_accounts() { - let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#; - let response = r#"{"jsonrpc":"2.0","result":["0x0000000000000000000000000000000000000001"],"id":1}"#; + let tester = EthTester::default(); + let address = tester.accounts_provider.new_account("").unwrap(); - assert_eq!(EthTester::default().io.handle_request(request), Some(response.to_owned())); + let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":[""#.to_owned() + &format!("0x{:?}", address) + r#""],"id":1}"#; + + assert_eq!(tester.io.handle_request(request), Some(response.to_owned())); } #[test] @@ -558,12 +559,9 @@ fn rpc_eth_estimate_gas_default_block() { #[test] fn rpc_eth_send_transaction() { - let account = TestAccount::new("123"); - let address = account.address(); - let secret = account.secret.clone(); - let tester = EthTester::default(); - tester.accounts_provider.accounts.write().unwrap().insert(address.clone(), account); + let address = tester.accounts_provider.new_account("").unwrap(); + tester.accounts_provider.unlock_account_permanently(address, "".into()).unwrap(); let request = r#"{ "jsonrpc": "2.0", "method": "eth_sendTransaction", @@ -584,7 +582,9 @@ fn rpc_eth_send_transaction() { action: Action::Call(Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap()), value: U256::from(0x9184e72au64), data: vec![] - }.sign(&secret); + }; + let signature = tester.accounts_provider.sign(address, t.hash()).unwrap(); + let t = t.with_signature(signature); let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#; @@ -599,7 +599,9 @@ fn rpc_eth_send_transaction() { action: Action::Call(Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap()), value: U256::from(0x9184e72au64), data: vec![] - }.sign(&secret); + }; + let signature = tester.accounts_provider.sign(address, t.hash()).unwrap(); + let t = t.with_signature(signature); let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#; @@ -610,7 +612,7 @@ fn rpc_eth_send_transaction() { fn rpc_eth_send_raw_transaction() { let tester = EthTester::default(); let address = tester.accounts_provider.new_account("abcd").unwrap(); - let secret = tester.accounts_provider.account_secret(&address).unwrap(); + tester.accounts_provider.unlock_account_permanently(address, "abcd".into()).unwrap(); let t = Transaction { nonce: U256::zero(), @@ -619,7 +621,9 @@ fn rpc_eth_send_raw_transaction() { action: Action::Call(Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap()), value: U256::from(0x9184e72au64), data: vec![] - }.sign(&secret); + }; + let signature = tester.accounts_provider.sign(address, t.hash()).unwrap(); + let t = t.with_signature(signature); let rlp = ::util::rlp::encode(&t).to_vec().to_hex(); diff --git a/rpc/src/v1/tests/mocked/eth_signing.rs b/rpc/src/v1/tests/mocked/eth_signing.rs index a70a4cdaf..afa6886fe 100644 --- a/rpc/src/v1/tests/mocked/eth_signing.rs +++ b/rpc/src/v1/tests/mocked/eth_signing.rs @@ -20,7 +20,7 @@ use v1::impls::EthSigningQueueClient; use v1::traits::EthSigning; use v1::helpers::{ConfirmationsQueue, SigningQueue}; use v1::tests::helpers::TestMinerService; -use util::keys::TestAccount; +use util::{Address, FixedHash}; struct EthSigningTester { pub queue: Arc, @@ -52,8 +52,7 @@ fn eth_signing() -> EthSigningTester { fn should_add_transaction_to_queue() { // given let tester = eth_signing(); - let account = TestAccount::new("123"); - let address = account.address(); + let address = Address::random(); assert_eq!(tester.queue.requests().len(), 0); // when diff --git a/rpc/src/v1/tests/mocked/personal.rs b/rpc/src/v1/tests/mocked/personal.rs index 02b07abd3..cca58c1d7 100644 --- a/rpc/src/v1/tests/mocked/personal.rs +++ b/rpc/src/v1/tests/mocked/personal.rs @@ -16,17 +16,16 @@ use std::sync::Arc; use std::str::FromStr; -use std::collections::HashMap; use jsonrpc_core::IoHandler; use util::numbers::*; -use util::keys::{TestAccount, TestAccountProvider}; +use ethcore::account_provider::AccountProvider; use v1::{PersonalClient, Personal}; use v1::tests::helpers::TestMinerService; use ethcore::client::TestBlockChainClient; use ethcore::transaction::{Action, Transaction}; struct PersonalTester { - accounts: Arc, + accounts: Arc, io: IoHandler, miner: Arc, // these unused fields are necessary to keep the data alive @@ -39,10 +38,8 @@ fn blockchain_client() -> Arc { Arc::new(client) } -fn accounts_provider() -> Arc { - let accounts = HashMap::new(); - let ap = TestAccountProvider::new(accounts); - Arc::new(ap) +fn accounts_provider() -> Arc { + Arc::new(AccountProvider::transient_provider()) } fn miner_service() -> Arc { @@ -99,13 +96,9 @@ fn should_return_port_number_if_signer_is_enabled() { #[test] fn accounts() { let tester = setup(None); - tester.accounts.accounts - .write() - .unwrap() - .insert(Address::from(1), TestAccount::new("test")); - + let address = tester.accounts.new_account("").unwrap(); let request = r#"{"jsonrpc": "2.0", "method": "personal_listAccounts", "params": [], "id": 1}"#; - let response = r#"{"jsonrpc":"2.0","result":["0x0000000000000000000000000000000000000001"],"id":1}"#; + let response = r#"{"jsonrpc":"2.0","result":[""#.to_owned() + &format!("0x{:?}", address) + r#""],"id":1}"#; assert_eq!(tester.io.handle_request(request), Some(response.to_owned())); } @@ -117,15 +110,9 @@ fn new_account() { let res = tester.io.handle_request(request); - let accounts = tester.accounts.accounts.read().unwrap(); + let accounts = tester.accounts.accounts(); assert_eq!(accounts.len(), 1); - - let address = accounts - .keys() - .nth(0) - .cloned() - .unwrap(); - + let address = accounts[0]; let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", address).as_ref() + r#"","id":1}"#; assert_eq!(res, Some(response)); @@ -133,11 +120,8 @@ fn new_account() { #[test] fn sign_and_send_transaction_with_invalid_password() { - let account = TestAccount::new("password123"); - let address = account.address(); - let tester = setup(None); - tester.accounts.accounts.write().unwrap().insert(address.clone(), account); + let address = tester.accounts.new_account("password123").unwrap(); let request = r#"{ "jsonrpc": "2.0", "method": "personal_signAndSendTransaction", @@ -158,12 +142,9 @@ fn sign_and_send_transaction_with_invalid_password() { #[test] fn sign_and_send_transaction() { - let account = TestAccount::new("password123"); - let address = account.address(); - let secret = account.secret.clone(); - let tester = setup(None); - tester.accounts.accounts.write().unwrap().insert(address.clone(), account); + let address = tester.accounts.new_account("password123").unwrap(); + let request = r#"{ "jsonrpc": "2.0", "method": "personal_signAndSendTransaction", @@ -184,7 +165,10 @@ fn sign_and_send_transaction() { action: Action::Call(Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap()), value: U256::from(0x9184e72au64), data: vec![] - }.sign(&secret); + }; + tester.accounts.unlock_account_temporarily(address, "password123".into()).unwrap(); + let signature = tester.accounts.sign(address, t.hash()).unwrap(); + let t = t.with_signature(signature); let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#; @@ -199,7 +183,10 @@ fn sign_and_send_transaction() { action: Action::Call(Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap()), value: U256::from(0x9184e72au64), data: vec![] - }.sign(&secret); + }; + tester.accounts.unlock_account_temporarily(address, "password123".into()).unwrap(); + let signature = tester.accounts.sign(address, t.hash()).unwrap(); + let t = t.with_signature(signature); let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#; diff --git a/rpc/src/v1/tests/mocked/personal_signer.rs b/rpc/src/v1/tests/mocked/personal_signer.rs index cf7661331..6fa734cec 100644 --- a/rpc/src/v1/tests/mocked/personal_signer.rs +++ b/rpc/src/v1/tests/mocked/personal_signer.rs @@ -16,10 +16,9 @@ use std::sync::Arc; use std::str::FromStr; -use std::collections::HashMap; use jsonrpc_core::IoHandler; use util::numbers::*; -use util::keys::{TestAccount, TestAccountProvider}; +use ethcore::account_provider::AccountProvider; use ethcore::client::TestBlockChainClient; use ethcore::transaction::{Transaction, Action}; use v1::{SignerClient, PersonalSigner}; @@ -30,7 +29,7 @@ use v1::types::TransactionRequest; struct PersonalSignerTester { queue: Arc, - accounts: Arc, + accounts: Arc, io: IoHandler, miner: Arc, // these unused fields are necessary to keep the data alive @@ -43,10 +42,8 @@ fn blockchain_client() -> Arc { Arc::new(client) } -fn accounts_provider() -> Arc { - let accounts = HashMap::new(); - let ap = TestAccountProvider::new(accounts); - Arc::new(ap) +fn accounts_provider() -> Arc { + Arc::new(AccountProvider::transient_provider()) } fn miner_service() -> Arc { @@ -146,16 +143,10 @@ fn should_not_remove_transaction_if_password_is_invalid() { #[test] fn should_confirm_transaction_and_dispatch() { - // given + //// given let tester = signer_tester(); - let account = TestAccount::new("test"); - let address = account.address(); - let secret = account.secret.clone(); + let address = tester.accounts.new_account("test").unwrap(); let recipient = Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap(); - tester.accounts.accounts - .write() - .unwrap() - .insert(address, account); tester.queue.add_request(TransactionRequest { from: address, to: Some(recipient), @@ -165,6 +156,7 @@ fn should_confirm_transaction_and_dispatch() { data: None, nonce: None, }); + let t = Transaction { nonce: U256::zero(), gas_price: U256::from(0x1000), @@ -172,7 +164,10 @@ fn should_confirm_transaction_and_dispatch() { action: Action::Call(recipient), value: U256::from(0x1), data: vec![] - }.sign(&secret); + }; + tester.accounts.unlock_account_temporarily(address, "test".into()).unwrap(); + let signature = tester.accounts.sign(address, t.hash()).unwrap(); + let t = t.with_signature(signature); assert_eq!(tester.queue.requests().len(), 1); diff --git a/sync/src/chain.rs b/sync/src/chain.rs index 8f7367501..cb781ea22 100644 --- a/sync/src/chain.rs +++ b/sync/src/chain.rs @@ -458,12 +458,13 @@ impl ChainSync { // Disable the peer for this syncing round if it gives invalid chain if !valid_response { trace!(target: "sync", "{} Deactivated for invalid headers response", peer_id); - self.deactivate_peer(io, peer_id); + self.deactivate_peer(io, peer_id); } + if headers.is_empty() { // Peer does not have any new subchain heads, deactivate it nd try with another trace!(target: "sync", "{} Deactivated for no data", peer_id); - self.deactivate_peer(io, peer_id); + self.deactivate_peer(io, peer_id); } match self.state { SyncState::ChainHead => { diff --git a/util/Cargo.toml b/util/Cargo.toml index 05e05d4c1..8b213ad47 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -18,7 +18,7 @@ rand = "0.3.12" time = "0.1.34" tiny-keccak = "1.0" rocksdb = { git = "https://github.com/ethcore/rust-rocksdb" } -lazy_static = "0.1" +lazy_static = "0.2" eth-secp256k1 = { git = "https://github.com/ethcore/rust-secp256k1" } rust-crypto = "0.2.34" elastic-array = "0.4" diff --git a/util/src/keys/directory.rs b/util/src/keys/directory.rs deleted file mode 100644 index 385a400c7..000000000 --- a/util/src/keys/directory.rs +++ /dev/null @@ -1,1247 +0,0 @@ -// Copyright 2015, 2016 Ethcore (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -//! Keys Directory - -use common::*; -use std::path::{PathBuf}; -use path::restrict_permissions_owner; - -const CURRENT_DECLARED_VERSION: u64 = 3; -const MAX_KEY_FILE_LEN: u64 = 1024 * 80; -const MAX_CACHE_USAGE_TRACK: usize = 128; - -/// Cipher type (currently only aes-128-ctr) -#[derive(PartialEq, Debug, Clone)] -pub enum CryptoCipherType { - /// aes-128-ctr with 128-bit initialisation vector(iv) - Aes128Ctr(H128) -} - -#[derive(PartialEq, Debug, Clone)] -enum KeyFileVersion { - V3(u64) -} - -/// key generator function -#[derive(PartialEq, Debug, Clone)] -pub enum Pbkdf2CryptoFunction { - /// keyed-hash generator (HMAC-256) - HMacSha256 -} - -#[derive(Clone)] -/// Kdf of type `Pbkdf2` -/// https://en.wikipedia.org/wiki/PBKDF2 -pub struct KdfPbkdf2Params { - /// desired length of the derived key, in octets - pub dk_len: u32, - /// cryptographic salt - pub salt: H256, - /// number of iterations for derived key - pub c: u32, - /// pseudo-random 2-parameters function - pub prf: Pbkdf2CryptoFunction -} - -#[derive(Debug)] -enum Pbkdf2ParseError { - InvalidParameter(&'static str), - InvalidPrf(Mismatch), - InvalidSaltFormat(UtilError), - MissingParameter(&'static str), -} - -impl KdfPbkdf2Params { - fn from_json(json: &BTreeMap) -> Result { - Ok(KdfPbkdf2Params{ - salt: match try!(json.get("salt").ok_or(Pbkdf2ParseError::MissingParameter("salt"))).as_string() { - None => { return Err(Pbkdf2ParseError::InvalidParameter("salt")) }, - Some(salt_value) => match H256::from_str(salt_value) { - Ok(salt_hex_value) => salt_hex_value, - Err(from_hex_error) => { return Err(Pbkdf2ParseError::InvalidSaltFormat(from_hex_error)); }, - } - }, - prf: match try!(json.get("prf").ok_or(Pbkdf2ParseError::MissingParameter("prf"))).as_string() { - Some("hmac-sha256") => Pbkdf2CryptoFunction::HMacSha256, - Some(unexpected_prf) => { return Err(Pbkdf2ParseError::InvalidPrf(Mismatch { expected: "hmac-sha256".to_owned(), found: unexpected_prf.to_owned() })); }, - None => { return Err(Pbkdf2ParseError::InvalidParameter("prf")); }, - }, - dk_len: try!(try!(json.get("dklen").ok_or(Pbkdf2ParseError::MissingParameter("dklen"))).as_u64().ok_or(Pbkdf2ParseError::InvalidParameter("dkLen"))) as u32, - c: try!(try!(json.get("c").ok_or(Pbkdf2ParseError::MissingParameter("c"))).as_u64().ok_or(Pbkdf2ParseError::InvalidParameter("c"))) as u32, - }) - } - - fn to_json(&self) -> Json { - let mut map = BTreeMap::new(); - map.insert("dklen".to_owned(), json_from_u32(self.dk_len)); - map.insert("salt".to_owned(), Json::String(format!("{:?}", self.salt))); - map.insert("prf".to_owned(), Json::String("hmac-sha256".to_owned())); - map.insert("c".to_owned(), json_from_u32(self.c)); - Json::Object(map) - } -} - -#[derive(Clone)] -/// Kdf of type `Scrypt`. -/// https://en.wikipedia.org/wiki/Scrypt -pub struct KdfScryptParams { - /// Desired length of the derived key, in octets. - pub dk_len: u32, - /// Parallelization parameter. - pub p: u32, - /// CPU/memory cost parameter. - pub n: u32, - /// TODO: comment - pub r: u32, - /// Cryptographic salt. - pub salt: H256, -} - -#[derive(Debug)] -enum ScryptParseError { - InvalidParameter(&'static str), - InvalidSaltFormat(UtilError), - MissingParameter(&'static str), -} - -fn json_from_u32(number: u32) -> Json { Json::U64(number as u64) } - -impl KdfScryptParams { - fn from_json(json: &BTreeMap) -> Result { - Ok(KdfScryptParams{ - salt: match try!(json.get("salt").ok_or(ScryptParseError::MissingParameter("salt"))).as_string() { - None => { return Err(ScryptParseError::InvalidParameter("salt")) }, - Some(salt_value) => match H256::from_str(salt_value) { - Ok(salt_hex_value) => salt_hex_value, - Err(from_hex_error) => { return Err(ScryptParseError::InvalidSaltFormat(from_hex_error)); }, - } - }, - dk_len: try!(try!(json.get("dklen").ok_or(ScryptParseError::MissingParameter("dklen"))).as_u64().ok_or(ScryptParseError::InvalidParameter("dkLen"))) as u32, - p: try!(try!(json.get("p").ok_or(ScryptParseError::MissingParameter("p"))).as_u64().ok_or(ScryptParseError::InvalidParameter("p"))) as u32, - n: try!(try!(json.get("n").ok_or(ScryptParseError::MissingParameter("n"))).as_u64().ok_or(ScryptParseError::InvalidParameter("n"))) as u32, - r: try!(try!(json.get("r").ok_or(ScryptParseError::MissingParameter("r"))).as_u64().ok_or(ScryptParseError::InvalidParameter("r"))) as u32, - }) - } - - fn to_json(&self) -> Json { - let mut map = BTreeMap::new(); - map.insert("dklen".to_owned(), json_from_u32(self.dk_len)); - map.insert("salt".to_owned(), Json::String(format!("{:?}", self.salt))); - map.insert("p".to_owned(), json_from_u32(self.p)); - map.insert("n".to_owned(), json_from_u32(self.n)); - map.insert("r".to_owned(), json_from_u32(self.r)); - Json::Object(map) - } -} - -#[derive(Clone)] -/// Settings for password derived key geberator function. -pub enum KeyFileKdf { - /// Password-Based Key Derivation Function 2 (PBKDF2) type. - /// https://en.wikipedia.org/wiki/PBKDF2 - Pbkdf2(KdfPbkdf2Params), - /// Scrypt password-based key derivation function. - /// https://en.wikipedia.org/wiki/Scrypt - Scrypt(KdfScryptParams) -} - -#[derive(Clone)] -/// Encrypted password or other arbitrary message -/// with settings for password derived key generator for decrypting content. -pub struct KeyFileCrypto { - /// Cipher type. - pub cipher_type: CryptoCipherType, - /// Cipher text (encrypted message). - pub cipher_text: Bytes, - /// Password derived key generator function settings. - pub kdf: KeyFileKdf, - /// Mac - pub mac: H256 -} - -impl KeyFileCrypto { - fn from_json(json: &Json) -> Result { - let as_object = match json.as_object() { - None => { return Err(CryptoParseError::InvalidJsonFormat); } - Some(obj) => obj - }; - - let cipher_type = match try!(as_object.get("cipher").ok_or(CryptoParseError::NoCipherType)).as_string() { - None => { return Err(CryptoParseError::InvalidCipherType(Mismatch { expected: "aes-128-ctr".to_owned(), found: "not a json string".to_owned() })); } - Some("aes-128-ctr") => CryptoCipherType::Aes128Ctr( - match try!(as_object.get("cipherparams").ok_or(CryptoParseError::NoCipherParameters)).as_object() { - None => { return Err(CryptoParseError::NoCipherParameters); }, - Some(cipher_param) => match H128::from_str(match cipher_param["iv"].as_string() { - None => { return Err(CryptoParseError::NoInitialVector); }, - Some(iv_hex_string) => iv_hex_string - }) - { - Ok(iv_value) => iv_value, - Err(hex_error) => { return Err(CryptoParseError::InvalidInitialVector(hex_error)); } - } - } - ), - Some(other_cipher_type) => { - return Err(CryptoParseError::InvalidCipherType( - Mismatch { expected: "aes-128-ctr".to_owned(), found: other_cipher_type.to_owned() })); - } - }; - - let kdf = match (try!(as_object.get("kdf").ok_or(CryptoParseError::NoKdf)).as_string(), try!(as_object.get("kdfparams").ok_or(CryptoParseError::NoKdfType)).as_object()) { - (None, _) => { return Err(CryptoParseError::NoKdfType); }, - (Some("scrypt"), Some(kdf_params)) => - match KdfScryptParams::from_json(kdf_params) { - Err(scrypt_params_error) => { return Err(CryptoParseError::Scrypt(scrypt_params_error)); }, - Ok(scrypt_params) => KeyFileKdf::Scrypt(scrypt_params) - }, - (Some("pbkdf2"), Some(kdf_params)) => - match KdfPbkdf2Params::from_json(kdf_params) { - Err(pbkdf2_params_error) => { return Err(CryptoParseError::KdfPbkdf2(pbkdf2_params_error)); }, - Ok(pbkdf2_params) => KeyFileKdf::Pbkdf2(pbkdf2_params) - }, - (Some(other_kdf), _) => { - return Err(CryptoParseError::InvalidKdfType( - Mismatch { expected: "pbkdf2/scrypt".to_owned(), found: other_kdf.to_owned()})); - } - }; - - let cipher_text = match try!(as_object.get("ciphertext").ok_or(CryptoParseError::NoCipherText)).as_string() { - None => { return Err(CryptoParseError::InvalidCipherText); } - Some(text) => text - }; - - let mac: H256 = match try!(as_object.get("mac").ok_or(CryptoParseError::NoMac)).as_string() { - None => { return Err(CryptoParseError::InvalidMacFormat(None)) }, - Some(salt_value) => match H256::from_str(salt_value) { - Ok(salt_hex_value) => salt_hex_value, - Err(from_hex_error) => { return Err(CryptoParseError::InvalidMacFormat(Some(from_hex_error))); }, - } - }; - - Ok(KeyFileCrypto { - cipher_text: match FromHex::from_hex(cipher_text) { Ok(bytes) => bytes, Err(_) => { return Err(CryptoParseError::InvalidCipherText); } }, - cipher_type: cipher_type, - kdf: kdf, - mac: mac, - }) - } - - fn to_json(&self) -> Json { - let mut map = BTreeMap::new(); - match self.cipher_type { - CryptoCipherType::Aes128Ctr(ref iv) => { - map.insert("cipher".to_owned(), Json::String("aes-128-ctr".to_owned())); - let mut cipher_params = BTreeMap::new(); - cipher_params.insert("iv".to_owned(), Json::String(format!("{:?}", iv))); - map.insert("cipherparams".to_owned(), Json::Object(cipher_params)); - } - } - map.insert("ciphertext".to_owned(), Json::String( - self.cipher_text.iter().map(|b| format!("{:02x}", b)).collect::>().join(""))); - - map.insert("kdf".to_owned(), Json::String(match self.kdf { - KeyFileKdf::Pbkdf2(_) => "pbkdf2".to_owned(), - KeyFileKdf::Scrypt(_) => "scrypt".to_owned() - })); - - map.insert("kdfparams".to_owned(), match self.kdf { - KeyFileKdf::Pbkdf2(ref pbkdf2_params) => pbkdf2_params.to_json(), - KeyFileKdf::Scrypt(ref scrypt_params) => scrypt_params.to_json() - }); - - map.insert("mac".to_owned(), Json::String(format!("{:?}", self.mac))); - - Json::Object(map) - } - - /// New pbkdf2-type secret. - /// `cipher-text` - encrypted cipher text. - /// `dk-len` - desired length of the derived key, in octets. - /// `c` - number of iterations for derived key. - /// `salt` - cryptographic site, random 256-bit hash (ensure it's crypto-random). - /// `iv` - initialisation vector. - pub fn new_pbkdf2(cipher_text: Bytes, iv: H128, salt: H256, mac: H256, c: u32, dk_len: u32) -> KeyFileCrypto { - KeyFileCrypto { - cipher_type: CryptoCipherType::Aes128Ctr(iv), - cipher_text: cipher_text, - kdf: KeyFileKdf::Pbkdf2(KdfPbkdf2Params { - dk_len: dk_len, - salt: salt, - c: c, - prf: Pbkdf2CryptoFunction::HMacSha256 - }), - mac: mac, - } - } -} - -/// Universally unique identifier -pub type Uuid = H128; - -fn new_uuid() -> Uuid { - H128::random() -} - -fn uuid_to_string(uuid: &Uuid) -> String { - let d1 = &uuid.as_slice()[0..4]; - let d2 = &uuid.as_slice()[4..6]; - let d3 = &uuid.as_slice()[6..8]; - let d4 = &uuid.as_slice()[8..10]; - let d5 = &uuid.as_slice()[10..16]; - format!("{}-{}-{}-{}-{}", d1.to_hex(), d2.to_hex(), d3.to_hex(), d4.to_hex(), d5.to_hex()) -} - -fn uuid_from_string(s: &str) -> Result { - let parts: Vec<&str> = s.split('-').collect(); - if parts.len() != 5 { return Err(UtilError::BadSize); } - - let mut uuid = H128::zero(); - - if parts[0].len() != 8 { return Err(UtilError::BadSize); } - uuid[0..4].clone_from_slice(&try!(FromHex::from_hex(parts[0]))); - if parts[1].len() != 4 { return Err(UtilError::BadSize); } - uuid[4..6].clone_from_slice(&try!(FromHex::from_hex(parts[1]))); - if parts[2].len() != 4 { return Err(UtilError::BadSize); } - uuid[6..8].clone_from_slice(&try!(FromHex::from_hex(parts[2]))); - if parts[3].len() != 4 { return Err(UtilError::BadSize); } - uuid[8..10].clone_from_slice(&try!(FromHex::from_hex(parts[3]))); - if parts[4].len() != 12 { return Err(UtilError::BadSize); } - uuid[10..16].clone_from_slice(&try!(FromHex::from_hex(parts[4]))); - - Ok(uuid) -} - - -#[derive(Clone)] -/// Stored key file struct with encrypted message `(cipher_text)` -/// also contains password derivation function settings (PBKDF2/Scrypt) -pub struct KeyFileContent { - version: KeyFileVersion, - /// Holds cypher and decrypt function settings. - pub crypto: KeyFileCrypto, - /// The identifier. - pub id: Uuid, - /// Account (if present) - pub account: Option
, -} - -#[derive(Debug)] -enum CryptoParseError { - InvalidMacFormat(Option), - NoMac, - NoCipherText, - InvalidCipherText, - NoCipherType, - InvalidJsonFormat, - InvalidKdfType(Mismatch), - InvalidCipherType(Mismatch), - NoInitialVector, - NoCipherParameters, - InvalidInitialVector(UtilError), - NoKdf, - NoKdfType, - Scrypt(ScryptParseError), - KdfPbkdf2(Pbkdf2ParseError) -} - -#[derive(Debug)] -enum KeyFileParseError { - InvalidVersion, - UnsupportedVersion(OutOfBounds), - InvalidJsonFormat, - InvalidJson, - InvalidIdentifier, - NoCryptoSection, - Crypto(CryptoParseError), -} - -impl KeyFileContent { - /// New stored key file struct with encrypted message (`cipher_text`) - /// also contains password derivation function settings (PBKDF2/Scrypt) - /// to decrypt `cipher_text` given the password is provided. - pub fn new(crypto: KeyFileCrypto) -> KeyFileContent { - KeyFileContent { - id: new_uuid(), - version: KeyFileVersion::V3(3), - crypto: crypto, - account: None - } - } - - /// Loads key from valid json, returns error and records warning if key is malformed - pub fn load(json: &Json) -> Result { - match Self::from_json(json) { - Ok(key_file) => Ok(key_file), - Err(e) => { - warn!(target: "sstore", "Error parsing json for key: {:?}", e); - Err(()) - } - } - } - - /// Returns key file version if it is known. - pub fn version(&self) -> Option { - match self.version { - KeyFileVersion::V3(declared) => Some(declared) - } - } - - fn from_json(json: &Json) -> Result { - let as_object = match json.as_object() { - None => { return Err(KeyFileParseError::InvalidJsonFormat); }, - Some(obj) => obj - }; - - let version = match as_object["version"].as_u64() { - None => { return Err(KeyFileParseError::InvalidVersion); }, - Some(json_version) => { - if json_version <= 2 { - return Err(KeyFileParseError::UnsupportedVersion(OutOfBounds { min: Some(3), max: None, found: json_version })) - }; - KeyFileVersion::V3(json_version) - } - }; - - let id_text = try!(as_object.get("id").and_then(|json| json.as_string()).ok_or(KeyFileParseError::InvalidIdentifier)); - let id = match uuid_from_string(&id_text) { - Err(_) => { return Err(KeyFileParseError::InvalidIdentifier); }, - Ok(id) => id - }; - - let account = as_object.get("address").and_then(|json| json.as_string()).and_then( - |account_text| match Address::from_str(account_text) { Ok(account) => Some(account), Err(_) => None }); - - let crypto = match as_object.get("crypto") { - None => { return Err(KeyFileParseError::NoCryptoSection); } - Some(crypto_json) => match KeyFileCrypto::from_json(crypto_json) { - Ok(crypto) => crypto, - Err(crypto_error) => { return Err(KeyFileParseError::Crypto(crypto_error)); } - } - }; - - Ok(KeyFileContent { - version: version, - id: id.clone(), - crypto: crypto, - account: account - }) - } - - fn to_json(&self) -> Json { - let mut map = BTreeMap::new(); - map.insert("id".to_owned(), Json::String(uuid_to_string(&self.id))); - map.insert("version".to_owned(), Json::U64(CURRENT_DECLARED_VERSION)); - map.insert("crypto".to_owned(), self.crypto.to_json()); - if let Some(ref address) = self.account { map.insert("address".to_owned(), Json::String(format!("{:?}", address))); } - Json::Object(map) - } -} - -#[derive(Debug)] -enum KeyFileLoadError { - TooLarge(OutOfBounds), - ParseError(KeyFileParseError), - ReadError(::std::io::Error), -} - -/// Represents directory for saving/loading key files. -pub struct KeyDirectory { - /// Directory path for key management. - path: String, - cache: RwLock>, - cache_usage: RwLock>, -} - -impl KeyDirectory { - /// Initializes new cache directory context with a given `path` - pub fn new(path: &Path) -> KeyDirectory { - KeyDirectory { - cache: RwLock::new(HashMap::new()), - path: path.to_str().expect("Initialized key directory with empty path").to_owned(), - cache_usage: RwLock::new(VecDeque::new()), - } - } - - /// saves (inserts or updates) given key - pub fn save(&mut self, key_file: KeyFileContent) -> Result<(Uuid), ::std::io::Error> { - { - let mut file = try!(fs::File::create(self.key_path(&key_file.id))); - let json = key_file.to_json(); - let json_text = format!("{}", json.pretty()); - let json_bytes = json_text.into_bytes(); - try!(file.write(&json_bytes)); - } - if let Err(error_code) = restrict_permissions_owner(self.key_path(&key_file.id).as_path()) { - fs::remove_file(self.key_path(&key_file.id)).unwrap(); - warn!(target: "sstore", "fatal: failed to modify permissions of the file (chmod: {})", error_code); - return Err(::std::io::Error::last_os_error()); - } - let mut cache = self.cache.write().unwrap(); - let id = key_file.id.clone(); - cache.insert(id.clone(), key_file); - Ok(id.clone()) - } - - /// Returns key given by id if corresponding file exists and no load error occured. - /// Warns if any error occured during the key loading - pub fn get(&self, id: &Uuid) -> Option { - let path = self.key_path(id); - { - let mut usage = self.cache_usage.write().unwrap(); - usage.push_back(id.clone()); - } - - if !self.cache.read().unwrap().contains_key(id) { - match KeyDirectory::load_key(&path) { - Ok(loaded_key) => { - self.cache.write().unwrap().insert(id.to_owned(), loaded_key); - } - Err(error) => { - warn!(target: "sstore", "error loading key {:?}: {:?}", id, error); - return None; - } - } - } - - // todo: replace with Ref::map when it stabilized to avoid copies - Some(self.cache.read().unwrap().get(id) - .expect("Key should be there, we have just inserted or checked it.") - .clone()) - } - - /// Returns current path to the directory with keys - pub fn path(&self) -> &str { - &self.path - } - - /// Removes keys that never been requested during last `MAX_USAGE_TRACK` times - pub fn collect_garbage(&mut self) { - let mut cache_usage = self.cache_usage.write().unwrap(); - - let total_usages = cache_usage.len(); - let untracked_usages = max(total_usages as i64 - MAX_CACHE_USAGE_TRACK as i64, 0) as usize; - if untracked_usages > 0 { - cache_usage.drain(..untracked_usages); - } - - if self.cache.read().unwrap().len() <= MAX_CACHE_USAGE_TRACK { return; } - - let uniqs: HashSet<&Uuid> = cache_usage.iter().collect(); - let removes:Vec = { - let cache = self.cache.read().unwrap(); - cache.keys().cloned().filter(|key| !uniqs.contains(key)).collect() - }; - if removes.is_empty() { return; } - let mut cache = self.cache.write().unwrap(); - for key in removes { cache.remove(&key); } - - cache.shrink_to_fit(); - } - - /// Reports how many keys are currently cached. - pub fn cache_size(&self) -> usize { - self.cache.read().unwrap().len() - } - - /// Removes key file from key directory - pub fn delete(&mut self, id: &Uuid) -> Result<(), ::std::io::Error> { - let path = self.key_path(id); - - if !self.cache.read().unwrap().contains_key(id) { - return match fs::remove_file(&path) { - Ok(_) => { - self.cache.write().unwrap().remove(&id); - Ok(()) - }, - Err(e) => Err(e) - }; - } - Ok(()) - } - - /// Enumerates all keys in the directory - pub fn list(&self) -> Result, ::std::io::Error> { - let mut result = Vec::new(); - for entry in try!(fs::read_dir(&self.path)) { - let entry = try!(entry); - if !try!(fs::metadata(entry.path())).is_dir() { - match entry.file_name().to_str() { - Some(ref name) => { - if let Ok(uuid) = uuid_from_string(name) { result.push(uuid); } - }, - None => { continue; } - }; - - } - } - Ok(result) - } - - fn key_path(&self, id: &Uuid) -> PathBuf { - let mut path = PathBuf::new(); - path.push(self.path.clone()); - path.push(uuid_to_string(&id)); - path - } - - fn load_key(path: &PathBuf) -> Result { - match fs::File::open(path.clone()) { - Ok(mut open_file) => { - match open_file.metadata() { - Ok(metadata) => - if metadata.len() > MAX_KEY_FILE_LEN { Err(KeyFileLoadError::TooLarge(OutOfBounds { min: Some(2), max: Some(MAX_KEY_FILE_LEN), found: metadata.len() })) } - else { KeyDirectory::load_from_file(&mut open_file) }, - Err(read_error) => Err(KeyFileLoadError::ReadError(read_error)) - } - }, - Err(read_error) => Err(KeyFileLoadError::ReadError(read_error)) - } - } - - fn load_from_file(file: &mut fs::File) -> Result { - let mut buf = String::new(); - match file.read_to_string(&mut buf) { - Ok(_) => {}, - Err(read_error) => { return Err(KeyFileLoadError::ReadError(read_error)); } - } - match Json::from_str(&buf) { - Ok(json) => match KeyFileContent::from_json(&json) { - Ok(key_file_content) => Ok(key_file_content), - Err(parse_error) => Err(KeyFileLoadError::ParseError(parse_error)) - }, - Err(_) => Err(KeyFileLoadError::ParseError(KeyFileParseError::InvalidJson)) - } - } - - /// Checks if key exists - pub fn exists(&self, id: &Uuid) -> bool { - KeyDirectory::load_key(&self.key_path(id)).is_ok() - } -} - - -#[cfg(test)] -mod file_tests { - use super::{KeyFileContent, KeyFileVersion, KeyFileKdf, KeyFileParseError, CryptoParseError, uuid_from_string, uuid_to_string, KeyFileCrypto, KdfPbkdf2Params}; - use common::*; - - #[test] - fn uuid_parses() { - let uuid = uuid_from_string("3198bc9c-6672-5ab3-d995-4942343ae5b6").unwrap(); - assert!(uuid > H128::zero()); - } - - #[test] - fn uuid_serializes() { - let uuid = uuid_from_string("3198bc9c-6fff-5ab3-d995-4942343ae5b6").unwrap(); - assert_eq!(uuid_to_string(&uuid), "3198bc9c-6fff-5ab3-d995-4942343ae5b6"); - } - - #[test] - fn can_read_keyfile() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" - }, - "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", - "kdf" : "pbkdf2", - "kdfparams" : { - "c" : 262144, - "dklen" : 32, - "prf" : "hmac-sha256", - "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" - }, - "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(key_file) => { - assert_eq!(KeyFileVersion::V3(3), key_file.version) - }, - Err(e) => panic!("Error parsing valid file: {:?}", e) - } - } - - #[test] - fn can_read_scrypt_kdf() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(key_file) => { - match key_file.crypto.kdf { - KeyFileKdf::Scrypt(_) => {}, - _ => { panic!("expected kdf params of crypto to be of scrypt type" ); } - } - }, - Err(e) => panic!("Error parsing valid file: {:?}", e) - } - } - - #[test] - fn can_read_scrypt_kdf_params() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(key_file) => { - match key_file.crypto.kdf { - KeyFileKdf::Scrypt(scrypt_params) => { - assert_eq!(262144, scrypt_params.n); - assert_eq!(1, scrypt_params.r); - assert_eq!(8, scrypt_params.p); - }, - _ => { panic!("expected kdf params of crypto to be of scrypt type" ); } - } - }, - Err(e) => panic!("Error parsing valid file: {:?}", e) - } - } - - #[test] - fn can_return_error_no_id() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "version" : 3 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(_) => { - panic!("Should be error of no crypto section, got ok"); - }, - Err(KeyFileParseError::InvalidIdentifier) => { }, - Err(other_error) => { panic!("should be error of no crypto section, got {:?}", other_error); } - } - } - - #[test] - fn can_return_error_no_crypto() { - let json = Json::from_str( - r#" - { - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(_) => { - panic!("Should be error of no identifier, got ok"); - }, - Err(KeyFileParseError::NoCryptoSection) => { }, - Err(other_error) => { panic!("should be error of no identifier, got {:?}", other_error); } - } - } - - #[test] - fn can_return_error_unsupported_version() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 1 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(_) => { - panic!("should be error of unsupported version, got ok"); - }, - Err(KeyFileParseError::UnsupportedVersion(_)) => { }, - Err(other_error) => { panic!("should be error of unsupported version, got {:?}", other_error); } - } - } - - - #[test] - fn can_return_error_initial_vector() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e4______66191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(_) => { - panic!("should be error of invalid initial vector, got ok"); - }, - Err(KeyFileParseError::Crypto(CryptoParseError::InvalidInitialVector(_))) => { }, - Err(other_error) => { panic!("should be error of invalid initial vector, got {:?}", other_error); } - } - } - - #[test] - fn can_return_error_for_invalid_scrypt_kdf() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen2" : 32, - "n5" : "xx", - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - } - "#).unwrap(); - - match KeyFileContent::from_json(&json) { - Ok(_) => { - panic!("Should be error of no identifier, got ok"); - }, - Err(KeyFileParseError::Crypto(CryptoParseError::Scrypt(_))) => { }, - Err(other_error) => { panic!("should be scrypt parse error, got {:?}", other_error); } - } - } - - #[test] - fn can_serialize_scrypt_back() { - let json = Json::from_str( - r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - } - "#).unwrap(); - - let key = KeyFileContent::from_json(&json).unwrap(); - let serialized = key.to_json(); - - assert!(serialized.as_object().is_some()); - } - - #[test] - fn can_create_key_with_new_id() { - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32)); - assert!(!uuid_to_string(&key.id).is_empty()); - } - - #[test] - fn can_load_json_from_itself() { - let cipher_text: Bytes = FromHex::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaa22222222222222222222222").unwrap(); - let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32)); - let json = key.to_json(); - - let loaded_key = KeyFileContent::from_json(&json).unwrap(); - - assert_eq!(loaded_key.id, key.id); - } - - #[test] - fn can_parse_kdf_params_fail() { - let json = Json::from_str( - r#" - { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - } - "#).unwrap(); - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("dklen"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("n"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("r"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("p"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("salt"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - - } - - #[test] - fn can_parse_kdf_params_scrypt_fail() { - let json = Json::from_str( - r#" - { - "dklen" : 32, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - } - "#).unwrap(); - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("dklen"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("r"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("p"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.remove("salt"); - let kdf = KdfPbkdf2Params::from_json(&invalid_json); - assert!(!kdf.is_ok()); - } - } - - #[test] - fn can_parse_crypto_fails() { - let json = Json::from_str( - r#" - { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }"#).unwrap(); - - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.insert("cipher".to_owned(), Json::String("unknown".to_owned())); - let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); - assert!(!crypto.is_ok()); - } - - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.insert("kdfparams".to_owned(), Json::String("122".to_owned())); - let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); - assert!(!crypto.is_ok()); - } - - { - let mut invalid_json = json.as_object().unwrap().clone(); - invalid_json.insert("kdf".to_owned(), Json::String("15522".to_owned())); - let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); - assert!(!crypto.is_ok()); - } - - } - -} - -#[cfg(test)] -mod directory_tests { - use super::{KeyDirectory, new_uuid, uuid_to_string, KeyFileContent, KeyFileCrypto, MAX_CACHE_USAGE_TRACK}; - use common::*; - use devtools::*; - - #[test] - fn key_directory_locates_keys() { - let temp_path = RandomTempPath::create_dir(); - let directory = KeyDirectory::new(temp_path.as_path()); - let uuid = new_uuid(); - - let path = directory.key_path(&uuid); - - assert!(path.to_str().unwrap().contains(&uuid_to_string(&uuid))); - } - - #[test] - fn loads_key() { - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32))).unwrap(); - let path = directory.key_path(&uuid); - - let key = KeyDirectory::load_key(&path).unwrap(); - - assert_eq!(key.id, uuid); - } - - #[test] - fn caches_keys() { - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let mut keys = Vec::new(); - for _ in 0..1000 { - let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); - keys.push(directory.save(key).unwrap()); - } - - for key_id in keys { - directory.get(&key_id).unwrap(); - } - - assert_eq!(1000, directory.cache_size()) - - } - - #[test] - fn collects_garbage() { - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let mut keys = Vec::new(); - for _ in 0..1000 { - let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); - keys.push(directory.save(key).unwrap()); - } - - for key_id in keys { - directory.get(&key_id).unwrap(); - } - - directory.collect_garbage(); - // since all keys are different, should be exactly MAX_CACHE_USAGE_TRACK - assert_eq!(MAX_CACHE_USAGE_TRACK, directory.cache_size()) - } - - #[test] - fn collects_garbage_on_empty() { - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - directory.collect_garbage(); - assert_eq!(0, directory.cache_size()) - } -} - -#[cfg(test)] -mod specs { - use super::*; - use common::*; - use devtools::*; - - #[test] - fn can_initiate_key_directory() { - let temp_path = RandomTempPath::create_dir(); - let directory = KeyDirectory::new(&temp_path.as_path()); - assert!(directory.path().len() > 0); - } - - #[test] - fn can_save_key() { - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - - let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32))); - - assert!(uuid.is_ok()); - } - - #[test] - fn can_load_key() { - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32))).unwrap(); - - let key = directory.get(&uuid).unwrap(); - - assert_eq!(key.crypto.cipher_text, cipher_text); - } - - #[test] - fn can_store_10_keys() { - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let mut keys = Vec::new(); - for _ in 0..10 { - let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); - keys.push(directory.save(key).unwrap()); - } - - assert_eq!(10, keys.len()) - } - - #[test] - fn can_list_keys() { - let temp_path = RandomTempPath::create_dir(); - let mut directory = KeyDirectory::new(&temp_path.as_path()); - - let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); - let mut keys = Vec::new(); - for _ in 0..33 { - let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); - keys.push(directory.save(key).unwrap()); - } - - assert_eq!(33, directory.list().unwrap().len()); - } -} diff --git a/util/src/keys/geth_import.rs b/util/src/keys/geth_import.rs deleted file mode 100644 index 509ebc89f..000000000 --- a/util/src/keys/geth_import.rs +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2015, 2016 Ethcore (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -//! Geth keys import/export tool - -use common::*; -use keys::store::SecretStore; -use keys::directory::KeyFileContent; -use std::path::PathBuf; -use path; - -/// Enumerates all geth keys in the directory and returns collection of tuples `(accountId, filename)` -pub fn enumerate_geth_keys(path: &Path) -> Result, ImportError> { - let mut entries = Vec::new(); - for entry in try!(fs::read_dir(path)) { - let entry = try!(entry); - if !try!(fs::metadata(entry.path())).is_dir() { - match entry.file_name().to_str() { - Some(name) => { - let parts: Vec<&str> = name.split("--").collect(); - if parts.len() != 3 { continue; } - let account_id = try!(Address::from_str(parts[2]).map_err(|_| ImportError::Format)); - entries.push((account_id, name.to_owned())); - }, - None => { continue; } - }; - } - } - Ok(entries) -} - -/// Geth import error -#[derive(Debug)] -pub enum ImportError { - /// Io error reading geth file - Io(io::Error), - /// format error - Format, -} - -impl From for ImportError { - fn from (err: io::Error) -> ImportError { - ImportError::Io(err) - } -} - -/// Imports one geth key to the store -pub fn import_geth_key(secret_store: &mut SecretStore, geth_keyfile_path: &Path) -> Result<(), ImportError> { - let mut file = try!(fs::File::open(geth_keyfile_path)); - let mut buf = String::new(); - try!(file.read_to_string(&mut buf)); - - let mut json_result = Json::from_str(&buf); - let mut json = match json_result { - Ok(ref mut parsed_json) => try!(parsed_json.as_object_mut().ok_or(ImportError::Format)), - Err(_) => { return Err(ImportError::Format); } - }; - if let Some(crypto_object) = json.get("Crypto").and_then(|crypto| crypto.as_object()).cloned() { - json.insert("crypto".to_owned(), Json::Object(crypto_object)); - json.remove("Crypto"); - } - match KeyFileContent::load(&Json::Object(json.clone())) { - Ok(key_file) => try!(secret_store.import_key(key_file)), - Err(_) => { return Err(ImportError::Format); } - }; - Ok(()) -} - -/// Imports all geth keys in the directory -pub fn import_geth_keys(secret_store: &mut SecretStore, geth_keyfiles_directory: &Path) -> Result { - use std::path::PathBuf; - let geth_files = try!(enumerate_geth_keys(geth_keyfiles_directory)); - let mut total = 0; - for &(ref address, ref file_path) in &geth_files { - let mut path = PathBuf::new(); - path.push(geth_keyfiles_directory); - path.push(file_path); - if let Err(e) = import_geth_key(secret_store, Path::new(&path)) { - warn!("Skipped geth address {}, error importing: {:?}", address, e) - } - else { total = total + 1} - } - Ok(total) -} - - -/// Gets the default geth keystore directory. -pub fn keystore_dir(is_testnet: bool) -> PathBuf { - path::ethereum::with_default(if is_testnet {"testnet/keystore"} else {"keystore"}) -} - -/// Imports key(s) from provided file/directory -pub fn import_keys_path(secret_store: &mut SecretStore, path: &str) -> Result { - // check if it is just one file or directory - if let Ok(meta) = fs::metadata(path) { - if meta.is_file() { - try!(import_geth_key(secret_store, Path::new(path))); - return Ok(1); - } - else if meta.is_dir() { - return Ok(try!(fs::read_dir(path)).fold( - 0, - |total, p| - total + - match p { - Ok(dir_entry) => import_keys_path(secret_store, dir_entry.path().to_str().unwrap()).unwrap_or_else(|_| 0), - Err(e) => { warn!("Error importing dir entry: {:?}", e); 0 }, - } - )) - } - } - Ok(0) -} - -/// Imports all keys from list of provided files/directories -pub fn import_keys_paths(secret_store: &mut SecretStore, path: &[String]) -> Result { - Ok(path.iter().fold(0, |total, ref p| total + import_keys_path(secret_store, &p).unwrap_or_else(|_| 0))) -} - -#[cfg(test)] -mod tests { - use super::*; - use common::*; - use keys::store::SecretStore; - - fn test_path() -> &'static str { - match ::std::fs::metadata("res") { - Ok(_) => "res/geth_keystore", - Err(_) => "util/res/geth_keystore" - } - } - - fn pat_path() -> &'static str { - match ::std::fs::metadata("res") { - Ok(_) => "res/pat", - Err(_) => "util/res/pat" - } - } - - fn test_path_param(param_val: &'static str) -> String { - test_path().to_owned() + param_val - } - - fn pat_path_param(param_val: &'static str) -> String { - pat_path().to_owned() + param_val - } - - #[test] - fn can_enumerate() { - let keys = enumerate_geth_keys(Path::new(test_path())).unwrap(); - assert_eq!(3, keys.len()); - } - - #[test] - fn can_import_geth_old() { - let temp = ::devtools::RandomTempPath::create_dir(); - let mut secret_store = SecretStore::new_in(temp.as_path()); - import_geth_key(&mut secret_store, Path::new(&test_path_param("/UTC--2016-02-17T09-20-45.721400158Z--3f49624084b67849c7b4e805c5988c21a430f9d9"))).unwrap(); - let key = secret_store.account(&Address::from_str("3f49624084b67849c7b4e805c5988c21a430f9d9").unwrap()); - assert!(key.is_some()); - } - - #[test] - fn can_import_geth140() { - let temp = ::devtools::RandomTempPath::create_dir(); - let mut secret_store = SecretStore::new_in(temp.as_path()); - import_geth_key(&mut secret_store, Path::new(&test_path_param("/UTC--2016-04-03T08-58-49.834202900Z--63121b431a52f8043c16fcf0d1df9cb7b5f66649"))).unwrap(); - let key = secret_store.account(&Address::from_str("63121b431a52f8043c16fcf0d1df9cb7b5f66649").unwrap()); - assert!(key.is_some()); - } - - #[test] - fn can_import_directory() { - let temp = ::devtools::RandomTempPath::create_dir(); - let mut secret_store = SecretStore::new_in(temp.as_path()); - import_geth_keys(&mut secret_store, Path::new(test_path())).unwrap(); - - let key = secret_store.account(&Address::from_str("3f49624084b67849c7b4e805c5988c21a430f9d9").unwrap()); - assert!(key.is_some()); - - let key = secret_store.account(&Address::from_str("5ba4dcf897e97c2bdf8315b9ef26c13c085988cf").unwrap()); - assert!(key.is_some()); - } - - #[test] - fn imports_as_scrypt_keys() { - use keys::directory::{KeyDirectory, KeyFileKdf}; - let temp = ::devtools::RandomTempPath::create_dir(); - { - let mut secret_store = SecretStore::new_in(temp.as_path()); - import_geth_keys(&mut secret_store, Path::new(test_path())).unwrap(); - } - - let key_directory = KeyDirectory::new(&temp.as_path()); - let key_file = key_directory.get(&H128::from_str("62a0ad73556d496a8e1c0783d30d3ace").unwrap()).unwrap(); - - match key_file.crypto.kdf { - KeyFileKdf::Scrypt(scrypt_params) => { - assert_eq!(262144, scrypt_params.n); - assert_eq!(8, scrypt_params.r); - assert_eq!(1, scrypt_params.p); - }, - _ => { panic!("expected kdf params of crypto to be of scrypt type" ); } - } - } - - #[test] - #[cfg(feature="heavy-tests")] - fn can_decrypt_with_imported() { - use keys::store::EncryptedHashMap; - - let temp = ::devtools::RandomTempPath::create_dir(); - let mut secret_store = SecretStore::new_in(temp.as_path()); - import_geth_keys(&mut secret_store, Path::new(test_path())).unwrap(); - - let val = secret_store.get::(&H128::from_str("62a0ad73556d496a8e1c0783d30d3ace").unwrap(), "123"); - assert!(val.is_ok()); - assert_eq!(32, val.unwrap().len()); - } - - #[test] - fn can_import_by_filename() { - let temp = ::devtools::RandomTempPath::create_dir(); - let mut secret_store = SecretStore::new_in(temp.as_path()); - - let amount = import_keys_path(&mut secret_store, &pat_path_param("/p1.json")).unwrap(); - assert_eq!(1, amount); - } - - #[test] - fn can_import_by_dir() { - let temp = ::devtools::RandomTempPath::create_dir(); - let mut secret_store = SecretStore::new_in(temp.as_path()); - - let amount = import_keys_path(&mut secret_store, pat_path()).unwrap(); - assert_eq!(2, amount); - } - - #[test] - fn can_import_mulitple() { - let temp = ::devtools::RandomTempPath::create_dir(); - let mut secret_store = SecretStore::new_in(temp.as_path()); - - let amount = import_keys_paths(&mut secret_store, &[pat_path_param("/p1.json"), pat_path_param("/p2.json")]).unwrap(); - assert_eq!(2, amount); - } - -} diff --git a/util/src/keys/mod.rs b/util/src/keys/mod.rs deleted file mode 100644 index 39c39aeef..000000000 --- a/util/src/keys/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015, 2016 Ethcore (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -//! Key management module - -pub mod directory; -pub mod store; -mod geth_import; -mod test_account_provider; - -pub use self::store::AccountProvider; -pub use self::test_account_provider::{TestAccount, TestAccountProvider}; -pub use self::geth_import::import_keys_paths; diff --git a/util/src/keys/store.rs b/util/src/keys/store.rs deleted file mode 100644 index b8a1de272..000000000 --- a/util/src/keys/store.rs +++ /dev/null @@ -1,754 +0,0 @@ -// Copyright 2015, 2016 Ethcore (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -//! Secret Store - -use keys::directory::*; -use common::*; -use rcrypto::pbkdf2::*; -use rcrypto::scrypt::*; -use rcrypto::hmac::*; -use crypto; -use chrono::*; - -const KEY_LENGTH: u32 = 32; -const KEY_ITERATIONS: u32 = 10240; -const KEY_LENGTH_AES: u32 = KEY_LENGTH/2; - -const KEY_LENGTH_USIZE: usize = KEY_LENGTH as usize; -const KEY_LENGTH_AES_USIZE: usize = KEY_LENGTH_AES as usize; - -// TODO: this file needs repotting into several separate files. - -/// Encrypted hash-map, each request should contain password -pub trait EncryptedHashMap { - /// Returns existing value for the key, if any - fn get(&self, key: &Key, password: &str) -> Result; - /// Insert new encrypted key-value and returns previous if there was any - fn insert(&mut self, key: Key, value: Value, password: &str) -> Option; - /// Removes key-value by key and returns the removed one, if any exists and password was provided - fn remove (&mut self, key: &Key, password: Option<&str>) -> Option; - /// Deletes key-value by key and returns if the key-value existed - fn delete(&mut self, key: &Key) -> bool { - self.remove::(key, None).is_some() - } -} - -/// Error retrieving value from encrypted hashmap -#[derive(Debug)] -pub enum EncryptedHashMapError { - /// Encryption failed - InvalidPassword, - /// No key in the hashmap - UnknownIdentifier, - /// Stored value is not well formed for the requested type - InvalidValueFormat(FromBytesError), -} - -/// Error while signing a message -#[derive(Debug)] -pub enum SigningError { - /// Account passed does not exist - NoAccount, - /// Account passed is not unlocked - AccountNotUnlocked, - /// Invalid passphrase - InvalidPassword, - /// Invalid secret in store - InvalidSecret -} - -/// Represent service for storing encrypted arbitrary data -pub struct SecretStore { - directory: KeyDirectory, - unlocks: RwLock>, - key_iterations: u32, -} - -struct AccountUnlock { - secret: H256, - /// expiration datetime (None - never) - expires: Option>, - /// Sccount should be relocked after first use. - relock_on_use: bool, -} - -/// Basic account management trait -pub trait AccountProvider: Send + Sync { - /// Lists all accounts - fn accounts(&self) -> Result, ::std::io::Error>; - /// Unlocks account with the password provided - fn unlock_account(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError>; - /// Unlocks account with the password provided; relocks it on the next call to `account_secret` or `sign`. - fn unlock_account_temp(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError>; - /// Creates account - fn new_account(&self, pass: &str) -> Result; - /// Returns secret for unlocked `account`. - fn account_secret(&self, account: &Address) -> Result; - /// Returns secret for locked account given passphrase. - fn locked_account_secret(&self, account: &Address, pass: &str) -> Result; - /// Returns signature when unlocked `account` signs `message`. - fn sign(&self, account: &Address, message: &H256) -> Result { - self.account_secret(account).and_then(|s| crypto::ec::sign(&s, message).map_err(|_| SigningError::InvalidSecret)) - } -} - -/// Thread-safe accounts management -pub struct AccountService { - secret_store: RwLock, -} - -impl AccountProvider for AccountService { - /// Lists all accounts - fn accounts(&self) -> Result, ::std::io::Error> { - Ok(try!(self.secret_store.read().unwrap().accounts()).iter().map(|&(addr, _)| addr).collect::>()) - } - /// Unlocks account with the password provided - fn unlock_account(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - self.secret_store.read().unwrap().unlock_account(account, pass) - } - fn unlock_account_temp(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - self.secret_store.read().unwrap().unlock_account_temp(account, pass) - } - /// Creates account - fn new_account(&self, pass: &str) -> Result { - self.secret_store.write().unwrap().new_account(pass) - } - /// Returns secret for unlocked account - fn account_secret(&self, account: &Address) -> Result { - self.secret_store.read().unwrap().account_secret(account) - } - /// Returns secret for locked account given passphrase. - fn locked_account_secret(&self, account: &Address, pass: &str) -> Result { - self.secret_store.read().unwrap().locked_account_secret(account, pass) - } - /// Signs a message using key of given unlocked account address. - fn sign(&self, account: &Address, message: &H256) -> Result { - self.secret_store.read().unwrap().sign(account, message) - } -} - -/// Which set of keys to import. -#[derive(PartialEq)] -pub enum ImportKeySet { - /// Empty set. - None, - /// Import legacy client's general keys. - Legacy, - /// Import legacy client's testnet keys. - LegacyTestnet, -} - -impl AccountService { - /// New account service with the keys store in specific location and configured security parameters. - pub fn with_security(path: &Path, key_iterations: u32, import_keys: ImportKeySet) -> Self { - let secret_store = RwLock::new(SecretStore::with_security(path, key_iterations)); - match import_keys { - ImportKeySet::None => {} - _ => { secret_store.write().unwrap().try_import_existing(import_keys == ImportKeySet::LegacyTestnet); } - } - AccountService { - secret_store: secret_store, - } - } - - #[cfg(test)] - fn new_test(temp: &::devtools::RandomTempPath) -> Self { - let secret_store = RwLock::new(SecretStore::new_test(temp)); - AccountService { - secret_store: secret_store - } - } - - /// Ticks the account service - pub fn tick(&self) { - self.secret_store.write().unwrap().collect_garbage(); - } - - /// Unlocks account for use (no expiration of unlock) - pub fn unlock_account_no_expire(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - self.secret_store.write().unwrap().unlock_account_with_expiration(account, pass, None, false) - } -} - -impl SecretStore { - /// new instance of Secret Store in specific directory - pub fn new_in(path: &Path) -> Self { - SecretStore::with_security(path, KEY_ITERATIONS) - } - - /// new instance of Secret Store in specific directory and configured security parameters - pub fn with_security(path: &Path, key_iterations: u32) -> Self { - ::std::fs::create_dir_all(&path).expect("Cannot access requested key directory - critical"); - SecretStore { - directory: KeyDirectory::new(path), - unlocks: RwLock::new(HashMap::new()), - key_iterations: key_iterations, - } - } - - /// trys to import keys in the known locations - pub fn try_import_existing(&mut self, is_testnet: bool) { - use keys::geth_import; - - let import_path = geth_import::keystore_dir(is_testnet); - if let Err(e) = geth_import::import_geth_keys(self, &import_path) { - trace!(target: "sstore", "Geth key not imported: {:?}", e); - } - } - - /// Lists all accounts and corresponding key ids - pub fn accounts(&self) -> Result, ::std::io::Error> { - let accounts = try!(self.directory.list()).iter().map(|key_id| self.directory.get(key_id)) - .filter(|key| key.is_some()) - .map(|key| { let some_key = key.unwrap(); (some_key.account, some_key.id) }) - .filter(|&(ref account, _)| account.is_some()) - .map(|(account, id)| (account.unwrap(), id)) - .collect::>(); - Ok(accounts) - } - - /// Resolves key_id by account address - pub fn account(&self, account: &Address) -> Option { - let mut accounts = match self.accounts() { - Ok(accounts) => accounts, - Err(e) => { warn!(target: "sstore", "Failed to load accounts: {}", e); return None; } - }; - accounts.retain(|&(ref store_account, _)| account == store_account); - accounts.first().and_then(|&(_, ref key_id)| Some(key_id.clone())) - } - - /// Imports pregenerated key, returns error if not saved correctly - pub fn import_key(&mut self, key_file: KeyFileContent) -> Result<(), ::std::io::Error> { - try!(self.directory.save(key_file)); - Ok(()) - } - - #[cfg(test)] - fn new_test(path: &::devtools::RandomTempPath) -> SecretStore { - SecretStore { - directory: KeyDirectory::new(path.as_path()), - unlocks: RwLock::new(HashMap::new()), - key_iterations: KEY_ITERATIONS, - } - } - - /// Unlocks account for use - pub fn unlock_account(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - self.unlock_account_with_expiration(account, pass, Some(UTC::now() + Duration::minutes(20)), false) - } - - /// Unlocks account for use (no expiration of unlock) - pub fn unlock_account_no_expire(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - self.unlock_account_with_expiration(account, pass, None, false) - } - - /// Unlocks account for use (no expiration of unlock) - pub fn unlock_account_temp(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - self.unlock_account_with_expiration(account, pass, None, true) - } - - fn unlock_account_with_expiration(&self, account: &Address, pass: &str, expiration: Option>, relock_on_use: bool) -> Result<(), EncryptedHashMapError> { - let secret_id = try!(self.account(&account).ok_or(EncryptedHashMapError::UnknownIdentifier)); - let secret = try!(self.get(&secret_id, pass)); - { - let mut write_lock = self.unlocks.write().unwrap(); - let mut unlock = write_lock.entry(*account) - .or_insert_with(|| AccountUnlock { secret: secret, expires: Some(UTC::now()), relock_on_use: relock_on_use }); - unlock.secret = secret; - unlock.expires = expiration; - } - Ok(()) - } - - /// Creates new account - pub fn new_account(&mut self, pass: &str) -> Result { - let key_pair = crypto::KeyPair::create().expect("Error creating key-pair. Something wrong with crypto libraries?"); - let address = Address::from(key_pair.public().sha3()); - let key_id = H128::random(); - self.insert(key_id.clone(), key_pair.secret().clone(), pass); - - let mut key_file = self.directory.get(&key_id).expect("the key was just inserted"); - key_file.account = Some(address); - try!(self.directory.save(key_file)); - Ok(address) - } - - /// Signs message with unlocked account. - pub fn sign(&self, account: &Address, message: &H256) -> Result { - let (relock, ret) = { - let read_lock = self.unlocks.read().unwrap(); - if let Some(unlock) = read_lock.get(account) { - (unlock.relock_on_use, match crypto::KeyPair::from_secret(unlock.secret) { - Ok(pair) => match pair.sign(message) { - Ok(signature) => Ok(signature), - Err(_) => Err(SigningError::InvalidSecret) - }, - Err(_) => Err(SigningError::InvalidSecret) - }) - } else { - (false, Err(SigningError::AccountNotUnlocked)) - } - }; - if relock { - self.unlocks.write().unwrap().remove(account); - } - ret - } - - /// Returns secret for unlocked account. - pub fn account_secret(&self, account: &Address) -> Result { - let (relock, ret) = { - let read_lock = self.unlocks.read().unwrap(); - if let Some(unlock) = read_lock.get(account) { - (unlock.relock_on_use, Ok(unlock.secret as crypto::Secret)) - } else { - (false, Err(SigningError::AccountNotUnlocked)) - } - }; - if relock { - self.unlocks.write().unwrap().remove(account); - } - ret - } - - /// Returns secret for locked account. - pub fn locked_account_secret(&self, account: &Address, pass: &str) -> Result { - let secret_id = try!(self.account(&account).ok_or(SigningError::NoAccount)); - self.get(&secret_id, pass).or_else(|e| Err(match e { - EncryptedHashMapError::InvalidPassword => SigningError::InvalidPassword, - EncryptedHashMapError::UnknownIdentifier => SigningError::NoAccount, - EncryptedHashMapError::InvalidValueFormat(_) => SigningError::InvalidSecret, - })) - } - - /// Makes account unlocks expire and removes unused key files from memory - pub fn collect_garbage(&mut self) { - let mut garbage_lock = self.unlocks.write().unwrap(); - self.directory.collect_garbage(); - let utc = UTC::now(); - let expired_addresses = garbage_lock.iter() - .filter(|&(_, unlock)| match unlock.expires { Some(ref expire_val) => expire_val < &utc, _ => false }) - .map(|(address, _)| address.clone()).collect::>(); - - for expired in expired_addresses { garbage_lock.remove(&expired); } - - garbage_lock.shrink_to_fit(); - } - - fn exists(&self, key: &H128) -> bool { - self.directory.exists(key) - } -} - -fn derive_key_iterations(password: &str, salt: &H256, c: u32) -> (Bytes, Bytes) { - let mut h_mac = Hmac::new(::rcrypto::sha2::Sha256::new(), password.as_bytes()); - let mut derived_key = vec![0u8; KEY_LENGTH_USIZE]; - pbkdf2(&mut h_mac, &salt.as_slice(), c, &mut derived_key); - let derived_right_bits = &derived_key[0..KEY_LENGTH_AES_USIZE]; - let derived_left_bits = &derived_key[KEY_LENGTH_AES_USIZE..KEY_LENGTH_USIZE]; - (derived_right_bits.to_vec(), derived_left_bits.to_vec()) -} - -fn derive_key(password: &str, salt: &H256, iterations: u32) -> (Bytes, Bytes) { - derive_key_iterations(password, salt, iterations) -} - -fn derive_key_scrypt(password: &str, salt: &H256, n: u32, p: u32, r: u32) -> (Bytes, Bytes) { - let mut derived_key = vec![0u8; KEY_LENGTH_USIZE]; - let scrypt_params = ScryptParams::new(n.trailing_zeros() as u8, r, p); - scrypt(password.as_bytes(), &salt.as_slice(), &scrypt_params, &mut derived_key); - let derived_right_bits = &derived_key[0..KEY_LENGTH_AES_USIZE]; - let derived_left_bits = &derived_key[KEY_LENGTH_AES_USIZE..KEY_LENGTH_USIZE]; - (derived_right_bits.to_vec(), derived_left_bits.to_vec()) -} - -fn derive_mac(derived_left_bits: &[u8], cipher_text: &[u8]) -> Bytes { - let mut mac = vec![0u8; KEY_LENGTH_AES_USIZE + cipher_text.len()]; - mac[0..KEY_LENGTH_AES_USIZE].clone_from_slice(derived_left_bits); - mac[KEY_LENGTH_AES_USIZE..cipher_text.len()+KEY_LENGTH_AES_USIZE].clone_from_slice(cipher_text); - mac -} - -impl EncryptedHashMap for SecretStore { - fn get(&self, key: &H128, password: &str) -> Result { - match self.directory.get(key) { - Some(key_file) => { - let (derived_left_bits, derived_right_bits) = match key_file.crypto.kdf { - KeyFileKdf::Pbkdf2(ref params) => derive_key_iterations(password, ¶ms.salt, params.c), - KeyFileKdf::Scrypt(ref params) => derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r) - }; - - if derive_mac(&derived_right_bits, &key_file.crypto.cipher_text) - .sha3() != key_file.crypto.mac { return Err(EncryptedHashMapError::InvalidPassword); } - - let mut val = vec![0u8; key_file.crypto.cipher_text.len()]; - match key_file.crypto.cipher_type { - CryptoCipherType::Aes128Ctr(ref iv) => { - crypto::aes::decrypt(&derived_left_bits, &iv.as_slice(), &key_file.crypto.cipher_text, &mut val); - } - }; - - match Value::from_bytes_variable(&val) { - Ok(value) => Ok(value), - Err(bytes_error) => Err(EncryptedHashMapError::InvalidValueFormat(bytes_error)) - } - }, - None => Err(EncryptedHashMapError::UnknownIdentifier) - } - } - - fn insert(&mut self, key: H128, value: Value, password: &str) -> Option { - let previous = if !self.exists(&key) { None } else { self.get(&key, password).ok() }; - - // crypto random initiators - let salt = H256::random(); - let iv = H128::random(); - - // two parts of derived key - // DK = [ DK[0..15] DK[16..31] ] = [derived_left_bits, derived_right_bits] - let (derived_left_bits, derived_right_bits) = derive_key(password, &salt, self.key_iterations); - - let mut cipher_text = vec![0u8; value.as_slice().len()]; - // aes-128-ctr with initial vector of iv - crypto::aes::encrypt(&derived_left_bits, &iv.clone(), &value.as_slice(), &mut cipher_text); - - // KECCAK(DK[16..31] ++ ), where DK[16..31] - derived_right_bits - let mac = derive_mac(&derived_right_bits, &cipher_text.clone()).sha3(); - - let mut key_file = KeyFileContent::new( - KeyFileCrypto::new_pbkdf2( - cipher_text, - iv, - salt, - mac, - self.key_iterations, - KEY_LENGTH)); - key_file.id = key; - if let Err(io_error) = self.directory.save(key_file) { - warn!("Error saving key file: {:?}", io_error); - } - previous - } - - fn remove(&mut self, key: &H128, password: Option<&str>) -> Option { - let previous = if let Some(pass) = password { - if let Ok(previous_value) = self.get(&key, pass) { Some(previous_value) } else { None } - } - else { None }; - - if let Err(io_error) = self.directory.delete(key) { - warn!("Error saving key file: {:?}", io_error); - } - previous - } - -} - -#[cfg(all(test, feature="heavy-tests"))] -mod vector_tests { - use super::{derive_mac,derive_key_iterations}; - use common::*; - - #[test] - fn mac_vector() { - let password = "testpassword"; - let salt = H256::from_str("ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd").unwrap(); - let cipher_text = FromHex::from_hex("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46").unwrap(); - let iterations = 262144u32; - - let (derived_left_bits, derived_right_bits) = derive_key_iterations(password, &salt, iterations); - assert_eq!("f06d69cdc7da0faffb1008270bca38f5", derived_left_bits.to_hex()); - assert_eq!("e31891a3a773950e6d0fea48a7188551", derived_right_bits.to_hex()); - - let mac_body = derive_mac(&derived_right_bits, &cipher_text); - assert_eq!("e31891a3a773950e6d0fea48a71885515318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", mac_body.to_hex()); - - let mac = mac_body.sha3(); - assert_eq!("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2", format!("{:?}", mac)); - } -} - - -#[cfg(test)] -mod tests { - use super::*; - use devtools::*; - use common::*; - use crypto::KeyPair; - use chrono::*; - - #[test] - fn can_insert() { - let temp = RandomTempPath::create_dir(); - let mut sstore = SecretStore::new_test(&temp); - - let id = H128::random(); - sstore.insert(id.clone(), "Cat".to_owned(), "pass"); - - assert!(sstore.get::(&id, "pass").is_ok()); - } - - #[test] - fn can_get_fail() { - let temp = RandomTempPath::create_dir(); - { - use keys::directory::{KeyFileContent, KeyFileCrypto}; - let mut write_sstore = SecretStore::new_test(&temp); - write_sstore.directory.save( - KeyFileContent::new( - KeyFileCrypto::new_pbkdf2( - FromHex::from_hex("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46").unwrap(), - H128::from_str("6087dab2f9fdbbfaddc31a909735c1e6").unwrap(), - H256::from_str("ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd").unwrap(), - H256::from_str("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2").unwrap(), - 262144, - 32))) - .unwrap(); - } - let sstore = SecretStore::new_test(&temp); - if let Ok(_) = sstore.get::(&H128::from_str("3198bc9c66725ab3d9954942343ae5b6").unwrap(), "testpassword") { - panic!("should be error loading key, we requested the wrong key"); - } - } - - fn pregenerate_keys(temp: &RandomTempPath, count: usize) -> Vec { - use keys::directory::{KeyFileContent, KeyFileCrypto}; - let mut write_sstore = SecretStore::new_test(&temp); - let mut result = Vec::new(); - for _ in 0..count { - result.push(write_sstore.directory.save( - KeyFileContent::new( - KeyFileCrypto::new_pbkdf2( - FromHex::from_hex("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46").unwrap(), - H128::from_str("6087dab2f9fdbbfaddc31a909735c1e6").unwrap(), - H256::from_str("ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd").unwrap(), - H256::from_str("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2").unwrap(), - 262144, - 32))) - .unwrap()); - } - result - } - - fn pregenerate_accounts(temp: &RandomTempPath, count: usize) -> Vec { - use keys::directory::{KeyFileContent, KeyFileCrypto}; - let mut write_sstore = SecretStore::new_test(&temp); - let mut result = Vec::new(); - for i in 0..count { - let mut key_file = - KeyFileContent::new( - KeyFileCrypto::new_pbkdf2( - FromHex::from_hex("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46").unwrap(), - H128::from_str("6087dab2f9fdbbfaddc31a909735c1e6").unwrap(), - H256::from_str("ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd").unwrap(), - H256::from_str("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2").unwrap(), - 262144, - 32)); - key_file.account = Some((i as u64).into()); - result.push(key_file.id.clone()); - write_sstore.import_key(key_file).unwrap(); - } - result - } - - #[test] - #[cfg(feature="heavy-tests")] - fn can_get() { - let temp = RandomTempPath::create_dir(); - let key_id = { - use keys::directory::{KeyFileContent, KeyFileCrypto}; - let mut write_sstore = SecretStore::new_test(&temp); - write_sstore.directory.save( - KeyFileContent::new( - KeyFileCrypto::new_pbkdf2( - FromHex::from_hex("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46").unwrap(), - H128::from_str("6087dab2f9fdbbfaddc31a909735c1e6").unwrap(), - H256::from_str("ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd").unwrap(), - H256::from_str("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2").unwrap(), - 262144, - 32))) - .unwrap() - }; - let sstore = SecretStore::new_test(&temp); - if let Err(e) = sstore.get::(&key_id, "testpassword") { - panic!("got no key: {:?}", e); - } - } - - #[test] - fn can_delete() { - let temp = RandomTempPath::create_dir(); - let keys = pregenerate_keys(&temp, 5); - - let mut sstore = SecretStore::new_test(&temp); - sstore.delete(&keys[2]); - - assert_eq!(4, sstore.directory.list().unwrap().len()) - } - - #[test] - fn can_create_account() { - let temp = RandomTempPath::create_dir(); - let mut sstore = SecretStore::new_test(&temp); - sstore.new_account("123").unwrap(); - assert_eq!(1, sstore.accounts().unwrap().len()); - } - - #[test] - fn can_unlock_account() { - let temp = RandomTempPath::create_dir(); - let mut sstore = SecretStore::new_test(&temp); - let address = sstore.new_account("123").unwrap(); - - let secret = sstore.unlock_account(&address, "123"); - assert!(secret.is_ok()); - } - - #[test] - fn can_sign_data() { - let temp = RandomTempPath::create_dir(); - let address = { - let mut sstore = SecretStore::new_test(&temp); - sstore.new_account("334").unwrap() - }; - let signature = { - let sstore = SecretStore::new_test(&temp); - sstore.unlock_account(&address, "334").unwrap(); - sstore.sign(&address, &H256::random()).unwrap() - }; - - assert!(signature != 0.into()); - } - - #[test] - fn can_relock_temp_account() { - let temp = RandomTempPath::create_dir(); - let address = { - let mut sstore = SecretStore::new_test(&temp); - sstore.new_account("334").unwrap() - }; - let signature = { - let sstore = SecretStore::new_test(&temp); - sstore.unlock_account_temp(&address, "334").unwrap(); - sstore.sign(&address, &H256::random()).unwrap(); - sstore.sign(&address, &H256::random()) - }; - assert!(signature.is_err()); - - let secret = { - let sstore = SecretStore::new_test(&temp); - sstore.unlock_account_temp(&address, "334").unwrap(); - sstore.account_secret(&address).unwrap(); - sstore.account_secret(&address) - }; - assert!(secret.is_err()); - } - - #[test] - fn can_import_account() { - use keys::directory::{KeyFileContent, KeyFileCrypto}; - let temp = RandomTempPath::create_dir(); - let mut key_file = - KeyFileContent::new( - KeyFileCrypto::new_pbkdf2( - FromHex::from_hex("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46").unwrap(), - H128::from_str("6087dab2f9fdbbfaddc31a909735c1e6").unwrap(), - H256::from_str("ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd").unwrap(), - H256::from_str("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2").unwrap(), - 262144, - 32)); - key_file.account = Some(Address::from_str("3f49624084b67849c7b4e805c5988c21a430f9d9").unwrap()); - - let mut sstore = SecretStore::new_test(&temp); - - sstore.import_key(key_file).unwrap(); - - assert_eq!(1, sstore.accounts().unwrap().len()); - assert!(sstore.account(&Address::from_str("3f49624084b67849c7b4e805c5988c21a430f9d9").unwrap()).is_some()); - } - - #[test] - fn can_list_accounts() { - let temp = RandomTempPath::create_dir(); - pregenerate_accounts(&temp, 30); - let sstore = SecretStore::new_test(&temp); - let accounts = sstore.accounts().unwrap(); - assert_eq!(30, accounts.len()); - } - - #[test] - fn validate_generated_addresses() { - let temp = RandomTempPath::create_dir(); - let mut sstore = SecretStore::new_test(&temp); - let addr = sstore.new_account("test").unwrap(); - sstore.unlock_account(&addr, "test").unwrap(); - let secret = sstore.account_secret(&addr).unwrap(); - let kp = KeyPair::from_secret(secret).unwrap(); - assert_eq!(Address::from(kp.public().sha3()), addr); - } - - - #[test] - fn secret_for_locked_account() { - // given - let temp = RandomTempPath::create_dir(); - let mut sstore = SecretStore::new_test(&temp); - let addr = sstore.new_account("test-pass").unwrap(); - - // when - // Invalid pass - let secret1 = sstore.locked_account_secret(&addr, "test-pass123"); - // Valid pass - let secret2 = sstore.locked_account_secret(&addr, "test-pass"); - // Account not unlocked - let secret3 = sstore.account_secret(&addr); - - - assert!(secret1.is_err(), "Invalid password should not return secret."); - assert!(secret2.is_ok(), "Should return secret provided valid passphrase."); - assert!(secret3.is_err(), "Account should still be locked."); - } - - - #[test] - fn can_create_service() { - let temp = RandomTempPath::create_dir(); - let svc = AccountService::new_test(&temp); - assert!(svc.accounts().unwrap().is_empty()); - } - - #[test] - fn accounts_expire() { - use std::collections::hash_map::*; - - let temp = RandomTempPath::create_dir(); - let svc = AccountService::new_test(&temp); - let address = svc.new_account("pass").unwrap(); - svc.unlock_account(&address, "pass").unwrap(); - assert!(svc.account_secret(&address).is_ok()); - { - let ss_rw = svc.secret_store.write().unwrap(); - let mut ua_rw = ss_rw.unlocks.write().unwrap(); - let entry = ua_rw.entry(address); - if let Entry::Occupied(mut occupied) = entry { occupied.get_mut().expires = Some(UTC::now() - Duration::minutes(1)) } - } - - svc.tick(); - - assert!(svc.account_secret(&address).is_err()); - } -} diff --git a/util/src/keys/test_account_provider.rs b/util/src/keys/test_account_provider.rs deleted file mode 100644 index 148996172..000000000 --- a/util/src/keys/test_account_provider.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2015, 2016 Ethcore (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -//! Test implementation of account provider. - -use std::sync::RwLock; -use std::collections::HashMap; -use std::io; -use hash::Address; -use crypto::{Secret, KeyPair}; -use super::store::{AccountProvider, SigningError, EncryptedHashMapError}; - -/// Account mock. -#[derive(Clone)] -pub struct TestAccount { - /// True if account is unlocked. - pub unlocked: bool, - /// Account's password. - pub password: String, - /// Account's secret. - pub secret: Secret, -} - -impl TestAccount { - /// Creates new test account. - pub fn new(password: &str) -> Self { - let pair = KeyPair::create().unwrap(); - TestAccount { - unlocked: false, - password: password.to_owned(), - secret: pair.secret().clone() - } - } - - /// Returns account address. - pub fn address(&self) -> Address { - KeyPair::from_secret(self.secret.clone()).unwrap().address() - } -} - -/// Test account provider. -pub struct TestAccountProvider { - /// Test provider accounts. - pub accounts: RwLock>, -} - -impl TestAccountProvider { - /// Basic constructor. - pub fn new(accounts: HashMap) -> Self { - TestAccountProvider { - accounts: RwLock::new(accounts), - } - } -} - -impl AccountProvider for TestAccountProvider { - fn accounts(&self) -> Result, io::Error> { - Ok(self.accounts.read().unwrap().keys().cloned().collect()) - } - - fn unlock_account(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - match self.accounts.write().unwrap().get_mut(account) { - Some(ref mut acc) if acc.password == pass => { - acc.unlocked = true; - Ok(()) - }, - Some(_) => Err(EncryptedHashMapError::InvalidPassword), - None => Err(EncryptedHashMapError::UnknownIdentifier), - } - } - - fn unlock_account_temp(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> { - // TODO; actually make it relock on use - self.unlock_account(account, pass) - } - - fn new_account(&self, pass: &str) -> Result { - let account = TestAccount::new(pass); - let address = KeyPair::from_secret(account.secret.clone()).unwrap().address(); - self.accounts.write().unwrap().insert(address.clone(), account); - Ok(address) - } - - fn account_secret(&self, address: &Address) -> Result { - // todo: consider checking if account is unlock. some test may need alteration then. - self.accounts - .read() - .unwrap() - .get(address) - .ok_or(SigningError::NoAccount) - .map(|acc| acc.secret.clone()) - } - - fn locked_account_secret(&self, address: &Address, pass: &str) -> Result { - let accounts = self.accounts.read().unwrap(); - match accounts.get(address) { - Some(ref acc) if acc.password == pass => { - Ok(acc.secret.clone()) - }, - Some(ref _acc) => Err(SigningError::InvalidPassword), - _ => Err(SigningError::NoAccount), - } - } -} - diff --git a/util/src/lib.rs b/util/src/lib.rs index e1a3537f2..e43bbbab0 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -151,7 +151,6 @@ pub mod io; pub mod network; pub mod log; pub mod panics; -pub mod keys; pub mod table; pub mod network_settings; pub mod path;