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
This commit is contained in:
Yeastplume 2019-08-19 13:05:21 +01:00 committed by GitHub
parent 62d976f9ef
commit a58cae651e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1224 additions and 294 deletions

32
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -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<Option<serde_json::Value>, 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::<LocalWalletClient>::new(client1.clone()).unwrap())
as Box<
WalletInst<
dyn WalletInst<
'static,
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
LocalWalletClient,
@ -648,7 +648,7 @@ pub fn run_doctest_foreign(
let mut wallet2 =
Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client2.clone()).unwrap())
as Box<
WalletInst<
dyn WalletInst<
'static,
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
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)]

View file

@ -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<SecretKey>,
}
pub use types::{ECDHPubkey, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Token};

View file

@ -54,6 +54,8 @@ where
pub wallet_inst: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
/// Flag to normalize some output during testing. Can mostly be ignored.
pub doctest_mode: bool,
/// Share ECDH key
pub shared_key: Arc<Mutex<Option<SecretKey>>>,
}
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)),
}
}

View file

@ -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<Option<serde_json::Value>, 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::<LocalWalletClient>::new(client1.clone()).unwrap())
as Box<
WalletInst<
dyn WalletInst<
'static,
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
LocalWalletClient,
@ -1439,7 +1439,7 @@ pub fn run_doctest_owner(
let mut wallet2 =
Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client2.clone()).unwrap())
as Box<
WalletInst<
dyn WalletInst<
'static,
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
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())
// 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();
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()
);
}
}
};
}

View file

@ -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<NodeHeightResult, ErrorKind>;
/**
Initializes the secure RPC-JSON API
(Documentation TBD)
*/
fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result<ECDHPubkey, ErrorKind>;
}
impl<'a, L, C, K> OwnerRpcS for Owner<'a, L, C, K>
@ -1471,4 +1481,30 @@ where
fn node_height(&self, token: Token) -> Result<NodeHeightResult, ErrorKind> {
Owner::node_height(self, (&token.keychain_mask).as_ref()).map_err(|e| e.kind())
}
fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result<ECDHPubkey, ErrorKind> {
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,
})
}
}

308
api/src/types.rs Normal file
View file

@ -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<SecretKey>,
}
/// 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<Self, Error> {
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<Value, Error> {
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<String, Error> {
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<Value, Error> {
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<Self, Error> {
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<Value, Error> {
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<String, Error> {
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<Value, Error> {
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<String, EncryptedBody>,
}
impl EncryptedResponse {
/// from json
pub fn from_json(id: u32, json_in: &Value, enc_key: &SecretKey) -> Result<Self, Error> {
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<Value, Error> {
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<String, Error> {
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<Value, Error> {
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(())
}

View file

@ -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" }

View file

@ -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<Mutex<Option<SecretKey>>> = Arc::new(Mutex::new(None));
}
fn check_middleware(
name: ForeignCheckMiddlewareFn,
node_version_info: Option<NodeVersionInfo>,
@ -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<Mutex<Box<dyn WalletInst<'static, L, C, K> + '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<SecretKey>) {
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<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_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<L, C, K> OwnerAPIHandlerV3<L, C, K>
where
L: WalletLCProvider<'static, C, K>,
@ -314,9 +420,47 @@ where
api: Owner<'static, L, C, K>,
) -> Box<dyn Future<Item = serde_json::Value, Error = Error> + 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.

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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::<LocalWalletClient>::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::<LocalWalletClient>::new(client).unwrap())
as Box<
WalletInst<
dyn WalletInst<
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
LocalWalletClient,
ExtKeychain,

View file

@ -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);
}

View file

@ -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 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();
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")?;
// 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,
);
});
// 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(())
})?;
// few values to keep things shorter
let reward = core::consensus::REWARD;
let mut slate = Slate::blank(2);
// 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(())
})?;
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(())
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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!({

View file

@ -359,7 +359,7 @@ impl SlateReceiver for KeybaseAllChannels {
DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(),
)
as Box<
WalletInst<
dyn WalletInst<
'static,
DefaultLCProvider<HTTPNodeClient, ExtKeychain>,
HTTPNodeClient,

View file

@ -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,

View file

@ -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,

View file

@ -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();

View file

@ -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:

View file

@ -218,7 +218,7 @@ fn prompt_pay_invoice(slate: &Slate, method: &str, dest: &str) -> Result<bool, P
pub fn inst_wallet<L, C, K>(
config: WalletConfig,
node_client: C,
) -> Result<Arc<Mutex<Box<WalletInst<'static, L, C, K>>>>, ParseError>
) -> Result<Arc<Mutex<Box<dyn WalletInst<'static, L, C, K>>>>, 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<WalletInst<'static, L, C, K>>;
as Box<dyn WalletInst<'static, L, C, K>>;
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<command::AccountArgs, ParseError> {
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)

View file

@ -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(())
}

View file

@ -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::<LocalWalletClient>::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<String, grin_wallet_controller::Error> {
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<IN>(url: &Url, api_secret: Option<String>, input: &IN) -> Result<String, api::Error>
where
IN: Serialize,
@ -257,6 +279,7 @@ where
Ok(res)
}
#[allow(dead_code)]
pub fn send_request<OUT>(
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<OUT>(
sec_req_id: u32,
internal_request_id: u32,
dest: &str,
req: &str,
shared_key: &SecretKey,
) -> Result<Result<OUT, WalletAPIReturnError>, 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)]

View file

@ -0,0 +1,9 @@
{
"jsonrpc": "2.0",
"method": "retrieve_summary_info",
"params": [
true,
1
],
"id": 1
}

View file

@ -0,0 +1,8 @@
{
"jsonrpc": "2.0",
"method": "init_secure_api",
"params": {
"ecdh_pubkey": "03b3c18c9a38783d105e238953b1638b021ba7456d87a5c085b3bdb75777b4c490"
},
"id": 1
}

View file

@ -0,0 +1,10 @@
{
"jsonrpc": "2.0",
"method": "open_wallet",
"params": {
"token": null,
"refresh_from_node": true,
"minimum_confirmations": 1
},
"id": 1
}

View file

@ -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(())
}

View file

@ -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::<String>(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::<String>(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::<RetrieveSummaryInfoResp>(
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::<RetrieveSummaryInfoResp>(
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::<String>(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::<String>(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::<RetrieveSummaryInfoResp>(
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::<RetrieveSummaryInfoResp>(
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::<String>(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(())
}