diff --git a/ethcore/src/account_provider/mod.rs b/ethcore/src/account_provider/mod.rs index 975d2cbc9..568cbd4e3 100755 --- a/ethcore/src/account_provider/mod.rs +++ b/ethcore/src/account_provider/mod.rs @@ -31,6 +31,7 @@ use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator}; use ethjson::misc::AccountMeta; use hardware_wallet::{Error as HardwareError, HardwareWalletManager, KeyPath}; pub use ethstore::ethkey::Signature; +pub use ethstore::{Derivation, IndexDerivation}; /// Type of unlock. #[derive(Clone)] @@ -197,6 +198,20 @@ impl AccountProvider { Ok(account.address) } + /// Generates new derived account based on the existing one + /// If password is not provided, account must be unlocked + /// New account will be created with the same password (if save: true) + pub fn derive_account(&self, address: &Address, password: Option, derivation: Derivation, save: bool) + -> Result + { + let account = self.sstore.account_ref(&address)?; + let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?; + Ok( + if save { self.sstore.insert_derived(SecretVaultRef::Root, &account, &password, derivation)?.address } + else { self.sstore.generate_derived(&account, &password, derivation)? } + ) + } + /// Import a new presale wallet. pub fn import_presale(&self, presale_json: &[u8], password: &str) -> Result { let account = self.sstore.import_presale(SecretVaultRef::Root, presale_json, password)?; @@ -458,6 +473,15 @@ impl AccountProvider { Ok(self.sstore.sign(&account, &password, &message)?) } + /// Signs message using the derived secret. If password is not provided the account must be unlocked. + pub fn sign_derived(&self, address: &Address, password: Option, derivation: Derivation, message: Message) + -> Result + { + let account = self.sstore.account_ref(address)?; + let password = password.map(Ok).unwrap_or_else(|| self.password(&account))?; + Ok(self.sstore.sign_derived(&account, &password, derivation, &message)?) + } + /// Signs given message with supplied token. Returns a token to use in next signing within this session. pub fn sign_with_token(&self, address: Address, token: AccountToken, message: Message) -> Result<(Signature, AccountToken), SignError> { let account = self.sstore.account_ref(&address)?; @@ -593,7 +617,8 @@ mod tests { use super::{AccountProvider, Unlock, DappId}; use std::time::Instant; use ethstore::ethkey::{Generator, Random}; - use ethstore::StoreAccountRef; + use ethstore::{StoreAccountRef, Derivation}; + use util::H256; #[test] fn unlock_account_temp() { @@ -606,6 +631,75 @@ mod tests { assert!(ap.sign(kp.address(), None, Default::default()).is_err()); } + #[test] + fn derived_account_nosave() { + let kp = Random.generate().unwrap(); + let ap = AccountProvider::transient_provider(); + assert!(ap.insert_account(kp.secret().clone(), "base").is_ok()); + assert!(ap.unlock_account_permanently(kp.address(), "base".into()).is_ok()); + + let derived_addr = ap.derive_account( + &kp.address(), + None, + Derivation::SoftHash(H256::from(999)), + false, + ).expect("Derivation should not fail"); + + assert!(ap.unlock_account_permanently(derived_addr, "base".into()).is_err(), + "There should be an error because account is not supposed to be saved"); + } + + #[test] + fn derived_account_save() { + let kp = Random.generate().unwrap(); + let ap = AccountProvider::transient_provider(); + assert!(ap.insert_account(kp.secret().clone(), "base").is_ok()); + assert!(ap.unlock_account_permanently(kp.address(), "base".into()).is_ok()); + + let derived_addr = ap.derive_account( + &kp.address(), + None, + Derivation::SoftHash(H256::from(999)), + true, + ).expect("Derivation should not fail"); + + assert!(ap.unlock_account_permanently(derived_addr, "base_wrong".into()).is_err(), + "There should be an error because password is invalid"); + + assert!(ap.unlock_account_permanently(derived_addr, "base".into()).is_ok(), + "Should be ok because account is saved and password is valid"); + } + + #[test] + fn derived_account_sign() { + let kp = Random.generate().unwrap(); + let ap = AccountProvider::transient_provider(); + assert!(ap.insert_account(kp.secret().clone(), "base").is_ok()); + assert!(ap.unlock_account_permanently(kp.address(), "base".into()).is_ok()); + + let derived_addr = ap.derive_account( + &kp.address(), + None, + Derivation::SoftHash(H256::from(1999)), + true, + ).expect("Derivation should not fail"); + ap.unlock_account_permanently(derived_addr, "base".into()) + .expect("Should be ok because account is saved and password is valid"); + + let msg = Default::default(); + let signed_msg1 = ap.sign(derived_addr, None, msg) + .expect("Signing with existing unlocked account should not fail"); + let signed_msg2 = ap.sign_derived( + &kp.address(), + None, + Derivation::SoftHash(H256::from(1999)), + msg, + ).expect("Derived signing with existing unlocked account should not fail"); + + assert_eq!(signed_msg1, signed_msg2, + "Signed messages should match"); + } + #[test] fn unlock_account_perm() { let kp = Random.generate().unwrap(); diff --git a/ethkey/src/extended.rs b/ethkey/src/extended.rs index 2d2e27b5b..c8f057a4b 100644 --- a/ethkey/src/extended.rs +++ b/ethkey/src/extended.rs @@ -106,7 +106,7 @@ impl ExtendedSecret { } /// Private key component of the extended key. - pub fn secret(&self) -> &Secret { + pub fn as_raw(&self) -> &Secret { &self.secret } } @@ -127,7 +127,7 @@ impl ExtendedPublic { pub fn from_secret(secret: &ExtendedSecret) -> Result { Ok( ExtendedPublic::new( - derivation::point(**secret.secret())?, + derivation::point(**secret.as_raw())?, secret.chain_code.clone(), ) ) @@ -410,7 +410,7 @@ mod tests { let (private_seed, chain_code) = master_chain_basic(); let extended_secret = ExtendedSecret::with_code(Secret::from_slice(&*private_seed).unwrap(), chain_code); let derived = f(extended_secret); - assert_eq!(**derived.secret(), test_private); + assert_eq!(**derived.as_raw(), test_private); } #[test] @@ -419,14 +419,14 @@ mod tests { let extended_secret = ExtendedSecret::with_code(secret.clone(), 0u64.into()); // hardened - assert_eq!(&**extended_secret.secret(), &*secret); - assert_eq!(&**extended_secret.derive(2147483648.into()).secret(), &"0927453daed47839608e414a3738dfad10aed17c459bbd9ab53f89b026c834b6".into()); - assert_eq!(&**extended_secret.derive(2147483649.into()).secret(), &"44238b6a29c6dcbe9b401364141ba11e2198c289a5fed243a1c11af35c19dc0f".into()); + assert_eq!(&**extended_secret.as_raw(), &*secret); + assert_eq!(&**extended_secret.derive(2147483648.into()).as_raw(), &"0927453daed47839608e414a3738dfad10aed17c459bbd9ab53f89b026c834b6".into()); + assert_eq!(&**extended_secret.derive(2147483649.into()).as_raw(), &"44238b6a29c6dcbe9b401364141ba11e2198c289a5fed243a1c11af35c19dc0f".into()); // normal - assert_eq!(&**extended_secret.derive(0.into()).secret(), &"bf6a74e3f7b36fc4c96a1e12f31abc817f9f5904f5a8fc27713163d1f0b713f6".into()); - assert_eq!(&**extended_secret.derive(1.into()).secret(), &"bd4fca9eb1f9c201e9448c1eecd66e302d68d4d313ce895b8c134f512205c1bc".into()); - assert_eq!(&**extended_secret.derive(2.into()).secret(), &"86932b542d6cab4d9c65490c7ef502d89ecc0e2a5f4852157649e3251e2a3268".into()); + assert_eq!(&**extended_secret.derive(0.into()).as_raw(), &"bf6a74e3f7b36fc4c96a1e12f31abc817f9f5904f5a8fc27713163d1f0b713f6".into()); + assert_eq!(&**extended_secret.derive(1.into()).as_raw(), &"bd4fca9eb1f9c201e9448c1eecd66e302d68d4d313ce895b8c134f512205c1bc".into()); + assert_eq!(&**extended_secret.derive(2.into()).as_raw(), &"86932b542d6cab4d9c65490c7ef502d89ecc0e2a5f4852157649e3251e2a3268".into()); let extended_public = ExtendedPublic::from_secret(&extended_secret).expect("Extended public should be created"); let derived_public = extended_public.derive(0.into()).expect("First derivation of public should succeed"); @@ -436,7 +436,7 @@ mod tests { Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap(), 064.into(), ); - assert_eq!(&**keypair.derive(2147483648u32.into()).expect("Derivation of keypair should succeed").secret().secret(), &"edef54414c03196557cf73774bc97a645c9a1df2164ed34f0c2a78d1375a930c".into()); + assert_eq!(&**keypair.derive(2147483648u32.into()).expect("Derivation of keypair should succeed").secret().as_raw(), &"edef54414c03196557cf73774bc97a645c9a1df2164ed34f0c2a78d1375a930c".into()); } #[test] @@ -461,7 +461,7 @@ mod tests { let derivation_secret = H256::from_str("51eaf04f9dbbc1417dc97e789edd0c37ecda88bac490434e367ea81b71b7b015").unwrap(); let extended_secret = ExtendedSecret::with_code(secret.clone(), 1u64.into()); - assert_eq!(&**extended_secret.derive(Derivation::Hard(derivation_secret)).secret(), &"2bc2d696fb744d77ff813b4a1ef0ad64e1e5188b622c54ba917acc5ebc7c5486".into()); + assert_eq!(&**extended_secret.derive(Derivation::Hard(derivation_secret)).as_raw(), &"2bc2d696fb744d77ff813b4a1ef0ad64e1e5188b622c54ba917acc5ebc7c5486".into()); } #[test] diff --git a/ethkey/src/lib.rs b/ethkey/src/lib.rs index 3882b3559..9c0c7907d 100644 --- a/ethkey/src/lib.rs +++ b/ethkey/src/lib.rs @@ -51,7 +51,7 @@ pub use self::prefix::Prefix; pub use self::random::Random; pub use self::signature::{sign, verify_public, verify_address, recover, Signature}; pub use self::secret::Secret; -pub use self::extended::{ExtendedPublic, ExtendedSecret, ExtendedKeyPair, DerivationError}; +pub use self::extended::{ExtendedPublic, ExtendedSecret, ExtendedKeyPair, DerivationError, Derivation}; use bigint::hash::{H160, H256, H512}; diff --git a/ethstore/src/error.rs b/ethstore/src/error.rs index ff6b4e4db..8a2eb5e8b 100755 --- a/ethstore/src/error.rs +++ b/ethstore/src/error.rs @@ -18,6 +18,7 @@ use std::fmt; use std::io::Error as IoError; use ethkey::Error as EthKeyError; use crypto::Error as EthCryptoError; +use ethkey::DerivationError; #[derive(Debug)] pub enum Error { @@ -35,6 +36,7 @@ pub enum Error { CreationFailed, EthKey(EthKeyError), EthCrypto(EthCryptoError), + Derivation(DerivationError), Custom(String), } @@ -55,6 +57,7 @@ impl fmt::Display for Error { Error::CreationFailed => "Account creation failed".into(), Error::EthKey(ref err) => err.to_string(), Error::EthCrypto(ref err) => err.to_string(), + Error::Derivation(ref err) => format!("Derivation error: {:?}", err), Error::Custom(ref s) => s.clone(), }; @@ -79,3 +82,9 @@ impl From for Error { Error::EthCrypto(err) } } + +impl From for Error { + fn from(err: DerivationError) -> Self { + Error::Derivation(err) + } +} diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs index ff1a4fa67..86b6dce46 100755 --- a/ethstore/src/ethstore.rs +++ b/ethstore/src/ethstore.rs @@ -21,12 +21,12 @@ use parking_lot::{Mutex, RwLock}; use crypto::KEY_ITERATIONS; use random::Random; -use ethkey::{Signature, Address, Message, Secret, Public, KeyPair}; +use ethkey::{self, Signature, Address, Message, Secret, Public, KeyPair, ExtendedKeyPair}; use dir::{KeyDirectory, VaultKeyDirectory, VaultKey, SetKeyError}; use account::SafeAccount; use presale::PresaleWallet; use json::{self, Uuid}; -use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef}; +use {import, Error, SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation}; pub struct EthStore { store: EthMultiStore, @@ -54,6 +54,16 @@ impl SimpleSecretStore for EthStore { self.store.insert_account(vault, secret, password) } + fn insert_derived(&self, vault: SecretVaultRef, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) + -> Result + { + self.store.insert_derived(vault, account_ref, password, derivation) + } + + fn generate_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) -> Result { + self.store.generate_derived(account_ref, password, derivation) + } + fn account_ref(&self, address: &Address) -> Result { self.store.account_ref(address) } @@ -71,8 +81,13 @@ impl SimpleSecretStore for EthStore { } fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result { - let account = self.get(account)?; - account.sign(password, message) + self.get(account)?.sign(password, message) + } + + fn sign_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation, message: &Message) + -> Result + { + self.store.sign_derived(account_ref, password, derivation, message) } fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error> { @@ -340,6 +355,23 @@ impl EthMultiStore { return Ok(()); } + + fn generate(&self, secret: Secret, derivation: Derivation) -> Result { + let mut extended = ExtendedKeyPair::new(secret); + match derivation { + Derivation::Hierarchical(path) => { + for path_item in path { + extended = extended.derive( + if path_item.soft { ethkey::Derivation::Soft(path_item.index) } + else { ethkey::Derivation::Hard(path_item.index) } + )?; + } + }, + Derivation::SoftHash(h256) => { extended = extended.derive(ethkey::Derivation::Soft(h256))?; } + Derivation::HardHash(h256) => { extended = extended.derive(ethkey::Derivation::Hard(h256))?; } + } + Ok(extended) + } } impl SimpleSecretStore for EthMultiStore { @@ -350,6 +382,54 @@ impl SimpleSecretStore for EthMultiStore { self.import(vault, account) } + fn insert_derived(&self, vault: SecretVaultRef, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) + -> Result + { + let accounts = self.get(account_ref)?; + for account in accounts { + // Skip if password is invalid + if !account.check_password(password) { + continue; + } + let extended = self.generate(account.crypto.secret(password)?, derivation)?; + return self.insert_account(vault, extended.secret().as_raw().clone(), password); + } + Err(Error::InvalidPassword) + } + + fn generate_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) + -> Result + { + let accounts = self.get(&account_ref)?; + for account in accounts { + // Skip if password is invalid + if !account.check_password(password) { + continue; + } + let extended = self.generate(account.crypto.secret(password)?, derivation)?; + + return Ok(ethkey::public_to_address(extended.public().public())); + } + Err(Error::InvalidPassword) + } + + fn sign_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation, message: &Message) + -> Result + { + let accounts = self.get(&account_ref)?; + for account in accounts { + // Skip if password is invalid + if !account.check_password(password) { + continue; + } + let extended = self.generate(account.crypto.secret(password)?, derivation)?; + let secret = extended.secret().as_raw(); + return Ok(ethkey::sign(&secret, message)?) + } + Err(Error::InvalidPassword) + + } + fn account_ref(&self, address: &Address) -> Result { self.reload_accounts()?; self.cache.read().keys() @@ -511,7 +591,7 @@ impl SimpleSecretStore for EthMultiStore { let vault_provider = self.dir.as_vault_provider().ok_or(Error::VaultsAreNotSupported)?; vault_provider.vault_meta(name) }) - + } fn set_vault_meta(&self, name: &str, meta: &str) -> Result<(), Error> { @@ -527,9 +607,10 @@ mod tests { use dir::{KeyDirectory, MemoryDirectory, RootDiskDirectory}; use ethkey::{Random, Generator, KeyPair}; - use secret_store::{SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef}; + use secret_store::{SimpleSecretStore, SecretStore, SecretVaultRef, StoreAccountRef, Derivation}; use super::{EthStore, EthMultiStore}; use devtools::RandomTempPath; + use util::H256; fn keypair() -> KeyPair { Random.generate().unwrap() @@ -898,4 +979,27 @@ mod tests { assert_eq!(store.get_vault_meta(name1).unwrap(), "Hello, world!!!".to_owned()); assert!(store.get_vault_meta("vault2").is_err()); } + + #[test] + fn should_store_derived_keys() { + // given we have one account in the store + let store = store(); + let keypair = keypair(); + let address = store.insert_account(SecretVaultRef::Root, keypair.secret().clone(), "test").unwrap(); + + // when we deriving from that account + let derived = store.insert_derived( + SecretVaultRef::Root, + &address, + "test", + Derivation::HardHash(H256::from(0)), + ).unwrap(); + + // there should be 2 accounts in the store + let accounts = store.accounts().unwrap(); + assert_eq!(accounts.len(), 2); + + // and we can sign with the derived contract + assert!(store.sign(&derived, "test", &Default::default()).is_ok(), "Second password should work for second store."); + } } diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs index 51ac0afba..f092c3fe6 100755 --- a/ethstore/src/lib.rs +++ b/ethstore/src/lib.rs @@ -57,5 +57,8 @@ pub use self::error::Error; pub use self::ethstore::{EthStore, EthMultiStore}; pub use self::import::{import_accounts, read_geth_accounts}; pub use self::presale::PresaleWallet; -pub use self::secret_store::{SecretVaultRef, StoreAccountRef, SimpleSecretStore, SecretStore}; +pub use self::secret_store::{ + SecretVaultRef, StoreAccountRef, SimpleSecretStore, SecretStore, + Derivation, IndexDerivation, +}; pub use self::random::{random_phrase, random_string}; diff --git a/ethstore/src/secret_store.rs b/ethstore/src/secret_store.rs index 442c48a1d..1eff95335 100755 --- a/ethstore/src/secret_store.rs +++ b/ethstore/src/secret_store.rs @@ -19,6 +19,7 @@ use std::path::PathBuf; use ethkey::{Address, Message, Signature, Secret, Public}; use Error; use json::Uuid; +use util::H256; /// Key directory reference #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -40,10 +41,12 @@ pub struct StoreAccountRef { pub trait SimpleSecretStore: Send + Sync { fn insert_account(&self, vault: SecretVaultRef, secret: Secret, password: &str) -> Result; + fn insert_derived(&self, vault: SecretVaultRef, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) -> Result; fn change_password(&self, account: &StoreAccountRef, old_password: &str, new_password: &str) -> Result<(), Error>; fn remove_account(&self, account: &StoreAccountRef, password: &str) -> Result<(), Error>; - + fn generate_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation) -> Result; fn sign(&self, account: &StoreAccountRef, password: &str, message: &Message) -> Result; + fn sign_derived(&self, account_ref: &StoreAccountRef, password: &str, derivation: Derivation, message: &Message) -> Result; fn decrypt(&self, account: &StoreAccountRef, password: &str, shared_mac: &[u8], message: &[u8]) -> Result, Error>; fn accounts(&self) -> Result, Error>; @@ -116,3 +119,21 @@ impl Hash for StoreAccountRef { self.address.hash(state); } } + +/// Node in hierarchical derivation. +pub struct IndexDerivation { + /// Node is soft (allows proof of parent from parent node). + pub soft: bool, + /// Index sequence of the node. + pub index: u32, +} + +/// Derivation scheme for keys +pub enum Derivation { + /// Hierarchical derivation + Hierarchical(Vec), + /// Hash derivation, soft. + SoftHash(H256), + /// Hash derivation, hard. + HardHash(H256), +} diff --git a/rpc/src/v1/impls/parity_accounts.rs b/rpc/src/v1/impls/parity_accounts.rs index 16dbae64f..e28ea2510 100644 --- a/rpc/src/v1/impls/parity_accounts.rs +++ b/rpc/src/v1/impls/parity_accounts.rs @@ -25,7 +25,7 @@ use ethcore::account_provider::AccountProvider; use jsonrpc_core::Error; use v1::helpers::errors; use v1::traits::ParityAccounts; -use v1::types::{H160 as RpcH160, H256 as RpcH256, DappId}; +use v1::types::{H160 as RpcH160, H256 as RpcH256, DappId, Derive, DeriveHierarchical, DeriveHash}; /// Account management (personal) rpc implementation. pub struct ParityAccountsClient { @@ -261,6 +261,32 @@ impl ParityAccounts for ParityAccountsClient { .map_err(|e| errors::account("Could not update vault metadata.", e)) .map(|_| true) } + + fn derive_key_index(&self, addr: RpcH160, password: String, derivation: DeriveHierarchical, save_as_account: bool) -> Result { + let addr: Address = addr.into(); + take_weak!(self.accounts) + .derive_account( + &addr, + Some(password), + Derive::from(derivation).to_derivation() + .map_err(|c| errors::account("Could not parse derivation request: {:?}", c))?, + save_as_account) + .map(Into::into) + .map_err(|e| errors::account("Could not derive account.", e)) + } + + fn derive_key_hash(&self, addr: RpcH160, password: String, derivation: DeriveHash, save_as_account: bool) -> Result { + let addr: Address = addr.into(); + take_weak!(self.accounts) + .derive_account( + &addr, + Some(password), + Derive::from(derivation).to_derivation() + .map_err(|c| errors::account("Could not parse derivation request: {:?}", c))?, + save_as_account) + .map(Into::into) + .map_err(|e| errors::account("Could not derive account.", e)) + } } fn into_vec(a: Vec) -> Vec where diff --git a/rpc/src/v1/tests/mocked/parity_accounts.rs b/rpc/src/v1/tests/mocked/parity_accounts.rs index 0f0a1836a..6c518945f 100644 --- a/rpc/src/v1/tests/mocked/parity_accounts.rs +++ b/rpc/src/v1/tests/mocked/parity_accounts.rs @@ -393,3 +393,43 @@ fn rpc_parity_get_set_vault_meta() { assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); } + +// name: parity_deriveAddressHash +// example: {"jsonrpc": "2.0", "method": "parity_deriveAddressHash", "params": ["0xc171033d5cbff7175f29dfd3a63dda3d6f8f385e", "password1", { "type": "soft", "hash": "0x0c0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0c0c" }, true ], "id": 3} +#[test] +fn derive_key_hash() { + let tester = setup(); + let hash = tester.accounts + .insert_account( + "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a".parse().unwrap(), + "password1") + .expect("account should be inserted ok"); + + assert_eq!(hash, "c171033d5cbff7175f29dfd3a63dda3d6f8f385e".parse().unwrap()); + + // derive by hash + let request = r#"{"jsonrpc": "2.0", "method": "parity_deriveAddressHash", "params": ["0xc171033d5cbff7175f29dfd3a63dda3d6f8f385e", "password1", { "type": "soft", "hash": "0x0c0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0c0c" }, true ], "id": 3}"#; + let response = r#"{"jsonrpc":"2.0","result":"0xf28c28fcddf4a9b8f474237278d3647f9c0d1b3c","id":3}"#; + let res = tester.io.handle_request_sync(&request); + assert_eq!(res, Some(response.into())); +} + +// name: parity_deriveAddressIndex +// example: {"jsonrpc": "2.0", "method": "parity_deriveAddressIndex", "params": ["0xc171033d5cbff7175f29dfd3a63dda3d6f8f385e", "password1", [{ "type": "soft", "index": 0 }, { "type": "soft", "index": 1 }], false ], "id": 3} +#[test] +fn derive_key_index() { + let tester = setup(); + let hash = tester.accounts + .insert_account( + "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a".parse().unwrap(), + "password1") + .expect("account should be inserted ok"); + + assert_eq!(hash, "c171033d5cbff7175f29dfd3a63dda3d6f8f385e".parse().unwrap()); + + // derive by hash + let request = r#"{"jsonrpc": "2.0", "method": "parity_deriveAddressIndex", "params": ["0xc171033d5cbff7175f29dfd3a63dda3d6f8f385e", "password1", [{ "type": "soft", "index": 0 }, { "type": "soft", "index": 1 }], false ], "id": 3}"#; + let response = r#"{"jsonrpc":"2.0","result":"0xcc548e0bb2efe792a920ae0fbf583b13919f274f","id":3}"#; + let res = tester.io.handle_request_sync(&request); + assert_eq!(res, Some(response.into())); +} diff --git a/rpc/src/v1/traits/parity_accounts.rs b/rpc/src/v1/traits/parity_accounts.rs index 72aeeaa95..576786073 100644 --- a/rpc/src/v1/traits/parity_accounts.rs +++ b/rpc/src/v1/traits/parity_accounts.rs @@ -18,7 +18,7 @@ use std::collections::BTreeMap; use jsonrpc_core::Error; -use v1::types::{H160, H256, DappId}; +use v1::types::{H160, H256, DappId, DeriveHash, DeriveHierarchical}; build_rpc_trait! { /// Personal Parity rpc interface. @@ -141,5 +141,16 @@ build_rpc_trait! { /// Set vault metadata string. #[rpc(name = "parity_setVaultMeta")] fn set_vault_meta(&self, String, String) -> Result; + + /// Derive new address from given account address using specific hash. + /// Resulting address can be either saved as a new account (with the same password). + #[rpc(name = "parity_deriveAddressHash")] + fn derive_key_hash(&self, H160, String, DeriveHash, bool) -> Result; + + /// Derive new address from given account address using + /// hierarchical derivation (sequence of 32-bit integer indices). + /// Resulting address can be either saved as a new account (with the same password). + #[rpc(name = "parity_deriveAddressIndex")] + fn derive_key_index(&self, H160, String, DeriveHierarchical, bool) -> Result; } } diff --git a/rpc/src/v1/types/derivation.rs b/rpc/src/v1/types/derivation.rs new file mode 100644 index 000000000..decf48171 --- /dev/null +++ b/rpc/src/v1/types/derivation.rs @@ -0,0 +1,131 @@ +// Copyright 2015-2017 Parity Technologies (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 . + +use std::fmt; +use serde::{Deserialize, Deserializer}; +use serde::de::{Error, Visitor}; + +use ethstore; + +use super::hash::H256; + +/// Type of derivation +pub enum DerivationType { + /// Soft - allow proof of parent + Soft, + /// Hard - does not allow proof of parent + Hard, +} + +/// Derivation request by hash +#[derive(Deserialize)] +pub struct DeriveHash { + hash: H256, + #[serde(rename="type")] + d_type: DerivationType, +} + +/// Node propertoes in hierarchical derivation request +#[derive(Deserialize)] +pub struct DeriveHierarchicalItem { + index: u64, + #[serde(rename="type")] + d_type: DerivationType, +} + +/// Hierarchical (index sequence) request +pub type DeriveHierarchical = Vec; + +/// Generic derivate request +pub enum Derive { + /// Hierarchical (index sequence) request + Hierarchical(DeriveHierarchical), + /// Hash request + Hash(DeriveHash), +} + +impl From for Derive { + fn from(d: DeriveHierarchical) -> Self { + Derive::Hierarchical(d) + } +} + +impl From for Derive { + fn from(d: DeriveHash) -> Self { + Derive::Hash(d) + } +} + +/// Error converting request data +#[derive(Debug)] +pub enum ConvertError { + IndexOverlfow(u64), +} + +impl Derive { + /// Convert to account provider struct dealing with possible overflows + pub fn to_derivation(self) -> Result { + Ok(match self { + Derive::Hierarchical(drv) => { + ethstore::Derivation::Hierarchical({ + let mut members = Vec::::new(); + for h in drv { + if h.index > ::std::u32::MAX as u64 { return Err(ConvertError::IndexOverlfow(h.index)); } + members.push(match h.d_type { + DerivationType::Soft => ethstore::IndexDerivation { soft: true, index: h.index as u32 }, + DerivationType::Hard => ethstore::IndexDerivation { soft: false, index: h.index as u32 }, + }); + } + members + }) + }, + Derive::Hash(drv) => { + match drv.d_type { + DerivationType::Soft => ethstore::Derivation::SoftHash(drv.hash.into()), + DerivationType::Hard => ethstore::Derivation::HardHash(drv.hash.into()), + } + }, + }) + } +} + +impl Deserialize for DerivationType { + fn deserialize(deserializer: D) -> Result where D: Deserializer { + deserializer.deserialize(DerivationTypeVisitor) + } +} + +struct DerivationTypeVisitor; + +impl Visitor for DerivationTypeVisitor { + type Value = DerivationType; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "'hard' or 'soft'") + } + + fn visit_str(self, value: &str) -> Result where E: Error { + match value { + "soft" => Ok(DerivationType::Soft), + "hard" => Ok(DerivationType::Hard), + _ => Err(Error::custom("invalid derivation type")), + } + } + + fn visit_string(self, value: String) -> Result where E: Error { + self.visit_str(value.as_ref()) + } +} diff --git a/rpc/src/v1/types/mod.rs b/rpc/src/v1/types/mod.rs index 83223d947..a4bfcb41f 100644 --- a/rpc/src/v1/types/mod.rs +++ b/rpc/src/v1/types/mod.rs @@ -24,6 +24,7 @@ mod bytes; mod call_request; mod confirmations; mod consensus_status; +mod derivation; mod filter; mod hash; mod histogram; @@ -51,6 +52,7 @@ pub use self::confirmations::{ TransactionModification, SignRequest, DecryptRequest, Either }; pub use self::consensus_status::*; +pub use self::derivation::{DeriveHash, DeriveHierarchical, Derive}; pub use self::filter::{Filter, FilterChanges}; pub use self::hash::{H64, H160, H256, H512, H520, H2048}; pub use self::histogram::Histogram; @@ -70,4 +72,3 @@ pub use self::transaction_request::TransactionRequest; pub use self::transaction_condition::TransactionCondition; pub use self::uint::{U128, U256}; pub use self::work::Work; -