From b9665c7cfee0c8c327f3ab140edd4eda8e5daa36 Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky Date: Mon, 20 Feb 2017 18:13:21 +0300 Subject: [PATCH] Secret store - initial version (#4567) * initial secret store commit * various fixes * license * (sstore, secstore) -> secretstore * marked KeyServer trait as IPC-ready * fixed style * ignore requests with Origin header * fixed tests * fixed Origin header check --- Cargo.lock | 18 +++ Cargo.toml | 2 + parity/blockchain.rs | 4 +- parity/cli/config.full.toml | 6 + parity/cli/config.toml | 3 + parity/cli/mod.rs | 33 +++++- parity/cli/usage.txt | 10 ++ parity/configuration.rs | 32 +++++- parity/dir.rs | 8 +- parity/main.rs | 5 + parity/run.rs | 10 +- parity/secretstore.rs | 105 ++++++++++++++++++ secret_store/Cargo.toml | 25 +++++ secret_store/build.rs | 22 ++++ secret_store/src/acl_storage.rs | 51 +++++++++ secret_store/src/http_listener.rs | 176 ++++++++++++++++++++++++++++++ secret_store/src/key_server.rs | 124 +++++++++++++++++++++ secret_store/src/key_storage.rs | 115 +++++++++++++++++++ secret_store/src/lib.rs | 52 +++++++++ secret_store/src/traits.rs | 24 ++++ secret_store/src/types/all.rs | 77 +++++++++++++ secret_store/src/types/mod.rs | 20 ++++ secret_store/src/types/mod.rs.in | 17 +++ 23 files changed, 931 insertions(+), 8 deletions(-) create mode 100644 parity/secretstore.rs create mode 100644 secret_store/Cargo.toml create mode 100644 secret_store/build.rs create mode 100644 secret_store/src/acl_storage.rs create mode 100644 secret_store/src/http_listener.rs create mode 100644 secret_store/src/key_server.rs create mode 100644 secret_store/src/key_storage.rs create mode 100644 secret_store/src/lib.rs create mode 100644 secret_store/src/traits.rs create mode 100644 secret_store/src/types/all.rs create mode 100644 secret_store/src/types/mod.rs create mode 100644 secret_store/src/types/mod.rs.in diff --git a/Cargo.lock b/Cargo.lock index 9ededba65..44989c92c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,7 @@ dependencies = [ "ethcore-light 1.6.0", "ethcore-logger 1.6.0", "ethcore-rpc 1.6.0", + "ethcore-secretstore 1.0.0", "ethcore-signer 1.6.0", "ethcore-stratum 1.6.0", "ethcore-util 1.6.0", @@ -635,6 +636,23 @@ dependencies = [ "transient-hashmap 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ethcore-secretstore" +version = "1.0.0" +dependencies = [ + "ethcore-devtools 1.6.0", + "ethcore-ipc 1.6.0", + "ethcore-ipc-codegen 1.6.0", + "ethcore-ipc-nano 1.6.0", + "ethcore-util 1.6.0", + "ethcrypto 0.1.0", + "ethkey 0.2.0", + "hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ethcore-signer" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 45179ef10..3b6bd7e97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ parity-updater = { path = "updater" } parity-reactor = { path = "util/reactor" } ethcore-dapps = { path = "dapps", optional = true } clippy = { version = "0.0.103", optional = true} +ethcore-secretstore = { path = "secret_store", optional = true } [dev-dependencies] ethcore-ipc-tests = { path = "ipc/tests" } @@ -83,6 +84,7 @@ evm-debug = ["ethcore/evm-debug"] evm-debug-tests = ["ethcore/evm-debug-tests"] slow-blocks = ["ethcore/slow-blocks"] final = ["ethcore-util/final"] +secretstore = ["ethcore-secretstore"] [[bin]] path = "parity/main.rs" diff --git a/parity/blockchain.rs b/parity/blockchain.rs index 59cfd0a59..1eeb3d71e 100644 --- a/parity/blockchain.rs +++ b/parity/blockchain.rs @@ -185,7 +185,7 @@ fn execute_import(cmd: ImportBlockchain) -> Result<(), String> { execute_upgrades(&cmd.dirs.base, &db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.db_root_path().as_path()))?; // create dirs used by parity - cmd.dirs.create_dirs(false, false)?; + cmd.dirs.create_dirs(false, false, false)?; // prepare client config let mut client_config = to_client_config( @@ -356,7 +356,7 @@ fn start_client( execute_upgrades(&dirs.base, &db_dirs, algorithm, compaction.compaction_profile(db_dirs.db_root_path().as_path()))?; // create dirs used by parity - dirs.create_dirs(false, false)?; + dirs.create_dirs(false, false, false)?; // prepare client config let client_config = to_client_config( diff --git a/parity/cli/config.full.toml b/parity/cli/config.full.toml index 0983bf792..a8b3c4fc1 100644 --- a/parity/cli/config.full.toml +++ b/parity/cli/config.full.toml @@ -67,6 +67,12 @@ path = "$HOME/.parity/dapps" user = "test_user" pass = "test_pass" +[secretstore] +disable = false +port = 8082 +interface = "local" +path = "$HOME/.parity/secretstore" + [ipfs] enable = false port = 5001 diff --git a/parity/cli/config.toml b/parity/cli/config.toml index 288f3b2ed..9b356a811 100644 --- a/parity/cli/config.toml +++ b/parity/cli/config.toml @@ -38,6 +38,9 @@ port = 8080 user = "username" pass = "password" +[secretstore] +port = 8082 + [ipfs] enable = false port = 5001 diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index a416aa4ce..f105c9c12 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -189,6 +189,16 @@ usage! { or |c: &Config| otry!(c.dapps).pass.clone().map(Some), flag_dapps_apis_all: bool = false, or |_| None, + // Secret Store + flag_no_secretstore: bool = false, + or |c: &Config| otry!(c.secretstore).disable.clone(), + flag_secretstore_port: u16 = 8082u16, + or |c: &Config| otry!(c.secretstore).port.clone(), + flag_secretstore_interface: String = "local", + or |c: &Config| otry!(c.secretstore).interface.clone(), + flag_secretstore_path: String = "$BASE/secretstore", + or |c: &Config| otry!(c.secretstore).path.clone(), + // IPFS flag_ipfs_api: bool = false, or |c: &Config| otry!(c.ipfs).enable.clone(), @@ -327,6 +337,7 @@ struct Config { rpc: Option, ipc: Option, dapps: Option, + secretstore: Option, ipfs: Option, mining: Option, footprint: Option, @@ -416,6 +427,14 @@ struct Dapps { pass: Option, } +#[derive(Default, Debug, PartialEq, RustcDecodable)] +struct SecretStore { + disable: Option, + port: Option, + interface: Option, + path: Option, +} + #[derive(Default, Debug, PartialEq, RustcDecodable)] struct Ipfs { enable: Option, @@ -495,7 +514,8 @@ struct Misc { mod tests { use super::{ Args, ArgsError, - Config, Operating, Account, Ui, Network, Rpc, Ipc, Dapps, Ipfs, Mining, Footprint, Snapshots, VM, Misc + Config, Operating, Account, Ui, Network, Rpc, Ipc, Dapps, Ipfs, Mining, Footprint, + Snapshots, VM, Misc, SecretStore, }; use toml; @@ -650,6 +670,11 @@ mod tests { flag_dapps_pass: Some("test_pass".into()), flag_dapps_apis_all: false, + flag_no_secretstore: false, + flag_secretstore_port: 8082u16, + flag_secretstore_interface: "local".into(), + flag_secretstore_path: "$HOME/.parity/secretstore".into(), + // IPFS flag_ipfs_api: false, flag_ipfs_api_port: 5001u16, @@ -839,6 +864,12 @@ mod tests { user: Some("username".into()), pass: Some("password".into()) }), + secretstore: Some(SecretStore { + disable: None, + port: Some(8082), + interface: None, + path: None, + }), ipfs: Some(Ipfs { enable: Some(false), port: Some(5001) diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index fd19a8004..430154752 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -179,6 +179,16 @@ API and Console Options: --ipfs-api-port PORT Configure on which port the IPFS HTTP API should listen. (default: {flag_ipfs_api_port}) +Secret Store Options: + --no-secretstore Disable Secret Store functionality. (default: {flag_no_secretstore}) + --secretstore-port PORT Specify the port portion for Secret Store Key Server + (default: {flag_secretstore_port}). + --secretstore-interface IP Specify the hostname portion for Secret Store Key Server, IP + should be an interface's IP address, or local + (default: {flag_secretstore_interface}). + --secretstore-path PATH Specify directory where Secret Store should save its data. + (default: {flag_secretstore_path}) + Sealing/Mining Options: --author ADDRESS Specify the block author (aka "coinbase") address for sending block rewards from sealed blocks. diff --git a/parity/configuration.rs b/parity/configuration.rs index 34fea453d..dd1b8546c 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -39,6 +39,7 @@ use dir::{self, Directories, default_hypervisor_path, default_local_path, defaul use dapps::Configuration as DappsConfiguration; use ipfs::Configuration as IpfsConfiguration; use signer::{Configuration as SignerConfiguration}; +use secretstore::Configuration as SecretStoreConfiguration; use updater::{UpdatePolicy, UpdateFilter, ReleaseTrack}; use run::RunCmd; use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, KillBlockchain, ExportState, DataFormat}; @@ -121,6 +122,7 @@ impl Configuration { let dapps_conf = self.dapps_config(); let ipfs_conf = self.ipfs_config(); let signer_conf = self.signer_config(); + let secretstore_conf = self.secretstore_config(); let format = self.format()?; let cmd = if self.args.flag_version { @@ -346,6 +348,7 @@ impl Configuration { dapps_conf: dapps_conf, ipfs_conf: ipfs_conf, signer_conf: signer_conf, + secretstore_conf: secretstore_conf, dapp: self.dapp_to_open()?, ui: self.args.cmd_ui, name: self.args.flag_identity, @@ -542,6 +545,15 @@ impl Configuration { } } + fn secretstore_config(&self) -> SecretStoreConfiguration { + SecretStoreConfiguration { + enabled: self.secretstore_enabled(), + interface: self.secretstore_interface(), + port: self.args.flag_secretstore_port, + data_path: self.directories().secretstore, + } + } + fn ipfs_config(&self) -> IpfsConfiguration { IpfsConfiguration { enabled: self.args.flag_ipfs_api, @@ -787,6 +799,7 @@ impl Configuration { let db_path = replace_home_for_db(&data_path, &local_path, &base_db_path); let keys_path = replace_home(&data_path, &self.args.flag_keys_path); let dapps_path = replace_home(&data_path, &self.args.flag_dapps_path); + let secretstore_path = replace_home(&data_path, &self.args.flag_secretstore_path); let ui_path = replace_home(&data_path, &self.args.flag_ui_path); if self.args.flag_geth && !cfg!(windows) { @@ -810,6 +823,7 @@ impl Configuration { db: db_path, dapps: dapps_path, signer: ui_path, + secretstore: secretstore_path, } } @@ -851,6 +865,13 @@ impl Configuration { }.into() } + fn secretstore_interface(&self) -> String { + match self.args.flag_secretstore_interface.as_str() { + "local" => "127.0.0.1", + x => x, + }.into() + } + fn stratum_interface(&self) -> String { match self.args.flag_stratum_interface.as_str() { "local" => "127.0.0.1", @@ -863,6 +884,10 @@ impl Configuration { !self.args.flag_dapps_off && !self.args.flag_no_dapps && cfg!(feature = "dapps") } + fn secretstore_enabled(&self) -> bool { + !self.args.flag_no_secretstore && cfg!(feature = "secretstore") + } + fn ui_enabled(&self) -> bool { if self.args.flag_force_ui { return true; @@ -1083,7 +1108,7 @@ mod tests { fn test_run_cmd() { let args = vec!["parity"]; let conf = parse(&args); - assert_eq!(conf.into_command().unwrap().cmd, Cmd::Run(RunCmd { + let mut expected = RunCmd { cache_config: Default::default(), dirs: Default::default(), spec: Default::default(), @@ -1113,6 +1138,7 @@ mod tests { dapps_conf: Default::default(), ipfs_conf: Default::default(), signer_conf: Default::default(), + secretstore_conf: Default::default(), ui: false, dapp: None, name: "".into(), @@ -1123,7 +1149,9 @@ mod tests { check_seal: true, download_old_blocks: true, verifier_settings: Default::default(), - })); + }; + expected.secretstore_conf.enabled = cfg!(feature = "secretstore"); + assert_eq!(conf.into_command().unwrap().cmd, Cmd::Run(expected)); } #[test] diff --git a/parity/dir.rs b/parity/dir.rs index f48d052a4..434254774 100644 --- a/parity/dir.rs +++ b/parity/dir.rs @@ -45,6 +45,7 @@ pub struct Directories { pub keys: String, pub signer: String, pub dapps: String, + pub secretstore: String, } impl Default for Directories { @@ -57,12 +58,13 @@ impl Default for Directories { keys: replace_home(&data_dir, "$BASE/keys"), signer: replace_home(&data_dir, "$BASE/signer"), dapps: replace_home(&data_dir, "$BASE/dapps"), + secretstore: replace_home(&data_dir, "$BASE/secretstore"), } } } impl Directories { - pub fn create_dirs(&self, dapps_enabled: bool, signer_enabled: bool) -> Result<(), String> { + pub fn create_dirs(&self, dapps_enabled: bool, signer_enabled: bool, secretstore_enabled: bool) -> Result<(), String> { fs::create_dir_all(&self.base).map_err(|e| e.to_string())?; fs::create_dir_all(&self.db).map_err(|e| e.to_string())?; fs::create_dir_all(&self.keys).map_err(|e| e.to_string())?; @@ -72,6 +74,9 @@ impl Directories { if dapps_enabled { fs::create_dir_all(&self.dapps).map_err(|e| e.to_string())?; } + if secretstore_enabled { + fs::create_dir_all(&self.secretstore).map_err(|e| e.to_string())?; + } Ok(()) } @@ -241,6 +246,7 @@ mod tests { keys: replace_home(&data_dir, "$BASE/keys"), signer: replace_home(&data_dir, "$BASE/signer"), dapps: replace_home(&data_dir, "$BASE/dapps"), + secretstore: replace_home(&data_dir, "$BASE/secretstore"), }; assert_eq!(expected, Directories::default()); } diff --git a/parity/main.rs b/parity/main.rs index bd4afca6c..eefd42a94 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -66,6 +66,10 @@ extern crate log as rlog; #[cfg(feature="stratum")] extern crate ethcore_stratum; + +#[cfg(feature="secretstore")] +extern crate ethcore_secretstore; + #[cfg(feature = "dapps")] extern crate ethcore_dapps; @@ -101,6 +105,7 @@ mod rpc_apis; mod run; mod signer; mod snapshot; +mod secretstore; mod upgrade; mod url; mod user_defaults; diff --git a/parity/run.rs b/parity/run.rs index df9fc7384..e91a8716b 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -49,6 +49,7 @@ use user_defaults::UserDefaults; use dapps; use ipfs; use signer; +use secretstore; use modules; use rpc_apis; use rpc; @@ -96,6 +97,7 @@ pub struct RunCmd { pub dapps_conf: dapps::Configuration, pub ipfs_conf: ipfs::Configuration, pub signer_conf: signer::Configuration, + pub secretstore_conf: secretstore::Configuration, pub dapp: Option, pub ui: bool, pub name: String, @@ -190,7 +192,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R execute_upgrades(&cmd.dirs.base, &db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.db_root_path().as_path()))?; // create dirs used by parity - cmd.dirs.create_dirs(cmd.dapps_conf.enabled, cmd.signer_conf.enabled)?; + cmd.dirs.create_dirs(cmd.dapps_conf.enabled, cmd.signer_conf.enabled, cmd.secretstore_conf.enabled)?; // run in daemon mode if let Some(pid_file) = cmd.daemon { @@ -422,6 +424,10 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R }; let signer_server = signer::start(cmd.signer_conf.clone(), signer_deps)?; + // secret store key server + let secretstore_deps = secretstore::Dependencies { }; + let secretstore_key_server = secretstore::start(cmd.secretstore_conf.clone(), secretstore_deps); + // the ipfs server let ipfs_server = match cmd.ipfs_conf.enabled { true => Some(ipfs::start_server(cmd.ipfs_conf.port, client.clone())?), @@ -484,7 +490,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R let restart = wait_for_exit(panic_handler, Some(updater), can_restart); // drop this stuff as soon as exit detected. - drop((http_server, ipc_server, dapps_server, signer_server, ipfs_server, event_loop)); + drop((http_server, ipc_server, dapps_server, signer_server, secretstore_key_server, ipfs_server, event_loop)); info!("Finishing work, please wait..."); diff --git a/parity/secretstore.rs b/parity/secretstore.rs new file mode 100644 index 000000000..79a209504 --- /dev/null +++ b/parity/secretstore.rs @@ -0,0 +1,105 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use dir::default_data_path; +use helpers::replace_home; + +#[derive(Debug, PartialEq, Clone)] +/// Secret store configuration +pub struct Configuration { + /// Is secret store functionality enabled? + pub enabled: bool, + /// Interface to listen to + pub interface: String, + /// Port to listen to + pub port: u16, + /// Data directory path for secret store + pub data_path: String, +} + +#[derive(Debug, PartialEq, Clone)] +/// Secret store dependencies +pub struct Dependencies { + // the only dependency will be BlockChainClient +} + +#[cfg(not(feature = "secretstore"))] +mod server { + use super::{Configuration, Dependencies}; + + /// Noop key server implementation + pub struct KeyServer; + + impl KeyServer { + /// Create new noop key server + pub fn new(_conf: Configuration, _deps: Dependencies) -> Result { + Ok(KeyServer) + } + } +} + +#[cfg(feature="secretstore")] +mod server { + use ethcore_secretstore; + use super::{Configuration, Dependencies}; + + /// Key server + pub struct KeyServer { + _key_server: Box, + } + + impl KeyServer { + /// Create new key server + pub fn new(conf: Configuration, _deps: Dependencies) -> Result { + let conf = ethcore_secretstore::ServiceConfiguration { + listener_addr: conf.interface, + listener_port: conf.port, + data_path: conf.data_path, + }; + + let key_server = ethcore_secretstore::start(conf) + .map_err(Into::::into)?; + + Ok(KeyServer { + _key_server: key_server, + }) + } + } +} + +pub use self::server::KeyServer; + +impl Default for Configuration { + fn default() -> Self { + let data_dir = default_data_path(); + Configuration { + enabled: true, + interface: "127.0.0.1".to_owned(), + port: 8082, + data_path: replace_home(&data_dir, "$BASE/secretstore"), + } + } +} + +/// Start secret store-related functionality +pub fn start(conf: Configuration, deps: Dependencies) -> Result, String> { + if !conf.enabled { + return Ok(None); + } + + KeyServer::new(conf, deps) + .map(|s| Some(s)) +} diff --git a/secret_store/Cargo.toml b/secret_store/Cargo.toml new file mode 100644 index 000000000..111ff5aff --- /dev/null +++ b/secret_store/Cargo.toml @@ -0,0 +1,25 @@ +[package] +description = "Ethcore Secret Store" +name = "ethcore-secretstore" +version = "1.0.0" +license = "GPL-3.0" +authors = ["Parity Technologies "] +build = "build.rs" + +[build-dependencies] +ethcore-ipc-codegen = { path = "../ipc/codegen" } + +[dependencies] +log = "0.3" +parking_lot = "0.3" +hyper = { version = "0.9", default-features = false } +url = "1.0" +ethcore-devtools = { path = "../devtools" } +ethcore-util = { path = "../util" } +ethcore-ipc = { path = "../ipc/rpc" } +ethcore-ipc-nano = { path = "../ipc/nano" } +ethcrypto = { path = "../ethcrypto" } +ethkey = { path = "../ethkey" } + +[profile.release] +debug = true diff --git a/secret_store/build.rs b/secret_store/build.rs new file mode 100644 index 000000000..b2b27ea1e --- /dev/null +++ b/secret_store/build.rs @@ -0,0 +1,22 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +extern crate ethcore_ipc_codegen; + +fn main() { + ethcore_ipc_codegen::derive_binary("src/types/mod.rs.in").unwrap(); + ethcore_ipc_codegen::derive_ipc_cond("src/traits.rs", cfg!(feature="ipc")).unwrap(); +} diff --git a/secret_store/src/acl_storage.rs b/secret_store/src/acl_storage.rs new file mode 100644 index 000000000..47ec3d44a --- /dev/null +++ b/secret_store/src/acl_storage.rs @@ -0,0 +1,51 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use std::collections::{HashMap, HashSet}; +use parking_lot::RwLock; +use types::all::{Error, DocumentAddress, Public}; + +/// ACL storage of Secret Store +pub trait AclStorage: Send + Sync { + /// Check if requestor with `public` key can access document with hash `document` + fn check(&self, public: &Public, document: &DocumentAddress) -> Result; +} + +/// Dummy ACL storage implementation +#[derive(Default, Debug)] +pub struct DummyAclStorage { + prohibited: RwLock>>, +} + +impl DummyAclStorage { + #[cfg(test)] + /// Prohibit given requestor access to given document + pub fn prohibit(&self, public: Public, document: DocumentAddress) { + self.prohibited.write() + .entry(public) + .or_insert_with(Default::default) + .insert(document); + } +} + +impl AclStorage for DummyAclStorage { + fn check(&self, public: &Public, document: &DocumentAddress) -> Result { + Ok(self.prohibited.read() + .get(public) + .map(|docs| !docs.contains(document)) + .unwrap_or(true)) + } +} diff --git a/secret_store/src/http_listener.rs b/secret_store/src/http_listener.rs new file mode 100644 index 000000000..92799d221 --- /dev/null +++ b/secret_store/src/http_listener.rs @@ -0,0 +1,176 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use std::str::FromStr; +use std::sync::Arc; +use hyper::header; +use hyper::uri::RequestUri; +use hyper::method::Method as HttpMethod; +use hyper::status::StatusCode as HttpStatusCode; +use hyper::server::{Server as HttpServer, Request as HttpRequest, Response as HttpResponse, Handler as HttpHandler, + Listening as HttpListening}; +use url::percent_encoding::percent_decode; + +use util::ToPretty; +use traits::KeyServer; +use types::all::{Error, ServiceConfiguration, RequestSignature, DocumentAddress, DocumentEncryptedKey}; + +/// Key server http-requests listener +pub struct KeyServerHttpListener { + _http_server: HttpListening, + handler: Arc>, +} + +/// Parsed http request +#[derive(Debug, Clone, PartialEq)] +enum Request { + /// Invalid request + Invalid, + /// Request encryption key of given document for given requestor + GetDocumentKey(DocumentAddress, RequestSignature), +} + +/// Cloneable http handler +struct KeyServerHttpHandler { + handler: Arc>, +} + +/// Shared http handler +struct KeyServerSharedHttpHandler { + key_server: T, +} + +impl KeyServerHttpListener where T: KeyServer + 'static { + /// Start KeyServer http listener + pub fn start(config: ServiceConfiguration, key_server: T) -> Result { + let shared_handler = Arc::new(KeyServerSharedHttpHandler { + key_server: key_server, + }); + let handler = KeyServerHttpHandler { + handler: shared_handler.clone(), + }; + + let listener_addr: &str = &format!("{}:{}", config.listener_addr, config.listener_port); + let http_server = HttpServer::http(&listener_addr).unwrap(); + let http_server = http_server.handle(handler).unwrap(); + let listener = KeyServerHttpListener { + _http_server: http_server, + handler: shared_handler, + }; + Ok(listener) + } +} + +impl KeyServer for KeyServerHttpListener where T: KeyServer + 'static { + fn document_key(&self, signature: &RequestSignature, document: &DocumentAddress) -> Result { + self.handler.key_server.document_key(signature, document) + } +} + +impl HttpHandler for KeyServerHttpHandler where T: KeyServer + 'static { + fn handle(&self, req: HttpRequest, mut res: HttpResponse) { + if req.method != HttpMethod::Get { + warn!(target: "secretstore", "Ignoring {}-request {}", req.method, req.uri); + *res.status_mut() = HttpStatusCode::NotFound; + return; + } + + if req.headers.has::() { + warn!(target: "secretstore", "Ignoring {}-request {} with Origin header", req.method, req.uri); + *res.status_mut() = HttpStatusCode::NotFound; + return; + } + + match req.uri { + RequestUri::AbsolutePath(ref path) => match parse_request(&path) { + Request::GetDocumentKey(document, signature) => { + let document_key = self.handler.key_server.document_key(&signature, &document) + .map_err(|err| { + warn!(target: "secretstore", "GetDocumentKey request {} has failed with: {}", req.uri, err); + err + }); + match document_key { + Ok(document_key) => { + let document_key = document_key.to_hex().into_bytes(); + res.headers_mut().set(header::ContentType::plaintext()); + if let Err(err) = res.send(&document_key) { + // nothing to do, but log error + warn!(target: "secretstore", "GetDocumentKey request {} response has failed with: {}", req.uri, err); + } + }, + Err(Error::BadSignature) => *res.status_mut() = HttpStatusCode::BadRequest, + Err(Error::AccessDenied) => *res.status_mut() = HttpStatusCode::Forbidden, + Err(Error::DocumentNotFound) => *res.status_mut() = HttpStatusCode::NotFound, + Err(Error::Database(_)) => *res.status_mut() = HttpStatusCode::InternalServerError, + Err(Error::Internal(_)) => *res.status_mut() = HttpStatusCode::InternalServerError, + } + }, + Request::Invalid => { + warn!(target: "secretstore", "Ignoring invalid {}-request {}", req.method, req.uri); + *res.status_mut() = HttpStatusCode::BadRequest; + }, + }, + _ => { + warn!(target: "secretstore", "Ignoring invalid {}-request {}", req.method, req.uri); + *res.status_mut() = HttpStatusCode::NotFound; + }, + }; + } +} + +fn parse_request(uri_path: &str) -> Request { + let uri_path = match percent_decode(uri_path.as_bytes()).decode_utf8() { + Ok(path) => path, + Err(_) => return Request::Invalid, + }; + + let path: Vec = uri_path.trim_left_matches('/').split('/').map(Into::into).collect(); + if path.len() != 2 || path[0].is_empty() || path[1].is_empty() { + return Request::Invalid; + } + + let document = DocumentAddress::from_str(&path[0]); + let signature = RequestSignature::from_str(&path[1]); + match (document, signature) { + (Ok(document), Ok(signature)) => Request::GetDocumentKey(document, signature), + _ => Request::Invalid, + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use super::super::RequestSignature; + use super::{parse_request, Request}; + + #[test] + fn parse_request_successful() { + assert_eq!(parse_request("/0000000000000000000000000000000000000000000000000000000000000001/a199fb39e11eefb61c78a4074a53c0d4424600a3e74aad4fb9d93a26c30d067e1d4d29936de0c73f19827394a1dd049480a0d581aee7ae7546968da7d3d1c2fd01"), + Request::GetDocumentKey("0000000000000000000000000000000000000000000000000000000000000001".into(), + RequestSignature::from_str("a199fb39e11eefb61c78a4074a53c0d4424600a3e74aad4fb9d93a26c30d067e1d4d29936de0c73f19827394a1dd049480a0d581aee7ae7546968da7d3d1c2fd01").unwrap())); + assert_eq!(parse_request("/%30000000000000000000000000000000000000000000000000000000000000001/a199fb39e11eefb61c78a4074a53c0d4424600a3e74aad4fb9d93a26c30d067e1d4d29936de0c73f19827394a1dd049480a0d581aee7ae7546968da7d3d1c2fd01"), + Request::GetDocumentKey("0000000000000000000000000000000000000000000000000000000000000001".into(), + RequestSignature::from_str("a199fb39e11eefb61c78a4074a53c0d4424600a3e74aad4fb9d93a26c30d067e1d4d29936de0c73f19827394a1dd049480a0d581aee7ae7546968da7d3d1c2fd01").unwrap())); + } + + #[test] + fn parse_request_failed() { + assert_eq!(parse_request("/0000000000000000000000000000000000000000000000000000000000000001"), Request::Invalid); + assert_eq!(parse_request("/0000000000000000000000000000000000000000000000000000000000000001/"), Request::Invalid); + assert_eq!(parse_request("/a/b"), Request::Invalid); + assert_eq!(parse_request("/0000000000000000000000000000000000000000000000000000000000000001/a199fb39e11eefb61c78a4074a53c0d4424600a3e74aad4fb9d93a26c30d067e1d4d29936de0c73f19827394a1dd049480a0d581aee7ae7546968da7d3d1c2fd01/0000000000000000000000000000000000000000000000000000000000000002"), Request::Invalid); + } +} diff --git a/secret_store/src/key_server.rs b/secret_store/src/key_server.rs new file mode 100644 index 000000000..32ac48031 --- /dev/null +++ b/secret_store/src/key_server.rs @@ -0,0 +1,124 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use ethcrypto; +use ethkey; +use super::acl_storage::AclStorage; +use super::key_storage::KeyStorage; +use traits::KeyServer; +use types::all::{Error, RequestSignature, DocumentAddress, DocumentEncryptedKey}; + +/// Secret store key server implementation +pub struct KeyServerImpl { + acl_storage: T, + key_storage: U, +} + +impl KeyServerImpl where T: AclStorage, U: KeyStorage { + /// Create new key server instance + pub fn new(acl_storage: T, key_storage: U) -> Self { + KeyServerImpl { + acl_storage: acl_storage, + key_storage: key_storage, + } + } +} + +impl KeyServer for KeyServerImpl where T: AclStorage, U: KeyStorage { + fn document_key(&self, signature: &RequestSignature, document: &DocumentAddress) -> Result { + // recover requestor' public key from signature + let public = ethkey::recover(signature, document) + .map_err(|_| Error::BadSignature)?; + + // check that requestor has access to the document + if !self.acl_storage.check(&public, document)? { + return Err(Error::AccessDenied); + } + + // read unencrypted document key + let document_key = self.key_storage.get(document)?; + // encrypt document key with requestor public key + let document_key = ethcrypto::ecies::encrypt_single_message(&public, &document_key) + .map_err(|err| Error::Internal(format!("Error encrypting document key: {}", err)))?; + Ok(document_key) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use ethcrypto; + use ethkey::{self, Secret}; + use acl_storage::DummyAclStorage; + use key_storage::KeyStorage; + use key_storage::tests::DummyKeyStorage; + use super::super::{Error, RequestSignature, DocumentAddress}; + use super::{KeyServer, KeyServerImpl}; + + const DOCUMENT1: &'static str = "0000000000000000000000000000000000000000000000000000000000000001"; + const DOCUMENT2: &'static str = "0000000000000000000000000000000000000000000000000000000000000002"; + const KEY1: &'static str = "key1"; + const PRIVATE1: &'static str = "03055e18a8434dcc9061cc1b81c4ef84dc7cf4574d755e52cdcf0c8898b25b11"; + const PUBLIC2: &'static str = "dfe62f56bb05fbd85b485bac749f3410309e24b352bac082468ce151e9ddb94fa7b5b730027fe1c7c5f3d5927621d269f91aceb5caa3c7fe944677a22f88a318"; + const PRIVATE2: &'static str = "0eb3816f4f705fa0fd952fb27b71b8c0606f09f4743b5b65cbc375bd569632f2"; + + fn create_key_server() -> KeyServerImpl { + let acl_storage = DummyAclStorage::default(); + let key_storage = DummyKeyStorage::default(); + key_storage.insert(DOCUMENT1.into(), KEY1.into()).unwrap(); + acl_storage.prohibit(PUBLIC2.into(), DOCUMENT1.into()); + KeyServerImpl::new(acl_storage, key_storage) + } + + fn make_signature(secret: &str, document: &'static str) -> RequestSignature { + let secret = Secret::from_str(secret).unwrap(); + let document: DocumentAddress = document.into(); + ethkey::sign(&secret, &document).unwrap() + } + + #[test] + fn document_key_succeeds() { + let key_server = create_key_server(); + let signature = make_signature(PRIVATE1, DOCUMENT1); + let document_key = key_server.document_key(&signature, &DOCUMENT1.into()).unwrap(); + let document_key = ethcrypto::ecies::decrypt_single_message(&Secret::from_str(PRIVATE1).unwrap(), &document_key); + assert_eq!(document_key, Ok(KEY1.into())); + } + + #[test] + fn document_key_fails_when_bad_signature() { + let key_server = create_key_server(); + let signature = RequestSignature::default(); + let document_key = key_server.document_key(&signature, &DOCUMENT1.into()); + assert_eq!(document_key, Err(Error::BadSignature)); + } + + #[test] + fn document_key_fails_when_acl_check_fails() { + let key_server = create_key_server(); + let signature = make_signature(PRIVATE2, DOCUMENT1); + let document_key = key_server.document_key(&signature, &DOCUMENT1.into()); + assert_eq!(document_key, Err(Error::AccessDenied)); + } + + #[test] + fn document_key_fails_when_document_not_found() { + let key_server = create_key_server(); + let signature = make_signature(PRIVATE1, DOCUMENT2); + let document_key = key_server.document_key(&signature, &DOCUMENT2.into()); + assert_eq!(document_key, Err(Error::DocumentNotFound)); + } +} diff --git a/secret_store/src/key_storage.rs b/secret_store/src/key_storage.rs new file mode 100644 index 000000000..fe7777410 --- /dev/null +++ b/secret_store/src/key_storage.rs @@ -0,0 +1,115 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use std::path::PathBuf; +use util::Database; +use types::all::{Error, ServiceConfiguration, DocumentAddress, DocumentKey}; + +/// Document encryption keys storage +pub trait KeyStorage: Send + Sync { + /// Insert document encryption key + fn insert(&self, document: DocumentAddress, key: DocumentKey) -> Result<(), Error>; + /// Get document encryption key + fn get(&self, document: &DocumentAddress) -> Result; +} + +/// Persistent document encryption keys storage +pub struct PersistentKeyStorage { + db: Database, +} + +impl PersistentKeyStorage { + /// Create new persistent document encryption keys storage + pub fn new(config: &ServiceConfiguration) -> Result { + let mut db_path = PathBuf::from(&config.data_path); + db_path.push("db"); + let db_path = db_path.to_str().ok_or(Error::Database("Invalid secretstore path".to_owned()))?; + + Ok(PersistentKeyStorage { + db: Database::open_default(&db_path).map_err(Error::Database)?, + }) + } +} + +impl KeyStorage for PersistentKeyStorage { + fn insert(&self, document: DocumentAddress, key: DocumentKey) -> Result<(), Error> { + let mut batch = self.db.transaction(); + batch.put(None, &document, &key); + self.db.write(batch).map_err(Error::Database) + } + + fn get(&self, document: &DocumentAddress) -> Result { + self.db.get(None, document) + .map_err(Error::Database)? + .ok_or(Error::DocumentNotFound) + .map(|key| key.to_vec()) + } +} + +#[cfg(test)] +pub mod tests { + use std::collections::HashMap; + use parking_lot::RwLock; + use devtools::RandomTempPath; + use super::super::types::all::{Error, ServiceConfiguration, DocumentAddress, DocumentKey}; + use super::{KeyStorage, PersistentKeyStorage}; + + #[derive(Default)] + /// In-memory document encryption keys storage + pub struct DummyKeyStorage { + keys: RwLock>, + } + + impl KeyStorage for DummyKeyStorage { + fn insert(&self, document: DocumentAddress, key: DocumentKey) -> Result<(), Error> { + self.keys.write().insert(document, key); + Ok(()) + } + + fn get(&self, document: &DocumentAddress) -> Result { + self.keys.read().get(document).cloned().ok_or(Error::DocumentNotFound) + } + } + + #[test] + fn persistent_key_storage() { + let path = RandomTempPath::create_dir(); + let config = ServiceConfiguration { + listener_addr: "0.0.0.0".to_owned(), + listener_port: 8082, + data_path: path.as_str().to_owned(), + }; + + let key1 = DocumentAddress::from(1); + let value1: DocumentKey = vec![0x77, 0x88]; + let key2 = DocumentAddress::from(2); + let value2: DocumentKey = vec![0x11, 0x22]; + let key3 = DocumentAddress::from(3); + + let key_storage = PersistentKeyStorage::new(&config).unwrap(); + key_storage.insert(key1.clone(), value1.clone()).unwrap(); + key_storage.insert(key2.clone(), value2.clone()).unwrap(); + assert_eq!(key_storage.get(&key1), Ok(value1.clone())); + assert_eq!(key_storage.get(&key2), Ok(value2.clone())); + assert_eq!(key_storage.get(&key3), Err(Error::DocumentNotFound)); + drop(key_storage); + + let key_storage = PersistentKeyStorage::new(&config).unwrap(); + assert_eq!(key_storage.get(&key1), Ok(value1)); + assert_eq!(key_storage.get(&key2), Ok(value2)); + assert_eq!(key_storage.get(&key3), Err(Error::DocumentNotFound)); + } +} diff --git a/secret_store/src/lib.rs b/secret_store/src/lib.rs new file mode 100644 index 000000000..390ae1e5e --- /dev/null +++ b/secret_store/src/lib.rs @@ -0,0 +1,52 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +#[macro_use] +extern crate log; +extern crate hyper; +extern crate parking_lot; +extern crate url; + +extern crate ethcore_devtools as devtools; +extern crate ethcore_util as util; +extern crate ethcore_ipc as ipc; +extern crate ethcrypto; +extern crate ethkey; + +mod types; + +mod traits { + #![allow(dead_code, unused_assignments, unused_variables, missing_docs)] // codegen issues + include!(concat!(env!("OUT_DIR"), "/traits.rs")); +} + +mod acl_storage; +mod http_listener; +mod key_server; +mod key_storage; + +pub use types::all::{DocumentAddress, DocumentKey, DocumentEncryptedKey, RequestSignature, Public, + Error, ServiceConfiguration}; +pub use traits::{KeyServer}; + +/// Start new key server instance +pub fn start(config: ServiceConfiguration) -> Result, Error> { + let acl_storage = acl_storage::DummyAclStorage::default(); + let key_storage = key_storage::PersistentKeyStorage::new(&config)?; + let key_server = key_server::KeyServerImpl::new(acl_storage, key_storage); + let listener = http_listener::KeyServerHttpListener::start(config, key_server)?; + Ok(Box::new(listener)) +} diff --git a/secret_store/src/traits.rs b/secret_store/src/traits.rs new file mode 100644 index 000000000..9a68e9c4d --- /dev/null +++ b/secret_store/src/traits.rs @@ -0,0 +1,24 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use types::all::{Error, RequestSignature, DocumentAddress, DocumentEncryptedKey}; + +#[ipc(client_ident="RemoteKeyServer")] +/// Secret store key server +pub trait KeyServer: Send + Sync { + /// Request encryption key of given document for given requestor + fn document_key(&self, signature: &RequestSignature, document: &DocumentAddress) -> Result; +} diff --git a/secret_store/src/types/all.rs b/secret_store/src/types/all.rs new file mode 100644 index 000000000..f318e6543 --- /dev/null +++ b/secret_store/src/types/all.rs @@ -0,0 +1,77 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use std::fmt; + +use ethkey; +use util; + +/// Document address type. +pub type DocumentAddress = util::H256; +/// Document key type. +pub type DocumentKey = util::Bytes; +/// Encrypted key type. +pub type DocumentEncryptedKey = util::Bytes; +/// Request signature type. +pub type RequestSignature = ethkey::Signature; +/// Public key type. +pub use ethkey::Public; + +#[derive(Debug, Clone, PartialEq)] +#[binary] +/// Secret store error +pub enum Error { + /// Bad signature is passed + BadSignature, + /// Access to resource is denied + AccessDenied, + /// Requested document not found + DocumentNotFound, + /// Database-related error + Database(String), + /// Internal error + Internal(String), +} + +#[derive(Debug)] +#[binary] +/// Secret store configuration +pub struct ServiceConfiguration { + /// Interface to listen to + pub listener_addr: String, + /// Port to listen to + pub listener_port: u16, + /// Data directory path for secret store + pub data_path: String, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Error::BadSignature => write!(f, "Bad signature"), + Error::AccessDenied => write!(f, "Access dened"), + Error::DocumentNotFound => write!(f, "Document not found"), + Error::Database(ref msg) => write!(f, "Database error: {}", msg), + Error::Internal(ref msg) => write!(f, "Internal error: {}", msg), + } + } +} + +impl Into for Error { + fn into(self) -> String { + format!("{}", self) + } +} diff --git a/secret_store/src/types/mod.rs b/secret_store/src/types/mod.rs new file mode 100644 index 000000000..584e78f30 --- /dev/null +++ b/secret_store/src/types/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! Types used in the public api + +#![allow(dead_code, unused_assignments, unused_variables)] // codegen issues +include!(concat!(env!("OUT_DIR"), "/mod.rs.in")); diff --git a/secret_store/src/types/mod.rs.in b/secret_store/src/types/mod.rs.in new file mode 100644 index 000000000..0681e2884 --- /dev/null +++ b/secret_store/src/types/mod.rs.in @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +pub mod all;