From 492a4570562973d96eca6eef8bba59d2b29cedb1 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Mon, 19 Aug 2019 13:05:21 +0100 Subject: [PATCH] Add `init_api_secure` function (#206) * adding initial version of init_secure_api * rustfmt * fix ECDH algo * rustfmt * trying to figure out best way of doing encryption * refactor secure requests and responses into json-rpc responses, with base64 payload for encrypted messages * rustfmt * return proper errors from encrypted api, include tests covering encrypted API error cases * rustfmt * add test for normal error (unencrypted) * rustfmt * change ports for test, add foreign listener to V2 sanity tests, add ability to select owner api port via command line * rustfmt * turn it to 11 * explicit teardown after rpc tests * update tests with explicit teardowns * update tests to perform explicit teardown * fix warnings, ensure all tests teardown * log output to diagnose CI windows build failures * disable owner api doctests on windows * rustfmt --- Cargo.lock | 32 +- api/Cargo.toml | 5 +- api/src/foreign_rpc.rs | 14 +- api/src/lib.rs | 13 +- api/src/owner.rs | 3 + api/src/owner_rpc.rs | 91 ++--- api/src/owner_rpc_s.rs | 44 ++- api/src/types.rs | 308 +++++++++++++++++ controller/Cargo.toml | 2 +- controller/src/controller.rs | 154 ++++++++- controller/tests/accounts.rs | 6 +- controller/tests/check.rs | 7 +- controller/tests/common/mod.rs | 9 +- controller/tests/file.rs | 6 +- controller/tests/invoice.rs | 351 ++++++++++---------- controller/tests/repost.rs | 6 +- controller/tests/restore.rs | 6 +- controller/tests/self_send.rs | 5 +- controller/tests/transaction.rs | 8 +- impls/src/adapters/http.rs | 6 +- impls/src/adapters/keybase.rs | 2 +- impls/src/lifecycle/default.rs | 1 + libwallet/src/error.rs | 4 + src/bin/grin-wallet.rs | 2 +- src/bin/grin-wallet.yml | 6 + src/cmd/wallet_args.rs | 24 +- tests/cmd_line_basic.rs | 3 +- tests/common/mod.rs | 126 ++++++- tests/data/v2_reqs/retrieve_info.req.json | 9 + tests/data/v3_reqs/init_secure_api.req.json | 8 + tests/data/v3_reqs/open_wallet.req.json | 10 + tests/{owner_v3.rs => owner_v2_sanity.rs} | 38 ++- tests/owner_v3_init_secure.rs | 219 ++++++++++++ 33 files changed, 1229 insertions(+), 299 deletions(-) create mode 100644 api/src/types.rs create mode 100644 tests/data/v2_reqs/retrieve_info.req.json create mode 100644 tests/data/v3_reqs/init_secure_api.req.json rename tests/{owner_v3.rs => owner_v2_sanity.rs} (67%) create mode 100644 tests/owner_v3_init_secure.rs diff --git a/Cargo.lock b/Cargo.lock index 95f575b4d..95656d2cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,18 @@ dependencies = [ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "easy-jsonrpc-mw" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "easy-jsonrpc-proc-macro-mw 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "jsonrpc-core 10.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "easy-jsonrpc-proc-macro" version = "0.5.0" @@ -417,6 +429,17 @@ dependencies = [ "syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "easy-jsonrpc-proc-macro-mw" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "encode_unicode" version = "0.3.5" @@ -789,8 +812,9 @@ dependencies = [ name = "grin_wallet_api" version = "2.1.0-beta.1" dependencies = [ + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "easy-jsonrpc 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "easy-jsonrpc-mw 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "grin_wallet_config 2.1.0-beta.1", @@ -798,6 +822,8 @@ dependencies = [ "grin_wallet_libwallet 2.1.0-beta.1", "grin_wallet_util 2.1.0-beta.1", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", @@ -823,7 +849,7 @@ name = "grin_wallet_controller" version = "2.1.0-beta.1" dependencies = [ "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "easy-jsonrpc 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "easy-jsonrpc-mw 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2839,7 +2865,9 @@ dependencies = [ "checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" "checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum easy-jsonrpc 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4a851f8e0ed5790b60ded487feb0dc3c7e7da52c4a0adc57c009bfc5af8ca1a" +"checksum easy-jsonrpc-mw 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c6f0a8e3a3a2c87620d0d0f1df8e619c2381affd6881558d19d66841b6335844" "checksum easy-jsonrpc-proc-macro 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9fb33793846951f339a70580375734416898ff8ddbb74401865031e25ba6751" +"checksum easy-jsonrpc-proc-macro-mw 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6368dbd2c6685fb84fc6e6a4749917ddc98905793fd06341c7e11a2504f2724" "checksum encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90b2c9496c001e8cb61827acdefad780795c42264c137744cae6f7d9e3450abd" "checksum enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" "checksum env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)" = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38" diff --git a/api/Cargo.toml b/api/Cargo.toml index f82fd0905..3720f50eb 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -15,10 +15,13 @@ failure_derive = "0.1" log = "0.4" uuid = { version = "0.7", features = ["serde", "v4"] } serde = "1" +rand = "0.5" serde_derive = "1" serde_json = "1" -easy-jsonrpc = "0.5.1" +easy-jsonrpc-mw = "0.5.3" chrono = { version = "0.4.4", features = ["serde"] } +ring = "0.13" +base64 = "0.9" grin_wallet_libwallet = { path = "../libwallet", version = "2.1.0-beta.1" } grin_wallet_config = { path = "../config", version = "2.1.0-beta.1" } diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs index 63fdaf136..a71cc6db5 100644 --- a/api/src/foreign_rpc.rs +++ b/api/src/foreign_rpc.rs @@ -20,13 +20,13 @@ use crate::libwallet::{ NodeVersionInfo, Slate, VersionInfo, VersionedSlate, WalletLCProvider, }; use crate::{Foreign, ForeignCheckMiddlewareFn}; -use easy_jsonrpc; +use easy_jsonrpc_mw; /// Public definition used to generate Foreign jsonrpc api. /// * When running `grin-wallet listen` with defaults, the V2 api is available at /// `localhost:3415/v2/foreign` /// * The endpoint only supports POST operations, with the json-rpc request as the body -#[easy_jsonrpc::rpc] +#[easy_jsonrpc_mw::rpc] pub trait ForeignRpc { /** Networked version of [Foreign::check_version](struct.Foreign.html#method.check_version). @@ -577,7 +577,7 @@ pub fn run_doctest_foreign( init_tx: bool, init_invoice_tx: bool, ) -> Result, String> { - use easy_jsonrpc::Handler; + use easy_jsonrpc_mw::Handler; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; use grin_wallet_libwallet::{api_impl, WalletInst}; @@ -613,7 +613,7 @@ pub fn run_doctest_foreign( let mut wallet1 = Box::new(DefaultWalletImpl::::new(client1.clone()).unwrap()) as Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider, LocalWalletClient, @@ -648,7 +648,7 @@ pub fn run_doctest_foreign( let mut wallet2 = Box::new(DefaultWalletImpl::::new(client2.clone()).unwrap()) as Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider, LocalWalletClient, @@ -751,7 +751,9 @@ pub fn run_doctest_foreign( }; api_foreign.doctest_mode = true; let foreign_api = &api_foreign as &dyn ForeignRpc; - Ok(foreign_api.handle_request(request).as_option()) + let res = foreign_api.handle_request(request).as_option(); + let _ = fs::remove_dir_all(test_dir); + Ok(res) } #[doc(hidden)] diff --git a/api/src/lib.rs b/api/src/lib.rs index 1e8826572..62eb33d2d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -43,6 +43,8 @@ mod owner; mod owner_rpc; mod owner_rpc_s; +mod types; + pub use crate::foreign::{Foreign, ForeignCheckMiddleware, ForeignCheckMiddlewareFn}; pub use crate::foreign_rpc::ForeignRpc; pub use crate::owner::Owner; @@ -53,13 +55,4 @@ pub use crate::foreign_rpc::foreign_rpc as foreign_rpc_client; pub use crate::foreign_rpc::run_doctest_foreign; pub use crate::owner_rpc::run_doctest_owner; -use grin_wallet_util::grin_core::libtx::secp_ser; -use util::secp::key::SecretKey; - -/// Wrapper for API Tokens -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(transparent)] -pub struct Token { - #[serde(with = "secp_ser::option_seckey_serde")] - keychain_mask: Option, -} +pub use types::{ECDHPubkey, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Token}; diff --git a/api/src/owner.rs b/api/src/owner.rs index df363c414..2a9413fb4 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -54,6 +54,8 @@ where pub wallet_inst: Arc>>>, /// Flag to normalize some output during testing. Can mostly be ignored. pub doctest_mode: bool, + /// Share ECDH key + pub shared_key: Arc>>, } impl<'a, L, C, K> Owner<'a, L, C, K> @@ -141,6 +143,7 @@ where Owner { wallet_inst, doctest_mode: false, + shared_key: Arc::new(Mutex::new(None)), } } diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 44503e707..5ec3be385 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -24,15 +24,15 @@ use crate::libwallet::{ }; use crate::util::Mutex; use crate::{Owner, OwnerRpcS}; -use easy_jsonrpc; +use easy_jsonrpc_mw; use std::sync::Arc; /// Public definition used to generate Owner jsonrpc api. /// * When running `grin-wallet owner_api` with defaults, the V2 api is available at /// `localhost:3420/v2/owner` /// * The endpoint only supports POST operations, with the json-rpc request as the body -#[easy_jsonrpc::rpc] -pub trait OwnerRpc { +#[easy_jsonrpc_mw::rpc] +pub trait OwnerRpc: Sync + Send { /** Networked version of [Owner::accounts](struct.Owner.html#method.accounts). @@ -1148,7 +1148,7 @@ pub trait OwnerRpc { } } # "# - # ,false, 5 ,true, false, false); + # ,false, 0 ,false, false, false); ``` */ fn verify_slate_messages(&self, slate: VersionedSlate) -> Result<(), ErrorKind>; @@ -1370,7 +1370,7 @@ pub fn run_doctest_owner( lock_tx: bool, finalize_tx: bool, ) -> Result, String> { - use easy_jsonrpc::Handler; + use easy_jsonrpc_mw::Handler; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; use grin_wallet_libwallet::{api_impl, WalletInst}; @@ -1404,7 +1404,7 @@ pub fn run_doctest_owner( let mut wallet1 = Box::new(DefaultWalletImpl::::new(client1.clone()).unwrap()) as Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider, LocalWalletClient, @@ -1439,7 +1439,7 @@ pub fn run_doctest_owner( let mut wallet2 = Box::new(DefaultWalletImpl::::new(client2.clone()).unwrap()) as Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider, LocalWalletClient, @@ -1547,13 +1547,15 @@ pub fn run_doctest_owner( let mut api_owner = Owner::new(wallet1); api_owner.doctest_mode = true; - if use_token { + let res = if use_token { let owner_api = &api_owner as &dyn OwnerRpcS; - Ok(owner_api.handle_request(request).as_option()) + owner_api.handle_request(request).as_option() } else { let owner_api = &api_owner as &dyn OwnerRpc; - Ok(owner_api.handle_request(request).as_option()) - } + owner_api.handle_request(request).as_option() + }; + let _ = fs::remove_dir_all(test_dir); + Ok(res) } #[doc(hidden)] @@ -1563,39 +1565,46 @@ macro_rules! doctest_helper_json_rpc_owner_assert_response { // create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return // json response. // In order to prevent leaking tempdirs, This function should not panic. - use grin_wallet_api::run_doctest_owner; - use serde_json; - use serde_json::Value; - use tempfile::tempdir; - - let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); - let dir = dir - .path() - .to_str() - .ok_or("Failed to convert tmpdir path to string.".to_owned()) - .unwrap(); - let request_val: Value = serde_json::from_str($request).unwrap(); - let expected_response: Value = serde_json::from_str($expected_response).unwrap(); - - let response = run_doctest_owner( - request_val, - dir, - $use_token, - $blocks_to_mine, - $perform_tx, - $lock_tx, - $finalize_tx, - ) - .unwrap() - .unwrap(); + // These cause LMDB to run out of disk space on CircleCI + // disable for now on windows + // TODO: Fix properly + #[cfg(not(target_os = "windows"))] + { + use grin_wallet_api::run_doctest_owner; + use serde_json; + use serde_json::Value; + use tempfile::tempdir; + + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + let dir = dir + .path() + .to_str() + .ok_or("Failed to convert tmpdir path to string.".to_owned()) + .unwrap(); + + let request_val: Value = serde_json::from_str($request).unwrap(); + let expected_response: Value = serde_json::from_str($expected_response).unwrap(); + + let response = run_doctest_owner( + request_val, + dir, + $use_token, + $blocks_to_mine, + $perform_tx, + $lock_tx, + $finalize_tx, + ) + .unwrap() + .unwrap(); - if response != expected_response { - panic!( - "(left != right) \nleft: {}\nright: {}", - serde_json::to_string_pretty(&response).unwrap(), - serde_json::to_string_pretty(&expected_response).unwrap() + if response != expected_response { + panic!( + "(left != right) \nleft: {}\nright: {}", + serde_json::to_string_pretty(&response).unwrap(), + serde_json::to_string_pretty(&expected_response).unwrap() ); + } } }; } diff --git a/api/src/owner_rpc_s.rs b/api/src/owner_rpc_s.rs index b95ecb56a..2861ee686 100644 --- a/api/src/owner_rpc_s.rs +++ b/api/src/owner_rpc_s.rs @@ -22,12 +22,15 @@ use crate::libwallet::{ OutputCommitMapping, Slate, SlateVersion, TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, }; -use crate::{Owner, Token}; -use easy_jsonrpc; +use crate::util::secp::key::{PublicKey, SecretKey}; +use crate::util::static_secp_instance; +use crate::{ECDHPubkey, Owner, Token}; +use easy_jsonrpc_mw; +use rand::thread_rng; /// Public definition used to generate Owner jsonrpc api. /// Secure version, that should be used when running the owner API in 'Secure' Mode -#[easy_jsonrpc::rpc] +#[easy_jsonrpc_mw::rpc] pub trait OwnerRpcS { /** Networked version of [Owner::accounts](struct.Owner.html#method.accounts). @@ -1199,7 +1202,7 @@ pub trait OwnerRpcS { } } # "# - # ,true, 5 ,true, false, false); + # ,true, 0 ,false, false, false); ``` */ fn verify_slate_messages(&self, token: Token, slate: VersionedSlate) -> Result<(), ErrorKind>; @@ -1300,6 +1303,13 @@ pub trait OwnerRpcS { ``` */ fn node_height(&self, token: Token) -> Result; + + /** + Initializes the secure RPC-JSON API + (Documentation TBD) + */ + + fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result; } impl<'a, L, C, K> OwnerRpcS for Owner<'a, L, C, K> @@ -1471,4 +1481,30 @@ where fn node_height(&self, token: Token) -> Result { Owner::node_height(self, (&token.keychain_mask).as_ref()).map_err(|e| e.kind()) } + + fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let sec_key = SecretKey::new(&secp, &mut thread_rng()); + + let mut shared_pubkey = ecdh_pubkey.ecdh_pubkey.clone(); + shared_pubkey + .mul_assign(&secp, &sec_key) + .map_err(|e| ErrorKind::Secp(e))?; + + let x_coord = shared_pubkey.serialize_vec(&secp, true); + let shared_key = + SecretKey::from_slice(&secp, &x_coord[1..]).map_err(|e| ErrorKind::Secp(e))?; + { + let mut s = self.shared_key.lock(); + *s = Some(shared_key); + } + + let pub_key = + PublicKey::from_secret_key(&secp, &sec_key).map_err(|e| ErrorKind::Secp(e))?; + + Ok(ECDHPubkey { + ecdh_pubkey: pub_key, + }) + } } diff --git a/api/src/types.rs b/api/src/types.rs new file mode 100644 index 000000000..2cd53c771 --- /dev/null +++ b/api/src/types.rs @@ -0,0 +1,308 @@ +// Copyright 2019 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::core::libtx::secp_ser; +use crate::libwallet::{Error, ErrorKind}; +use crate::util::secp::key::{PublicKey, SecretKey}; +use crate::util::{from_hex, to_hex}; +use failure::ResultExt; + +use base64; +use rand::{thread_rng, Rng}; +use ring::aead; +use serde_json::{self, Value}; +use std::collections::HashMap; + +/// Wrapper for API Tokens +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +pub struct Token { + #[serde(with = "secp_ser::option_seckey_serde")] + /// Token to XOR mask against the stored wallet seed + pub keychain_mask: Option, +} + +/// Wrapper for ECDH Public keys +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +pub struct ECDHPubkey { + /// public key, flattened + #[serde(with = "secp_ser::pubkey_serde")] + pub ecdh_pubkey: PublicKey, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptedBody { + /// nonce used for encryption + pub nonce: String, + /// Encrypted base64 body request + pub body_enc: String, +} + +impl EncryptedBody { + /// Encrypts and encodes json as base 64 + pub fn from_json(json_in: &Value, enc_key: &SecretKey) -> Result { + let mut to_encrypt = serde_json::to_string(&json_in) + .context(ErrorKind::APIEncryption( + "EncryptedBody Enc: Unable to encode JSON".to_owned(), + ))? + .as_bytes() + .to_vec(); + let sealing_key = aead::SealingKey::new(&aead::AES_256_GCM, &enc_key.0).context( + ErrorKind::APIEncryption("EncryptedBody Enc: Unable to create key".to_owned()), + )?; + let nonce: [u8; 12] = thread_rng().gen(); + let suffix_len = aead::AES_256_GCM.tag_len(); + for _ in 0..suffix_len { + to_encrypt.push(0); + } + aead::seal_in_place(&sealing_key, &nonce, &[], &mut to_encrypt, suffix_len).context( + ErrorKind::APIEncryption("EncryptedBody: Encryption Failed".to_owned()), + )?; + + Ok(EncryptedBody { + nonce: to_hex(nonce.to_vec()), + body_enc: base64::encode(&to_encrypt), + }) + } + + /// return serialize JSON self + pub fn as_json_value(&self) -> Result { + let res = serde_json::to_value(self).context(ErrorKind::APIEncryption( + "EncryptedBody: JSON serialization failed".to_owned(), + ))?; + Ok(res) + } + + /// return serialized JSON self as string + pub fn as_json_str(&self) -> Result { + let res = self.as_json_value()?; + let res = serde_json::to_string(&res).context(ErrorKind::APIEncryption( + "EncryptedBody: JSON String serialization failed".to_owned(), + ))?; + Ok(res) + } + + /// Return original request + pub fn decrypt(&self, dec_key: &SecretKey) -> Result { + let mut to_decrypt = base64::decode(&self.body_enc).context(ErrorKind::APIEncryption( + "EncryptedBody Dec: Encrypted request contains invalid Base64".to_string(), + ))?; + let opening_key = aead::OpeningKey::new(&aead::AES_256_GCM, &dec_key.0).context( + ErrorKind::APIEncryption("EncryptedBody Dec: Unable to create key".to_owned()), + )?; + let nonce = from_hex(self.nonce.clone()).context(ErrorKind::APIEncryption( + "EncryptedBody Dec: Invalid Nonce".to_string(), + ))?; + aead::open_in_place(&opening_key, &nonce, &[], 0, &mut to_decrypt).context( + ErrorKind::APIEncryption( + "EncryptedBody Dec: Decryption Failed (is key correct?)".to_string(), + ), + )?; + for _ in 0..aead::AES_256_GCM.tag_len() { + to_decrypt.pop(); + } + let decrypted = String::from_utf8(to_decrypt).context(ErrorKind::APIEncryption( + "EncryptedBody Dec: Invalid UTF-8".to_string(), + ))?; + Ok( + serde_json::from_str(&decrypted).context(ErrorKind::APIEncryption( + "EncryptedBody Dec: Invalid JSON".to_string(), + ))?, + ) + } +} + +/// Wrapper for secure JSON requests +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptedRequest { + /// JSON RPC response + pub jsonrpc: String, + /// method + pub method: String, + /// id + pub id: u32, + /// Body params, which includes nonce and encrypted request + pub params: EncryptedBody, +} + +impl EncryptedRequest { + /// from json + pub fn from_json(id: u32, json_in: &Value, enc_key: &SecretKey) -> Result { + Ok(EncryptedRequest { + jsonrpc: "2.0".to_owned(), + method: "encrypted_request_v3".to_owned(), + id: id, + params: EncryptedBody::from_json(json_in, enc_key)?, + }) + } + + /// return serialize JSON self + pub fn as_json_value(&self) -> Result { + let res = serde_json::to_value(self).context(ErrorKind::APIEncryption( + "EncryptedRequest: JSON serialization failed".to_owned(), + ))?; + Ok(res) + } + + /// return serialized JSON self as string + pub fn as_json_str(&self) -> Result { + let res = self.as_json_value()?; + let res = serde_json::to_string(&res).context(ErrorKind::APIEncryption( + "EncryptedRequest: JSON String serialization failed".to_owned(), + ))?; + Ok(res) + } + + /// Return decrypted body + pub fn decrypt(&self, dec_key: &SecretKey) -> Result { + self.params.decrypt(dec_key) + } +} + +/// Wrapper for secure JSON requests +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptedResponse { + /// JSON RPC response + pub jsonrpc: String, + /// id + pub id: u32, + /// result + pub result: HashMap, +} + +impl EncryptedResponse { + /// from json + pub fn from_json(id: u32, json_in: &Value, enc_key: &SecretKey) -> Result { + let mut result_set = HashMap::new(); + result_set.insert( + "Ok".to_string(), + EncryptedBody::from_json(json_in, enc_key)?, + ); + Ok(EncryptedResponse { + jsonrpc: "2.0".to_owned(), + id: id, + result: result_set, + }) + } + + /// return serialize JSON self + pub fn as_json_value(&self) -> Result { + let res = serde_json::to_value(self).context(ErrorKind::APIEncryption( + "EncryptedResponse: JSON serialization failed".to_owned(), + ))?; + Ok(res) + } + + /// return serialized JSON self as string + pub fn as_json_str(&self) -> Result { + let res = self.as_json_value()?; + let res = serde_json::to_string(&res).context(ErrorKind::APIEncryption( + "EncryptedResponse: JSON String serialization failed".to_owned(), + ))?; + Ok(res) + } + + /// Return decrypted body + pub fn decrypt(&self, dec_key: &SecretKey) -> Result { + self.result.get("Ok").unwrap().decrypt(dec_key) + } +} + +/// Wrapper for encryption error responses +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptionError { + /// code + pub code: i32, + /// message + pub message: String, +} + +/// Wrapper for encryption error responses +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptionErrorResponse { + /// JSON RPC response + pub jsonrpc: String, + /// id + pub id: u32, + /// error + pub error: EncryptionError, +} + +impl EncryptionErrorResponse { + /// Create new response + pub fn new(id: u32, code: i32, message: &str) -> Self { + EncryptionErrorResponse { + jsonrpc: "2.0".to_owned(), + id: id, + error: EncryptionError { + code: code, + message: message.to_owned(), + }, + } + } + + /// return serialized JSON self + pub fn as_json_value(&self) -> Value { + let res = serde_json::to_value(self).context(ErrorKind::APIEncryption( + "EncryptedResponse: JSON serialization failed".to_owned(), + )); + match res { + Ok(r) => r, + // proverbial "should never happen" + Err(r) => serde_json::json!({ + "json_rpc" : "2.0", + "id" : "1", + "error" : { + "message": format!("internal error serialising json error response {}", r), + "code": -32000 + } + } + ), + } + } +} + +#[test] +fn encrypted_request() -> Result<(), Error> { + use crate::util::{from_hex, static_secp_instance}; + + let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e"; + let shared_key = { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + + let sec_key_bytes = from_hex(sec_key_str.to_owned()).unwrap(); + SecretKey::from_slice(&secp, &sec_key_bytes)? + }; + let req = serde_json::json!({ + "jsonrpc": "2.0", + "method": "accounts", + "id": 1, + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000" + } + }); + let enc_req = EncryptedRequest::from_json(1, &req, &shared_key)?; + println!("{:?}", enc_req); + let dec_req = enc_req.decrypt(&shared_key)?; + println!("{:?}", dec_req); + assert_eq!(req, dec_req); + let enc_res = EncryptedResponse::from_json(1, &req, &shared_key)?; + println!("{:?}", enc_res); + println!("{:?}", enc_res.as_json_str()?); + let dec_res = enc_res.decrypt(&shared_key)?; + println!("{:?}", dec_res); + assert_eq!(req, dec_res); + Ok(()) +} diff --git a/controller/Cargo.toml b/controller/Cargo.toml index 6027ad959..82aeee645 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -29,7 +29,7 @@ tokio-retry = "0.1" uuid = { version = "0.7", features = ["serde", "v4"] } url = "1.7.0" chrono = { version = "0.4.4", features = ["serde"] } -easy-jsonrpc = "0.5.1" +easy-jsonrpc-mw = "0.5.3" lazy_static = "1" grin_wallet_util = { path = "../util", version = "2.1.0-beta.1" } diff --git a/controller/src/controller.rs b/controller/src/controller.rs index 3e7705ac2..8907f66a1 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -32,15 +32,22 @@ use serde_json; use std::net::SocketAddr; use std::sync::Arc; -use crate::apiwallet::{Foreign, ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, OwnerRpcS}; -use easy_jsonrpc; -use easy_jsonrpc::{Handler, MaybeReply}; +use crate::apiwallet::{ + EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign, + ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, OwnerRpcS, +}; +use easy_jsonrpc_mw; +use easy_jsonrpc_mw::{Handler, MaybeReply}; lazy_static! { pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue = HeaderValue::from_str("Basic realm=GrinOwnerAPI").unwrap(); } +lazy_static! { + pub static ref OWNER_API_SHARED_KEY: Arc>> = Arc::new(Mutex::new(None)); +} + fn check_middleware( name: ForeignCheckMiddlewareFn, node_version_info: Option, @@ -138,7 +145,6 @@ where } let api_handler_v2 = OwnerAPIHandlerV2::new(wallet.clone()); - let api_handler_v3 = OwnerAPIHandlerV3::new(wallet.clone()); router @@ -295,6 +301,106 @@ where pub wallet: Arc + 'static>>>, } +pub struct OwnerV3Helpers; + +impl OwnerV3Helpers { + /// Checks whether a request is to init the secure API + pub fn is_init_secure_api(val: &serde_json::Value) -> bool { + if let Some(m) = val["method"].as_str() { + match m { + "init_secure_api" => true, + _ => false, + } + } else { + false + } + } + + /// Checks whether a request is an encrypted request + pub fn is_encrypted_request(val: &serde_json::Value) -> bool { + if let Some(m) = val["method"].as_str() { + match m { + "encrypted_request_v3" => true, + _ => false, + } + } else { + false + } + } + + /// whether encryption is enabled + pub fn encryption_enabled() -> bool { + let share_key_ref = OWNER_API_SHARED_KEY.lock(); + share_key_ref.is_some() + } + + /// If incoming is an encrypted request, check there is a shared key, + /// Otherwise return an error value + pub fn check_encryption_started() -> Result<(), serde_json::Value> { + match OwnerV3Helpers::encryption_enabled() { + true => Ok(()), + false => Err(EncryptionErrorResponse::new( + 1, + -32001, + "Encryption must be enabled. Please call 'init_secure_api` first", + ) + .as_json_value()), + } + } + + /// Update the statically held owner API shared key + pub fn update_owner_api_shared_key(val: &serde_json::Value, new_key: Option) { + if let Some(_) = val["result"]["Ok"].as_str() { + let mut share_key_ref = OWNER_API_SHARED_KEY.lock(); + *share_key_ref = new_key; + } + } + + /// Decrypt an encrypted request + pub fn decrypt_request( + req: &serde_json::Value, + ) -> Result<(u32, serde_json::Value), serde_json::Value> { + let share_key_ref = OWNER_API_SHARED_KEY.lock(); + let shared_key = share_key_ref.as_ref().unwrap(); + let enc_req: EncryptedRequest = serde_json::from_value(req.clone()).map_err(|e| { + EncryptionErrorResponse::new( + 1, + -32002, + &format!("Encrypted request format error: {}", e), + ) + .as_json_value() + })?; + let id = enc_req.id; + let res = enc_req.decrypt(&shared_key).map_err(|e| { + EncryptionErrorResponse::new(1, -32002, &format!("Decryption error: {}", e.kind())) + .as_json_value() + })?; + Ok((id, res)) + } + + /// Encrypt a response + pub fn encrypt_response( + id: u32, + res: &serde_json::Value, + ) -> Result { + let share_key_ref = OWNER_API_SHARED_KEY.lock(); + let shared_key = share_key_ref.as_ref().unwrap(); + let enc_res = EncryptedResponse::from_json(id, res, &shared_key).map_err(|e| { + EncryptionErrorResponse::new(1, -32003, &format!("EncryptionError: {}", e.kind())) + .as_json_value() + })?; + let res = enc_res.as_json_value().map_err(|e| { + EncryptionErrorResponse::new( + 1, + -32002, + &format!("Encrypted response format error: {}", e), + ) + .as_json_value() + })?; + Ok(res) + } +} + impl OwnerAPIHandlerV3 where L: WalletLCProvider<'static, C, K>, @@ -314,9 +420,47 @@ where api: Owner<'static, L, C, K>, ) -> Box + Send> { Box::new(parse_body(req).and_then(move |val: serde_json::Value| { + let mut val = val; let owner_api_s = &api as &dyn OwnerRpcS; + let mut is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val); + let mut was_encrypted = false; + let mut encrypted_req_id = 0; + if !is_init_secure_api { + if let Err(v) = OwnerV3Helpers::check_encryption_started() { + return ok(v); + } + let res = OwnerV3Helpers::decrypt_request(&val); + match res { + Err(e) => return ok(e), + Ok(v) => { + encrypted_req_id = v.0; + val = v.1; + } + } + was_encrypted = true; + } + // check again, in case it was an encrypted call to init_secure_api + is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val); match owner_api_s.handle_request(val) { - MaybeReply::Reply(r) => ok(r), + MaybeReply::Reply(mut r) => { + let unencrypted_intercept = r.clone(); + if was_encrypted { + let res = OwnerV3Helpers::encrypt_response(encrypted_req_id, &r); + r = match res { + Ok(v) => v, + Err(v) => return ok(v), + } + } + // intercept init_secure_api response (after encryption, + // in case it was an encrypted call to 'init_api_secure') + if is_init_secure_api { + OwnerV3Helpers::update_owner_api_shared_key( + &unencrypted_intercept, + api.shared_key.lock().clone(), + ); + } + ok(r) + } MaybeReply::DontReply => { // Since it's http, we need to return something. We return [] because jsonrpc // clients will parse it as an empty batch response. diff --git a/controller/tests/accounts.rs b/controller/tests/accounts.rs index 06948bc56..8350a55b5 100644 --- a/controller/tests/accounts.rs +++ b/controller/tests/accounts.rs @@ -30,12 +30,10 @@ use std::time::Duration; #[macro_use] mod common; -use common::{create_wallet_proxy, setup}; +use common::{clean_output_dir, create_wallet_proxy, setup}; /// Various tests on accounts within the same wallet fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); - // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -265,7 +263,9 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { #[test] fn accounts() { let test_dir = "test_output/accounts"; + setup(test_dir); if let Err(e) = accounts_test_impl(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } diff --git a/controller/tests/check.rs b/controller/tests/check.rs index 27ea04af5..3c1ed920a 100644 --- a/controller/tests/check.rs +++ b/controller/tests/check.rs @@ -32,7 +32,7 @@ use util::ZeroingString; #[macro_use] mod common; -use common::{create_wallet_proxy, setup}; +use common::{clean_output_dir, create_wallet_proxy, setup}; macro_rules! send_to_dest { ($a:expr, $m: expr, $b:expr, $c:expr, $d:expr) => { @@ -48,8 +48,6 @@ macro_rules! wallet_info { /// Various tests on checking functionality fn check_repair_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); - // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -753,12 +751,15 @@ fn check_repair() { if let Err(e) = check_repair_impl(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } #[test] fn two_wallets_one_seed() { let test_dir = "test_output/two_wallets_one_seed"; + setup(test_dir); if let Err(e) = two_wallets_one_seed_impl(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } diff --git a/controller/tests/common/mod.rs b/controller/tests/common/mod.rs index c1ef72b35..596f63704 100644 --- a/controller/tests/common/mod.rs +++ b/controller/tests/common/mod.rs @@ -101,7 +101,7 @@ pub fn create_local_wallet( Arc< Mutex< Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, LocalWalletClient, @@ -114,7 +114,7 @@ pub fn create_local_wallet( ) { let mut wallet = Box::new(DefaultWalletImpl::::new(client).unwrap()) as Box< - WalletInst< + dyn WalletInst< DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, LocalWalletClient, ExtKeychain, @@ -130,6 +130,7 @@ pub fn create_local_wallet( (Arc::new(Mutex::new(wallet)), mask) } +#[allow(dead_code)] pub fn open_local_wallet( test_dir: &str, name: &str, @@ -139,7 +140,7 @@ pub fn open_local_wallet( Arc< Mutex< Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, LocalWalletClient, @@ -152,7 +153,7 @@ pub fn open_local_wallet( ) { let mut wallet = Box::new(DefaultWalletImpl::::new(client).unwrap()) as Box< - WalletInst< + dyn WalletInst< DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, LocalWalletClient, ExtKeychain, diff --git a/controller/tests/file.rs b/controller/tests/file.rs index fbdba439a..b538366d1 100644 --- a/controller/tests/file.rs +++ b/controller/tests/file.rs @@ -31,12 +31,10 @@ use serde_json; #[macro_use] mod common; -use common::{create_wallet_proxy, setup}; +use common::{clean_output_dir, create_wallet_proxy, setup}; /// self send impl fn file_exchange_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); - // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -224,7 +222,9 @@ fn file_exchange_test_impl(test_dir: &'static str) -> Result<(), libwallet::Erro #[test] fn wallet_file_exchange() { let test_dir = "test_output/file_exchange"; + setup(test_dir); if let Err(e) = file_exchange_test_impl(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs index d21f43a1d..ca52ea0ec 100644 --- a/controller/tests/invoice.rs +++ b/controller/tests/invoice.rs @@ -31,193 +31,186 @@ use common::{clean_output_dir, create_wallet_proxy, setup}; /// self send impl fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { - { - setup(test_dir); - - // Create a new proxy to simulate server and wallet responses - let mut wallet_proxy = create_wallet_proxy(test_dir); - let chain = wallet_proxy.chain.clone(); - - create_wallet_and_add!( - client1, - wallet1, - mask1_i, - test_dir, - "wallet1", - None, - &mut wallet_proxy, - true - ); - let mask1 = (&mask1_i).as_ref(); - create_wallet_and_add!( - client2, - wallet2, - mask2_i, - test_dir, - "wallet2", - None, - &mut wallet_proxy, - true - ); - let mask2 = (&mask2_i).as_ref(); - - // Set the wallet proxy listener running - thread::spawn(move || { - if let Err(e) = wallet_proxy.run() { - error!("Wallet Proxy error: {}", e); - } - }); - - // few values to keep things shorter - let reward = core::consensus::REWARD; - - // add some accounts - wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - api.create_account_path(m, "mining")?; - api.create_account_path(m, "listener")?; - Ok(()) - })?; - - // Get some mining done - { - wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + true + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); } - let mut bh = 10u64; - let _ = test_framework::award_blocks_to_wallet( - &chain, - wallet1.clone(), - mask1, - bh as usize, - false, - ); + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; - // Sanity check wallet 1 contents - wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; - assert!(wallet1_refreshed); - assert_eq!(wallet1_info.last_confirmed_height, bh); - assert_eq!(wallet1_info.total, bh * reward); - Ok(()) - })?; - - let mut slate = Slate::blank(2); - - wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { - // Wallet 2 inititates an invoice transaction, requesting payment - let args = IssueInvoiceTxArgs { - amount: reward * 2, - ..Default::default() - }; - slate = api.issue_invoice_tx(m, args)?; - Ok(()) - })?; - - wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - // Wallet 1 receives the invoice transaction - let args = InitTxArgs { - src_acct_name: None, - amount: slate.amount, - minimum_confirmations: 2, - max_outputs: 500, - num_change_outputs: 1, - selection_strategy_is_use_all: true, - ..Default::default() - }; - slate = api.process_invoice_tx(m, &slate, args)?; - api.tx_lock_outputs(m, &slate, 0)?; - Ok(()) - })?; - - // wallet 2 finalizes and posts - wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { - // Wallet 2 receives the invoice transaction - slate = api.finalize_invoice_tx(&slate)?; - Ok(()) - })?; - - // wallet 1 posts so wallet 2 doesn't get the mined amount - wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - api.post_tx(m, &slate.tx, false)?; - Ok(()) - })?; - bh += 1; - - let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); - bh += 3; - - // Check transaction log for wallet 2 - wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { - let (_, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; - assert!(refreshed); - assert!(txs.len() == 1); - println!( - "last confirmed height: {}, bh: {}", - wallet2_info.last_confirmed_height, bh - ); - assert!(refreshed); - assert_eq!(wallet2_info.amount_currently_spendable, slate.amount); - Ok(()) - })?; - - // Check transaction log for wallet 1, ensure only 1 entry - // exists - wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; - assert!(refreshed); - assert_eq!(txs.len() as u64, bh + 1); - println!( - "Wallet 1: last confirmed height: {}, bh: {}", - wallet1_info.last_confirmed_height, bh - ); - Ok(()) - })?; - - // Test self-sending - wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - // Wallet 1 inititates an invoice transaction, requesting payment - let args = IssueInvoiceTxArgs { - amount: reward * 2, - ..Default::default() - }; - slate = api.issue_invoice_tx(m, args)?; - // Wallet 1 receives the invoice transaction - let args = InitTxArgs { - src_acct_name: None, - amount: slate.amount, - minimum_confirmations: 2, - max_outputs: 500, - num_change_outputs: 1, - selection_strategy_is_use_all: true, - ..Default::default() - }; - slate = api.process_invoice_tx(m, &slate, args)?; - api.tx_lock_outputs(m, &slate, 0)?; - Ok(()) - })?; - - // wallet 1 finalizes and posts - wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { - // Wallet 2 receives the invoice transaction - slate = api.finalize_invoice_tx(&slate)?; - Ok(()) - })?; - - let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); - //bh += 3; - - // let logging finish - thread::sleep(Duration::from_millis(200)); + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; } + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Sanity check wallet 1 contents + wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + Ok(()) + })?; + + let mut slate = Slate::blank(2); + + wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { + // Wallet 2 inititates an invoice transaction, requesting payment + let args = IssueInvoiceTxArgs { + amount: reward * 2, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + // Wallet 1 receives the invoice transaction + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate, 0)?; + Ok(()) + })?; + + // wallet 2 finalizes and posts + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + // Wallet 2 receives the invoice transaction + slate = api.finalize_invoice_tx(&slate)?; + Ok(()) + })?; + + // wallet 1 posts so wallet 2 doesn't get the mined amount + wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + api.post_tx(m, &slate.tx, false)?; + Ok(()) + })?; + bh += 1; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { + let (_, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert!(refreshed); + assert!(txs.len() == 1); + println!( + "last confirmed height: {}, bh: {}", + wallet2_info.last_confirmed_height, bh + ); + assert!(refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, slate.amount); + Ok(()) + })?; + + // Check transaction log for wallet 1, ensure only 1 entry + // exists + wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert!(refreshed); + assert_eq!(txs.len() as u64, bh + 1); + println!( + "Wallet 1: last confirmed height: {}, bh: {}", + wallet1_info.last_confirmed_height, bh + ); + Ok(()) + })?; + + // Test self-sending + wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + // Wallet 1 inititates an invoice transaction, requesting payment + let args = IssueInvoiceTxArgs { + amount: reward * 2, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + // Wallet 1 receives the invoice transaction + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate, 0)?; + Ok(()) + })?; + + // wallet 1 finalizes and posts + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + // Wallet 2 receives the invoice transaction + slate = api.finalize_invoice_tx(&slate)?; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + //bh += 3; + + // let logging finish + thread::sleep(Duration::from_millis(200)); - clean_output_dir(test_dir); Ok(()) } #[test] fn wallet_invoice_tx() -> Result<(), libwallet::Error> { let test_dir = "test_output/invoice_tx"; - invoice_tx_impl(test_dir) + setup(test_dir); + invoice_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) } diff --git a/controller/tests/repost.rs b/controller/tests/repost.rs index d65cfba64..52ad06e21 100644 --- a/controller/tests/repost.rs +++ b/controller/tests/repost.rs @@ -28,12 +28,10 @@ use std::time::Duration; #[macro_use] mod common; -use common::{create_wallet_proxy, setup}; +use common::{clean_output_dir, create_wallet_proxy, setup}; /// self send impl fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); - // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -257,7 +255,9 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> #[test] fn wallet_file_repost() { let test_dir = "test_output/file_repost"; + setup(test_dir); if let Err(e) = file_repost_test_impl(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } diff --git a/controller/tests/restore.rs b/controller/tests/restore.rs index 2bdb77c1d..5667bfe34 100644 --- a/controller/tests/restore.rs +++ b/controller/tests/restore.rs @@ -30,7 +30,7 @@ use std::time::Duration; #[macro_use] mod common; -use common::{create_wallet_proxy, setup}; +use common::{clean_output_dir, create_wallet_proxy, setup}; fn restore_wallet(base_dir: &'static str, wallet_dir: &str) -> Result<(), libwallet::Error> { let source_seed = format!("{}/{}/wallet_data/wallet.seed", base_dir, wallet_dir); @@ -196,8 +196,6 @@ fn compare_wallet_restore( /// Build up 2 wallets, perform a few transactions on them /// Then attempt to restore them in separate directories and check contents are the same fn setup_restore(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); - // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -417,6 +415,7 @@ fn perform_restore(test_dir: &'static str) -> Result<(), libwallet::Error> { #[test] fn wallet_restore() { let test_dir = "test_output/wallet_restore"; + setup(test_dir); if let Err(e) = setup_restore(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } @@ -425,4 +424,5 @@ fn wallet_restore() { } // let logging finish thread::sleep(Duration::from_millis(200)); + clean_output_dir(test_dir); } diff --git a/controller/tests/self_send.rs b/controller/tests/self_send.rs index 11c885fc7..3ccca6849 100644 --- a/controller/tests/self_send.rs +++ b/controller/tests/self_send.rs @@ -27,11 +27,10 @@ use std::time::Duration; #[macro_use] mod common; -use common::{create_wallet_proxy, setup}; +use common::{clean_output_dir, create_wallet_proxy, setup}; /// self send impl fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -138,7 +137,9 @@ fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { #[test] fn wallet_self_send() { let test_dir = "test_output/self_send"; + setup(test_dir); if let Err(e) = self_send_test_impl(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } diff --git a/controller/tests/transaction.rs b/controller/tests/transaction.rs index 99ef809b6..22731c0de 100644 --- a/controller/tests/transaction.rs +++ b/controller/tests/transaction.rs @@ -28,13 +28,12 @@ use std::thread; use std::time::Duration; mod common; -use common::{create_wallet_proxy, setup}; +use common::{clean_output_dir, create_wallet_proxy, setup}; /// Exercises the Transaction API fully with a test NodeClient operating /// directly on a chain instance /// Callable with any type of wallet fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -350,7 +349,6 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> /// Test rolling back transactions and outputs when a transaction is never /// posted to a chain fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { - setup(test_dir); // Create a new proxy to simulate server and wallet responses let mut wallet_proxy = create_wallet_proxy(test_dir); let chain = wallet_proxy.chain.clone(); @@ -528,15 +526,19 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { #[test] fn db_wallet_basic_transaction_api() { let test_dir = "test_output/basic_transaction_api"; + setup(test_dir); if let Err(e) = basic_transaction_api(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } #[test] fn db_wallet_tx_rollback() { let test_dir = "test_output/tx_rollback"; + setup(test_dir); if let Err(e) = tx_rollback(test_dir) { panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); } + clean_output_dir(test_dir); } diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/http.rs index cc3ce942d..e57daa040 100644 --- a/impls/src/adapters/http.rs +++ b/impls/src/adapters/http.rs @@ -36,7 +36,7 @@ impl HttpSlateSender { } /// Check version of the listening wallet - fn check_other_version(&self) -> Result<(), Error> { + fn check_other_version(&self, url: &Url) -> Result<(), Error> { let req = json!({ "jsonrpc": "2.0", "method": "check_version", @@ -44,7 +44,7 @@ impl HttpSlateSender { "params": [] }); - let res: String = post(&self.base_url, None, &req).map_err(|e| { + let res: String = post(url, None, &req).map_err(|e| { let mut report = format!("Performing version check (is recipient listening?): {}", e); let err_string = format!("{}", e); if err_string.contains("404") { @@ -101,7 +101,7 @@ impl SlateSender for HttpSlateSender { .expect("/v2/foreign is an invalid url path"); debug!("Posting transaction slate to {}", url); - self.check_other_version()?; + self.check_other_version(&url)?; // Note: not using easy-jsonrpc as don't want the dependencies in this crate let req = json!({ diff --git a/impls/src/adapters/keybase.rs b/impls/src/adapters/keybase.rs index ce22847da..d0df52280 100644 --- a/impls/src/adapters/keybase.rs +++ b/impls/src/adapters/keybase.rs @@ -359,7 +359,7 @@ impl SlateReceiver for KeybaseAllChannels { DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(), ) as Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider, HTTPNodeClient, diff --git a/impls/src/lifecycle/default.rs b/impls/src/lifecycle/default.rs index e73fa7d08..1627091e2 100644 --- a/impls/src/lifecycle/default.rs +++ b/impls/src/lifecycle/default.rs @@ -124,6 +124,7 @@ where match LMDBBackend::new(&data_dir_name, self.node_client.clone()) { Err(e) => { let msg = format!("Error creating wallet: {}, Data Dir: {}", e, &data_dir_name); + error!("{}", msg); return Err(ErrorKind::Lifecycle(msg).into()); } Ok(d) => d, diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index 2cabe1f61..ac13e5e18 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -117,6 +117,10 @@ pub enum ErrorKind { #[fail(display = "Signature error: {}", _0)] Signature(String), + /// OwnerAPIEncryption + #[fail(display = "{}", _0)] + APIEncryption(String), + /// Attempt to use duplicate transaction id in separate transactions #[fail(display = "Duplicate transaction ID error")] DuplicateTransactionId, diff --git a/src/bin/grin-wallet.rs b/src/bin/grin-wallet.rs index 531833b29..8c3d607f0 100644 --- a/src/bin/grin-wallet.rs +++ b/src/bin/grin-wallet.rs @@ -99,7 +99,7 @@ fn real_main() -> i32 { panic!("Error loading wallet configuration: {}", e); }); - config.members.as_mut().unwrap().wallet.chain_type = Some(chain_type); + //config.members.as_mut().unwrap().wallet.chain_type = Some(chain_type); // Load logging config let l = config.members.as_mut().unwrap().logging.clone().unwrap(); diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index a5017f425..2eb7e106d 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -70,6 +70,12 @@ subcommands: takes_value: true - owner_api: about: Runs the wallet's local web API + args: + - port: + help: Port on which to run the wallet owner listener + short: l + long: port + takes_value: true - send: about: Builds a transaction to send coins and sends to the specified listener directly args: diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 35083a225..877b90e87 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -218,7 +218,7 @@ fn prompt_pay_invoice(slate: &Slate, method: &str, dest: &str) -> Result( config: WalletConfig, node_client: C, -) -> Result>>>, ParseError> +) -> Result>>>, ParseError> where DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K>, @@ -226,7 +226,7 @@ where K: keychain::Keychain + 'static, { let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client.clone()).unwrap()) - as Box>; + as Box>; let lc = wallet.lc_provider().unwrap(); lc.set_wallet_directory(&config.data_file_dir); Ok(Arc::new(Mutex::new(wallet))) @@ -396,6 +396,16 @@ pub fn parse_listen_args( }) } +pub fn parse_owner_api_args( + config: &mut WalletConfig, + args: &ArgMatches, +) -> Result<(), ParseError> { + if let Some(port) = args.value_of("port") { + config.owner_api_listen_port = Some(port.parse().unwrap()); + } + Ok(()) +} + pub fn parse_account_args(account_args: &ArgMatches) -> Result { let create = match account_args.value_of("create") { None => None, @@ -811,6 +821,9 @@ where let keychain_mask = match wallet_args.subcommand() { ("init", Some(_)) => None, ("recover", _) => None, + // Owner API can be started without a wallet present + // TODO: Not quite yet, next PR will deal with this + //("owner_api", _) => None, _ => { let mut wallet_lock = wallet.lock(); let lc = wallet_lock.lc_provider().unwrap(); @@ -853,11 +866,12 @@ where let a = arg_parse!(parse_listen_args(&mut c, &args)); command::listen(wallet, keychain_mask, &c, &a, &global_wallet_args.clone()) } - ("owner_api", Some(_)) => { + ("owner_api", Some(args)) => { + let mut c = wallet_config.clone(); let mut g = global_wallet_args.clone(); g.tls_conf = None; - print!("mask: {:?}", keychain_mask); - command::owner_api(wallet, keychain_mask, &wallet_config, &g) + arg_parse!(parse_owner_api_args(&mut c, &args)); + command::owner_api(wallet, keychain_mask, &c, &g) } ("web", Some(_)) => { command::owner_api(wallet, keychain_mask, &wallet_config, &global_wallet_args) diff --git a/tests/cmd_line_basic.rs b/tests/cmd_line_basic.rs index b9a4f39c5..08b4c2ac9 100644 --- a/tests/cmd_line_basic.rs +++ b/tests/cmd_line_basic.rs @@ -30,7 +30,7 @@ use grin_wallet_impls::DefaultLCProvider; use grin_wallet_util::grin_keychain::ExtKeychain; mod common; -use common::{execute_command, initial_setup_wallet, instantiate_wallet, setup}; +use common::{clean_output_dir, execute_command, initial_setup_wallet, instantiate_wallet, setup}; /// command line tests fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::Error> { @@ -482,6 +482,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // let logging finish thread::sleep(Duration::from_millis(200)); + clean_output_dir(test_dir); Ok(()) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index fa20960a3..2830143c4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -14,6 +14,7 @@ //! Common functions for wallet integration tests extern crate grin_wallet; +use grin_wallet_config as config; use grin_wallet_impls::test_framework::LocalWalletClient; use grin_wallet_util::grin_util as util; @@ -23,12 +24,14 @@ use std::sync::Arc; use std::{env, fs}; use util::{Mutex, ZeroingString}; +use grin_wallet_api::{EncryptedRequest, EncryptedResponse}; use grin_wallet_config::{GlobalWalletConfig, WalletConfig, GRIN_WALLET_DIR}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; use grin_wallet_libwallet::{WalletInfo, WalletInst}; use grin_wallet_util::grin_core::global::{self, ChainTypes}; use grin_wallet_util::grin_keychain::ExtKeychain; -use util::secp::key::SecretKey; +use grin_wallet_util::grin_util::{from_hex, static_secp_instance}; +use util::secp::key::{PublicKey, SecretKey}; use grin_wallet::cmd::wallet_args; use grin_wallet_util::grin_api as api; @@ -100,7 +103,7 @@ macro_rules! setup_proxy { }; } -fn clean_output_dir(test_dir: &str) { +pub fn clean_output_dir(test_dir: &str) { let _ = fs::remove_dir_all(test_dir); } @@ -192,7 +195,7 @@ pub fn instantiate_wallet( Arc< Mutex< Box< - WalletInst< + dyn WalletInst< 'static, DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, LocalWalletClient, @@ -208,7 +211,7 @@ pub fn instantiate_wallet( wallet_config.chain_type = None; let mut wallet = Box::new(DefaultWalletImpl::::new(node_client).unwrap()) as Box< - WalletInst< + dyn WalletInst< DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, LocalWalletClient, ExtKeychain, @@ -247,6 +250,25 @@ pub fn execute_command( wallet_args::wallet_command(&args, config.clone(), client.clone(), true) } +// as above, but without necessarily setting up the wallet +#[allow(dead_code)] +pub fn execute_command_no_setup( + app: &App, + test_dir: &str, + wallet_name: &str, + client: &LocalWalletClient, + arg_vec: Vec<&str>, +) -> Result { + let args = app.clone().get_matches_from(arg_vec); + let _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); + let config = config::initial_setup_wallet(&ChainTypes::AutomatedTesting, None).unwrap(); + let mut wallet_config = config.members.unwrap().wallet.clone(); + wallet_config.chain_type = None; + wallet_config.api_secret_path = None; + wallet_config.node_api_secret_path = None; + wallet_args::wallet_command(&args, wallet_config, client.clone(), true) +} + pub fn post(url: &Url, api_secret: Option, input: &IN) -> Result where IN: Serialize, @@ -257,6 +279,7 @@ where Ok(res) } +#[allow(dead_code)] pub fn send_request( id: u64, dest: &str, @@ -266,19 +289,30 @@ where OUT: DeserializeOwned, { let url = Url::parse(dest).unwrap(); - let req: Value = serde_json::from_str(req).unwrap(); - let res: String = post(&url, None, &req).map_err(|e| { + let req_val: Value = serde_json::from_str(req).unwrap(); + let res = post(&url, None, &req_val).map_err(|e| { let err_string = format!("{}", e); println!("{}", err_string); thread::sleep(Duration::from_millis(200)); e })?; + + let res_val: Value = serde_json::from_str(&res).unwrap(); + // encryption error, just return the string + if res_val["error"] != json!(null) { + return Ok(Err(WalletAPIReturnError { + message: res_val["error"]["message"].as_str().unwrap().to_owned(), + code: res_val["error"]["code"].as_i64().unwrap() as i32, + })); + } + let res = serde_json::from_str(&res).unwrap(); let res = easy_jsonrpc::Response::from_json_response(res).unwrap(); let res = res.outputs.get(&id).unwrap().clone().unwrap(); if res["Err"] != json!(null) { Ok(Err(WalletAPIReturnError { message: res["Err"].as_str().unwrap().to_owned(), + code: res["error"]["code"].as_i64().unwrap() as i32, })) } else { // deserialize result into expected type @@ -287,10 +321,88 @@ where } } +#[allow(dead_code)] +pub fn send_request_enc( + sec_req_id: u32, + internal_request_id: u32, + dest: &str, + req: &str, + shared_key: &SecretKey, +) -> Result, api::Error> +where + OUT: DeserializeOwned, +{ + let url = Url::parse(dest).unwrap(); + let req_val: Value = serde_json::from_str(req).unwrap(); + let req = EncryptedRequest::from_json(sec_req_id, &req_val, &shared_key).unwrap(); + let res = post(&url, None, &req).map_err(|e| { + let err_string = format!("{}", e); + println!("{}", err_string); + thread::sleep(Duration::from_millis(200)); + e + })?; + + let res_val: Value = serde_json::from_str(&res).unwrap(); + // encryption error, just return the string + if res_val["error"] != json!(null) { + return Ok(Err(WalletAPIReturnError { + message: res_val["error"]["message"].as_str().unwrap().to_owned(), + code: res_val["error"]["code"].as_i64().unwrap() as i32, + })); + } + + let enc_resp: EncryptedResponse = serde_json::from_str(&res).unwrap(); + let res = enc_resp.decrypt(shared_key).unwrap(); + if res["error"] != json!(null) { + return Ok(Err(WalletAPIReturnError { + message: res["error"]["message"].as_str().unwrap().to_owned(), + code: res["error"]["code"].as_i64().unwrap() as i32, + })); + } + let res = easy_jsonrpc::Response::from_json_response(res).unwrap(); + let res = res + .outputs + .get(&(internal_request_id as u64)) + .unwrap() + .clone() + .unwrap(); + + if res["Err"] != json!(null) { + Ok(Err(WalletAPIReturnError { + message: res["Err"].as_str().unwrap().to_owned(), + code: res_val["error"]["code"].as_i64().unwrap() as i32, + })) + } else { + // deserialize result into expected type + let value: OUT = serde_json::from_value(res["Ok"].clone()).unwrap(); + Ok(Ok(value)) + } +} + +#[allow(dead_code)] +pub fn derive_ecdh_key(sec_key_str: &str, other_pubkey: &PublicKey) -> SecretKey { + let sec_key_bytes = from_hex(sec_key_str.to_owned()).unwrap(); + let sec_key = { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + SecretKey::from_slice(&secp, &sec_key_bytes).unwrap() + }; + + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + + let mut shared_pubkey = other_pubkey.clone(); + shared_pubkey.mul_assign(&secp, &sec_key).unwrap(); + + let x_coord = shared_pubkey.serialize_vec(&secp, true); + SecretKey::from_slice(&secp, &x_coord[1..]).unwrap() +} + // Types to make working with json responses easier #[derive(Clone, Debug, Serialize, Deserialize)] pub struct WalletAPIReturnError { - message: String, + pub message: String, + pub code: i32, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/tests/data/v2_reqs/retrieve_info.req.json b/tests/data/v2_reqs/retrieve_info.req.json new file mode 100644 index 000000000..7bc9be8d0 --- /dev/null +++ b/tests/data/v2_reqs/retrieve_info.req.json @@ -0,0 +1,9 @@ +{ + "jsonrpc": "2.0", + "method": "retrieve_summary_info", + "params": [ + true, + 1 + ], + "id": 1 +} diff --git a/tests/data/v3_reqs/init_secure_api.req.json b/tests/data/v3_reqs/init_secure_api.req.json new file mode 100644 index 000000000..11d9e8e13 --- /dev/null +++ b/tests/data/v3_reqs/init_secure_api.req.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "method": "init_secure_api", + "params": { + "ecdh_pubkey": "03b3c18c9a38783d105e238953b1638b021ba7456d87a5c085b3bdb75777b4c490" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/open_wallet.req.json b/tests/data/v3_reqs/open_wallet.req.json index e69de29bb..9e5d096c4 100644 --- a/tests/data/v3_reqs/open_wallet.req.json +++ b/tests/data/v3_reqs/open_wallet.req.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "method": "open_wallet", + "params": { + "token": null, + "refresh_from_node": true, + "minimum_confirmations": 1 + }, + "id": 1 +} diff --git a/tests/owner_v3.rs b/tests/owner_v2_sanity.rs similarity index 67% rename from tests/owner_v3.rs rename to tests/owner_v2_sanity.rs index f5f1db84a..3ad2334e8 100644 --- a/tests/owner_v3.rs +++ b/tests/owner_v2_sanity.rs @@ -31,11 +31,14 @@ use grin_wallet_util::grin_keychain::ExtKeychain; #[macro_use] mod common; use common::RetrieveSummaryInfoResp; -use common::{execute_command, initial_setup_wallet, instantiate_wallet, send_request, setup}; +use common::{ + clean_output_dir, execute_command, initial_setup_wallet, instantiate_wallet, send_request, + setup, +}; #[test] -fn owner_v3() -> Result<(), grin_wallet_controller::Error> { - let test_dir = "target/test_output/owner_v3"; +fn owner_v2_sanity() -> Result<(), grin_wallet_controller::Error> { + let test_dir = "target/test_output/owner_v2_sanity"; setup(test_dir); setup_proxy!(test_dir, chain, wallet1, client1, mask1, wallet2, client2, _mask2); @@ -44,6 +47,7 @@ fn owner_v3() -> Result<(), grin_wallet_controller::Error> { let bh = 10u64; let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + let client1_2 = client1.clone(); // run the owner listener on wallet 1 let arg_vec = vec!["grin-wallet", "-p", "password", "owner_api"]; @@ -55,7 +59,7 @@ fn owner_v3() -> Result<(), grin_wallet_controller::Error> { }); // run the foreign listener for wallet 2 - let arg_vec = vec!["grin-wallet", "-p", "password", "listen"]; + let arg_vec = vec!["grin-wallet", "-p", "password", "listen", "-l", "23415"]; // Set owner listener running thread::spawn(move || { let yml = load_yaml!("../src/bin/grin-wallet.yml"); @@ -65,12 +69,30 @@ fn owner_v3() -> Result<(), grin_wallet_controller::Error> { thread::sleep(Duration::from_millis(200)); - // Send simple retrieve_info request to owner listener - let req = include_str!("data/v3_reqs/retrieve_info.req.json"); - let res = send_request(1, "http://127.0.0.1:3420/v3/owner", req)?; + // 1) Send simple retrieve_info request to owner listener + let req = include_str!("data/v2_reqs/retrieve_info.req.json"); + let res = send_request(1, "http://127.0.0.1:3420/v2/owner", req)?; assert!(res.is_ok()); let value: RetrieveSummaryInfoResp = res.unwrap(); assert_eq!(value.1.amount_currently_spendable, 420000000000); - println!("Response: {:?}", value); + println!("Response 1: {:?}", value); + + // 2) Send to wallet 2 foreign listener + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "send", + "-d", + "http://127.0.0.1:23415", + "10", + ]; + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + let res = execute_command(&app, test_dir, "wallet1", &client1_2, arg_vec.clone()); + println!("Response 2: {:?}", res); + assert!(res.is_ok()); + + clean_output_dir(test_dir); Ok(()) } diff --git a/tests/owner_v3_init_secure.rs b/tests/owner_v3_init_secure.rs new file mode 100644 index 000000000..4693c1343 --- /dev/null +++ b/tests/owner_v3_init_secure.rs @@ -0,0 +1,219 @@ +// Copyright 2019 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_api::ECDHPubkey; +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_wallet_impls::DefaultLCProvider; +use grin_wallet_util::grin_keychain::ExtKeychain; +use grin_wallet_util::grin_util::secp::key::SecretKey; +use grin_wallet_util::grin_util::{from_hex, static_secp_instance}; +use serde_json; + +#[macro_use] +mod common; +use common::{ + clean_output_dir, derive_ecdh_key, execute_command, initial_setup_wallet, instantiate_wallet, + send_request, send_request_enc, setup, RetrieveSummaryInfoResp, +}; + +#[test] +fn owner_v3_init_secure() -> Result<(), grin_wallet_controller::Error> { + let test_dir = "target/test_output/owner_v3_init_secure"; + setup(test_dir); + + // Create a new proxy to simulate server and wallet responses + setup_proxy!(test_dir, chain, wallet1, client1, mask1, wallet2, client2, _mask2); + + // add some blocks manually + let bh = 2u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // run a wallet owner listener + let arg_vec = vec!["grin-wallet", "-p", "password", "owner_api", "-l", "33420"]; + thread::spawn(move || { + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).unwrap(); + }); + thread::sleep(Duration::from_millis(200)); + + // use in all tests + let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e"; + let _pub_key_str = "03b3c18c9a38783d105e238953b1638b021ba7456d87a5c085b3bdb75777b4c490"; + + let sec_key_bytes = from_hex(sec_key_str.to_owned()).unwrap(); + let sec_key = { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + SecretKey::from_slice(&secp, &sec_key_bytes).unwrap() + }; + + // 1) Attempt to send an encrypted request before calling `init_secure_api` + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::(1, 1, "http://127.0.0.1:33420/v3/owner", &req, &sec_key)?; + println!("RES 1: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32001); + + // 2) Call any function on the V3 api without calling 'init_secure_api` first + let res = send_request::(1, "http://127.0.0.1:33420/v3/owner", &req)?; + println!("RES 2: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32001); + + // 3) Call 'init_secure_api' and negotiate shared key + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request(1, "http://127.0.0.1:33420/v3/owner", req)?; + println!("RES 3: {:?}", res); + + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 4) A normal request, correct key + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 4: {:?}", res); + assert!(res.is_ok()); + + // 5) A normal request, incorrect key + let mut bad_key = shared_key.clone(); + bad_key.0[0] = 0; + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &bad_key, + )?; + println!("RES 5: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32002); + + // 6) A malformed encrypted json request (missing nonce) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "encrypted_request_v3", + "params": { + "body_enc:": "thisiswrong", + } + }); + let res = send_request::(1, "http://127.0.0.1:33420/v3/owner", &req.to_string())?; + println!("RES 6: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32002); + + // 7) A malformed encrypted json request (garbage encrypted content) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "encrypted_request_v3", + "params": { + "nonce": "32", + "body_enc": "thisiswrong", + } + }); + let res = send_request::(1, "http://127.0.0.1:33420/v3/owner", &req.to_string())?; + println!("RES 7: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32002); + + // 8) Encrypted call to `init_secure_api`, followed by re-deriving key + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request_enc( + 1, + 1, + "http://127.0.0.1:33420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 8: {:?}", res); + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 9) A normal request, with new correct key + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + 9, + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 9: {:?}", res); + assert!(res.is_ok()); + + // 10) Call 'init_secure_api' unencrypted (which we can do) and negotiate new shared key + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request(1, "http://127.0.0.1:33420/v3/owner", req)?; + println!("RES 10: {:?}", res); + + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 11) A normal request, correct key + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + 11, + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 11: {:?}", res); + assert!(res.is_ok()); + + // 12) A request which triggers and API error (not an encryption error) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "method_dun_exist", + "params": { + "nope": "nope", + } + }) + .to_string(); + let res = + send_request_enc::(12, 1, "http://127.0.0.1:33420/v3/owner", &req, &shared_key)?; + println!("RES 12: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32601); + + clean_output_dir(test_dir); + + Ok(()) +}