RPC for confirming with token

This commit is contained in:
Tomasz Drwięga 2016-11-30 16:11:41 +01:00
parent 6397556cbb
commit c028f106b1
10 changed files with 237 additions and 54 deletions

View File

@ -401,6 +401,28 @@ impl AccountProvider {
Ok((signature, new_token)) Ok((signature, new_token))
} }
/// Decrypts a message with given token. Returns a token to use in next operation for this account.
pub fn decrypt_with_token(&self, account: Address, token: AccountToken, shared_mac: &[u8], message: &[u8])
-> Result<(Vec<u8>, AccountToken), Error>
{
let is_std_password = try!(self.sstore.test_password(&account, &token));
let new_token = random_string(16);
let message = if is_std_password {
// Insert to transient store
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
// decrypt
try!(self.sstore.decrypt(&account, &token, shared_mac, message))
} else {
// check transient store
try!(self.transient_sstore.change_password(&account, &token, &new_token));
// and decrypt
try!(self.transient_sstore.decrypt(&account, &token, shared_mac, message))
};
Ok((message, new_token))
}
/// Decrypts a message. If password is not provided the account must be unlocked. /// Decrypts a message. If password is not provided the account must be unlocked.
pub fn decrypt(&self, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> { pub fn decrypt(&self, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account))); let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account)));
@ -477,8 +499,8 @@ mod tests {
#[test] #[test]
fn should_sign_and_return_token() { fn should_sign_and_return_token() {
let kp = Random.generate().unwrap();
// given // given
let kp = Random.generate().unwrap();
let ap = AccountProvider::transient_provider(); let ap = AccountProvider::transient_provider();
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok()); assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::fmt::Debug;
use rlp; use rlp;
use util::{Address, H256, U256, Uint, Bytes}; use util::{Address, H256, U256, Uint, Bytes};
use util::bytes::ToPretty; use util::bytes::ToPretty;
@ -37,45 +38,100 @@ use v1::types::{
pub const DEFAULT_MAC: [u8; 2] = [0, 0]; pub const DEFAULT_MAC: [u8; 2] = [0, 0];
pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload: ConfirmationPayload, pass: Option<String>) -> Result<ConfirmationResponse, Error> type AccountToken = String;
#[derive(Debug, Clone, PartialEq)]
pub enum SignWith {
Nothing,
Password(String),
Token(AccountToken),
}
#[derive(Debug)]
pub enum WithToken<T: Debug> {
No(T),
Yes(T, AccountToken),
}
impl<T: Debug> WithToken<T> {
pub fn map<S, F>(self, f: F) -> WithToken<S> where
S: Debug,
F: FnOnce(T) -> S,
{
match self {
WithToken::No(v) => WithToken::No(f(v)),
WithToken::Yes(v, token) => WithToken::Yes(f(v), token),
}
}
pub fn into_value(self) -> T {
match self {
WithToken::No(v) => v,
WithToken::Yes(v, ..) => v,
}
}
}
impl<T: Debug> From<(T, AccountToken)> for WithToken<T> {
fn from(tuple: (T, AccountToken)) -> Self {
WithToken::Yes(tuple.0, tuple.1)
}
}
pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload: ConfirmationPayload, pass: SignWith) -> Result<WithToken<ConfirmationResponse>, Error>
where C: MiningBlockChainClient, M: MinerService where C: MiningBlockChainClient, M: MinerService
{ {
match payload { match payload {
ConfirmationPayload::SendTransaction(request) => { ConfirmationPayload::SendTransaction(request) => {
sign_and_dispatch(client, miner, accounts, request, pass) sign_and_dispatch(client, miner, accounts, request, pass)
.map(|result| result
.map(RpcH256::from) .map(RpcH256::from)
.map(ConfirmationResponse::SendTransaction) .map(ConfirmationResponse::SendTransaction)
)
}, },
ConfirmationPayload::SignTransaction(request) => { ConfirmationPayload::SignTransaction(request) => {
sign_no_dispatch(client, miner, accounts, request, pass) sign_no_dispatch(client, miner, accounts, request, pass)
.map(|result| result
.map(RpcRichRawTransaction::from) .map(RpcRichRawTransaction::from)
.map(ConfirmationResponse::SignTransaction) .map(ConfirmationResponse::SignTransaction)
)
}, },
ConfirmationPayload::Signature(address, hash) => { ConfirmationPayload::Signature(address, hash) => {
signature(accounts, address, hash, pass) signature(accounts, address, hash, pass)
.map(|result| result
.map(RpcH520::from) .map(RpcH520::from)
.map(ConfirmationResponse::Signature) .map(ConfirmationResponse::Signature)
)
}, },
ConfirmationPayload::Decrypt(address, data) => { ConfirmationPayload::Decrypt(address, data) => {
decrypt(accounts, address, data, pass) decrypt(accounts, address, data, pass)
.map(|result| result
.map(RpcBytes) .map(RpcBytes)
.map(ConfirmationResponse::Decrypt) .map(ConfirmationResponse::Decrypt)
)
}, },
} }
} }
fn signature(accounts: &AccountProvider, address: Address, hash: H256, password: Option<String>) -> Result<Signature, Error> { fn signature(accounts: &AccountProvider, address: Address, hash: H256, password: SignWith) -> Result<WithToken<Signature>, Error> {
accounts.sign(address, password.clone(), hash).map_err(|e| match password { match password.clone() {
Some(_) => errors::from_password_error(e), SignWith::Nothing => accounts.sign(address, None, hash).map(WithToken::No),
None => errors::from_signing_error(e), SignWith::Password(pass) => accounts.sign(address, Some(pass), hash).map(WithToken::No),
SignWith::Token(token) => accounts.sign_with_token(address, token, hash).map(Into::into),
}.map_err(|e| match password {
SignWith::Nothing => errors::from_signing_error(e),
_ => errors::from_password_error(e),
}) })
} }
fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: Option<String>) -> Result<Bytes, Error> { fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: SignWith) -> Result<WithToken<Bytes>, Error> {
accounts.decrypt(address, password.clone(), &DEFAULT_MAC, &msg) match password.clone() {
.map_err(|e| match password { SignWith::Nothing => accounts.decrypt(address, None, &DEFAULT_MAC, &msg).map(WithToken::No),
Some(_) => errors::from_password_error(e), SignWith::Password(pass) => accounts.decrypt(address, Some(pass), &DEFAULT_MAC, &msg).map(WithToken::No),
None => errors::from_signing_error(e), SignWith::Token(token) => accounts.decrypt_with_token(address, token, &DEFAULT_MAC, &msg).map(Into::into),
}.map_err(|e| match password {
SignWith::Nothing => errors::from_signing_error(e),
_ => errors::from_password_error(e),
}) })
} }
@ -88,7 +144,7 @@ pub fn dispatch_transaction<C, M>(client: &C, miner: &M, signed_transaction: Sig
.map(|_| hash) .map(|_| hash)
} }
pub fn sign_no_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: Option<String>) -> Result<SignedTransaction, Error> pub fn sign_no_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: SignWith) -> Result<WithToken<SignedTransaction>, Error>
where C: MiningBlockChainClient, M: MinerService { where C: MiningBlockChainClient, M: MinerService {
let network_id = client.signing_network_id(); let network_id = client.signing_network_id();
@ -110,20 +166,32 @@ pub fn sign_no_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider,
let hash = t.hash(network_id); let hash = t.hash(network_id);
let signature = try!(signature(accounts, address, hash, password)); let signature = try!(signature(accounts, address, hash, password));
t.with_signature(signature, network_id) signature.map(|sig| {
t.with_signature(sig, network_id)
})
}; };
Ok(signed_transaction) Ok(signed_transaction)
} }
pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: Option<String>) -> Result<H256, Error> pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider, filled: FilledTransactionRequest, password: SignWith) -> Result<WithToken<H256>, Error>
where C: MiningBlockChainClient, M: MinerService where C: MiningBlockChainClient, M: MinerService
{ {
let network_id = client.signing_network_id(); let network_id = client.signing_network_id();
let signed_transaction = try!(sign_no_dispatch(client, miner, accounts, filled, password)); let signed_transaction = try!(sign_no_dispatch(client, miner, accounts, filled, password));
let (signed_transaction, token) = match signed_transaction {
WithToken::No(signed_transaction) => (signed_transaction, None),
WithToken::Yes(signed_transaction, token) => (signed_transaction, Some(token)),
};
trace!(target: "miner", "send_transaction: dispatching tx: {} for network ID {:?}", rlp::encode(&signed_transaction).to_vec().pretty(), network_id); trace!(target: "miner", "send_transaction: dispatching tx: {} for network ID {:?}", rlp::encode(&signed_transaction).to_vec().pretty(), network_id);
dispatch_transaction(&*client, &*miner, signed_transaction) dispatch_transaction(&*client, &*miner, signed_transaction).map(|hash| {
match token {
Some(ref token) => WithToken::Yes(hash, token.clone()),
None => WithToken::No(hash),
}
})
} }
pub fn fill_optional_fields<C, M>(request: TransactionRequest, client: &C, miner: &M) -> FilledTransactionRequest pub fn fill_optional_fields<C, M>(request: TransactionRequest, client: &C, miner: &M) -> FilledTransactionRequest

View File

@ -114,7 +114,7 @@ impl<C: 'static, M: 'static> Personal for PersonalClient<C, M> where C: MiningBl
&*miner, &*miner,
&*accounts, &*accounts,
request, request,
Some(password) dispatch::SignWith::Password(password)
).map(Into::into) ).map(|v| v.into_value().into())
} }
} }

View File

@ -26,7 +26,7 @@ use ethcore::miner::MinerService;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::traits::Signer; use v1::traits::Signer;
use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, U256, Bytes}; use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken, U256, Bytes};
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload}; use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload};
use v1::helpers::dispatch::{self, dispatch_transaction}; use v1::helpers::dispatch::{self, dispatch_transaction};
@ -60,6 +60,35 @@ impl<C: 'static, M: 'static> SignerClient<C, M> where C: MiningBlockChainClient,
take_weak!(self.client).keep_alive(); take_weak!(self.client).keep_alive();
Ok(()) Ok(())
} }
fn confirm_internal<F>(&self, id: U256, modification: TransactionModification, f: F) -> Result<ConfirmationResponse, Error> where
F: FnOnce(&C, &M, &AccountProvider, ConfirmationPayload) -> Result<ConfirmationResponse, Error>,
{
try!(self.active());
let id = id.into();
let accounts = take_weak!(self.accounts);
let signer = take_weak!(self.signer);
let client = take_weak!(self.client);
let miner = take_weak!(self.miner);
signer.peek(&id).map(|confirmation| {
let mut payload = confirmation.payload.clone();
// Modify payload
match (&mut payload, modification.gas_price) {
(&mut ConfirmationPayload::SendTransaction(ref mut request), Some(gas_price)) => {
request.gas_price = gas_price.into();
},
_ => {},
}
let result = f(&*client, &*miner, &*accounts, payload);
// Execute
if let Ok(ref response) = result {
signer.request_confirmed(id, Ok(response.clone()));
}
result
}).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id)))
}
} }
impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService { impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService {
@ -78,30 +107,14 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC
// TODO [ToDr] TransactionModification is redundant for some calls // TODO [ToDr] TransactionModification is redundant for some calls
// might be better to replace it in future // might be better to replace it in future
fn confirm_request(&self, id: U256, modification: TransactionModification, pass: String) -> Result<ConfirmationResponse, Error> { fn confirm_request(&self, id: U256, modification: TransactionModification, pass: String) -> Result<ConfirmationResponse, Error> {
try!(self.active()); self.confirm_internal(id, modification, move |client, miner, accounts, payload| {
dispatch::execute(client, miner, accounts, payload, dispatch::SignWith::Password(pass))
let id = id.into(); .map(|v| v.into_value())
let accounts = take_weak!(self.accounts); })
let signer = take_weak!(self.signer);
let client = take_weak!(self.client);
let miner = take_weak!(self.miner);
signer.peek(&id).map(|confirmation| {
let mut payload = confirmation.payload.clone();
// Modify payload
match (&mut payload, modification.gas_price) {
(&mut ConfirmationPayload::SendTransaction(ref mut request), Some(gas_price)) => {
request.gas_price = gas_price.into();
},
_ => {},
} }
// Execute
let result = dispatch::execute(&*client, &*miner, &*accounts, payload, Some(pass)); fn confirm_request_with_token(&self, id: U256, modification: TransactionModification, token: String) -> Result<ConfirmationResponseWithToken, Error> {
if let Ok(ref response) = result { unimplemented!()
signer.request_confirmed(id, Ok(response.clone()));
}
result
}).unwrap_or_else(|| Err(errors::invalid_params("Unknown RequestID", id)))
} }
fn confirm_request_raw(&self, id: U256, bytes: Bytes) -> Result<ConfirmationResponse, Error> { fn confirm_request_raw(&self, id: U256, bytes: Bytes) -> Result<ConfirmationResponse, Error> {

View File

@ -99,7 +99,9 @@ impl<C, M> SigningQueueClient<C, M> where
let sender = payload.sender(); let sender = payload.sender();
if accounts.is_unlocked(sender) { if accounts.is_unlocked(sender) {
return dispatch::execute(&*client, &*miner, &*accounts, payload, None).map(DispatchResult::Value); return dispatch::execute(&*client, &*miner, &*accounts, payload, dispatch::SignWith::Nothing)
.map(|v| v.into_value())
.map(DispatchResult::Value);
} }
take_weak!(self.signer).add_request(payload) take_weak!(self.signer).add_request(payload)

View File

@ -75,7 +75,8 @@ impl<C, M> SigningUnsafeClient<C, M> where
let accounts = take_weak!(self.accounts); let accounts = take_weak!(self.accounts);
let payload = dispatch::from_rpc(payload, &*client, &*miner); let payload = dispatch::from_rpc(payload, &*client, &*miner);
dispatch::execute(&*client, &*miner, &*accounts, payload, None) dispatch::execute(&*client, &*miner, &*accounts, payload, dispatch::SignWith::Nothing)
.map(|v| v.into_value())
} }
} }

View File

@ -209,6 +209,52 @@ fn should_confirm_transaction_and_dispatch() {
assert_eq!(tester.miner.imported_transactions.lock().len(), 1); assert_eq!(tester.miner.imported_transactions.lock().len(), 1);
} }
#[test]
fn should_confirm_transaction_with_token() {
// given
let tester = signer_tester();
let address = tester.accounts.new_account("test").unwrap();
let recipient = Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap();
tester.signer.add_request(ConfirmationPayload::SendTransaction(FilledTransactionRequest {
from: address,
to: Some(recipient),
gas_price: U256::from(10_000),
gas: U256::from(10_000_000),
value: U256::from(1),
data: vec![],
nonce: None,
})).unwrap();
let t = Transaction {
nonce: U256::zero(),
gas_price: U256::from(0x1000),
gas: U256::from(10_000_000),
action: Action::Call(recipient),
value: U256::from(0x1),
data: vec![]
};
let (signature, token) = tester.accounts.sign_with_token(address, "test".into(), t.hash(None)).unwrap();
let t = t.with_signature(signature, None);
assert_eq!(tester.signer.requests().len(), 1);
// when
let request = r#"{
"jsonrpc":"2.0",
"method":"signer_confirmRequestWithToken",
"params":["0x1", {"gasPrice":"0x1000"}, ""#.to_owned() + &token + r#""],
"id":1
}"#;
let response = r#"{"jsonrpc":"2.0","result":{"result":""#.to_owned() +
format!("0x{:?}", t.hash()).as_ref() +
r#""token":""},"id":1}"#;
// then
assert_eq!(tester.io.handle_request_sync(&request), Some(response.to_owned()));
assert_eq!(tester.signer.requests().len(), 0);
assert_eq!(tester.miner.imported_transactions.lock().len(), 1);
}
#[test] #[test]
fn should_confirm_transaction_with_rlp() { fn should_confirm_transaction_with_rlp() {
// given // given

View File

@ -18,7 +18,7 @@
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::helpers::auto_args::Wrap; use v1::helpers::auto_args::Wrap;
use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse}; use v1::types::{U256, Bytes, TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken};
build_rpc_trait! { build_rpc_trait! {
@ -33,6 +33,10 @@ build_rpc_trait! {
#[rpc(name = "signer_confirmRequest")] #[rpc(name = "signer_confirmRequest")]
fn confirm_request(&self, U256, TransactionModification, String) -> Result<ConfirmationResponse, Error>; fn confirm_request(&self, U256, TransactionModification, String) -> Result<ConfirmationResponse, Error>;
/// Confirm specific request with token.
#[rpc(name = "signer_confirmRequestWithToken")]
fn confirm_request_with_token(&self, U256, TransactionModification, String) -> Result<ConfirmationResponseWithToken, Error>;
/// Confirm specific request with already signed data. /// Confirm specific request with already signed data.
#[rpc(name = "signer_confirmRequestRaw")] #[rpc(name = "signer_confirmRequestRaw")]
fn confirm_request_raw(&self, U256, Bytes) -> Result<ConfirmationResponse, Error>; fn confirm_request_raw(&self, U256, Bytes) -> Result<ConfirmationResponse, Error>;

View File

@ -101,6 +101,15 @@ impl Serialize for ConfirmationResponse {
} }
} }
/// Confirmation response with additional token for further requests
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ConfirmationResponseWithToken {
/// Actual response
pub result: ConfirmationResponse,
/// New token
pub token: String,
}
/// Confirmation payload, i.e. the thing to be confirmed /// Confirmation payload, i.e. the thing to be confirmed
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
pub enum ConfirmationPayload { pub enum ConfirmationPayload {
@ -247,5 +256,21 @@ mod tests {
gas_price: None, gas_price: None,
}); });
} }
#[test]
fn should_serialize_confirmation_response_with_token() {
// given
let response = ConfirmationResponseWithToken {
result: ConfirmationResponse::SendTransaction(H256::default()),
token: "test-token".into(),
};
// when
let res = serde_json::to_string(&response);
let expected = r#"{"result":"0x0000000000000000000000000000000000000000","token":"test-token"}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());
}
} }

View File

@ -38,7 +38,9 @@ pub use self::bytes::Bytes;
pub use self::block::{RichBlock, Block, BlockTransactions}; pub use self::block::{RichBlock, Block, BlockTransactions};
pub use self::block_number::BlockNumber; pub use self::block_number::BlockNumber;
pub use self::call_request::CallRequest; pub use self::call_request::CallRequest;
pub use self::confirmations::{ConfirmationPayload, ConfirmationRequest, ConfirmationResponse, TransactionModification, SignRequest, DecryptRequest, Either}; pub use self::confirmations::{
ConfirmationPayload, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken, TransactionModification, SignRequest, DecryptRequest, Either
};
pub use self::filter::{Filter, FilterChanges}; pub use self::filter::{Filter, FilterChanges};
pub use self::hash::{H64, H160, H256, H512, H520, H2048}; pub use self::hash::{H64, H160, H256, H512, H520, H2048};
pub use self::index::Index; pub use self::index::Index;