From e505fe018600e43270f9192bb860680373d14575 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Fri, 16 Mar 2018 17:39:49 +0100 Subject: [PATCH] KeyStore implementation + key derivation (#97) * improve ed25519 bindings * probably broken child derivation * basic keystore * keystore integration in CLI * constant-time mac comparison * fix spaces --- polkadot/cli/Cargo.toml | 2 + polkadot/cli/src/cli.yml | 6 +- polkadot/cli/src/error.rs | 7 + polkadot/cli/src/lib.rs | 27 ++++ polkadot/keystore/Cargo.toml | 18 +++ polkadot/keystore/src/lib.rs | 241 +++++++++++++++++++++++++++++++++++ 6 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 polkadot/keystore/Cargo.toml create mode 100644 polkadot/keystore/src/lib.rs diff --git a/polkadot/cli/Cargo.toml b/polkadot/cli/Cargo.toml index 4bb48abf2b..37d920c3c0 100644 --- a/polkadot/cli/Cargo.toml +++ b/polkadot/cli/Cargo.toml @@ -12,6 +12,7 @@ log = "0.3" hex-literal = "0.1" triehash = "0.1" ed25519 = { path = "../../substrate/ed25519" } +app_dirs = "1.1" substrate-client = { path = "../../substrate/client" } substrate-codec = { path = "../../substrate/codec" } substrate-runtime-io = { path = "../../substrate/runtime-io" } @@ -22,3 +23,4 @@ substrate-rpc-servers = { path = "../../substrate/rpc-servers" } polkadot-primitives = { path = "../primitives" } polkadot-executor = { path = "../executor" } polkadot-runtime = { path = "../runtime" } +polkadot-keystore = { path = "../keystore" } diff --git a/polkadot/cli/src/cli.yml b/polkadot/cli/src/cli.yml index 2e98e2c68e..96679a6835 100644 --- a/polkadot/cli/src/cli.yml +++ b/polkadot/cli/src/cli.yml @@ -5,7 +5,11 @@ args: - log: short: l value_name: LOG_PATTERN - help: Sets a custom logging + help: Sets a custom logging filter + takes_value: true + - keystore-path: + value_name: KEYSTORE_PATH + help: specify custom keystore path takes_value: true subcommands: - collator: diff --git a/polkadot/cli/src/error.rs b/polkadot/cli/src/error.rs index 6b1e2b6024..6c9e22cd55 100644 --- a/polkadot/cli/src/error.rs +++ b/polkadot/cli/src/error.rs @@ -26,4 +26,11 @@ error_chain! { links { Client(client::error::Error, client::error::ErrorKind) #[doc="Client error"]; } + errors { + /// Key store errors + Keystore(e: ::keystore::Error) { + description("Keystore error"), + display("Keystore error: {:?}", e), + } + } } diff --git a/polkadot/cli/src/lib.rs b/polkadot/cli/src/lib.rs index 807ff6c2cb..c5e34af10a 100644 --- a/polkadot/cli/src/lib.rs +++ b/polkadot/cli/src/lib.rs @@ -18,6 +18,7 @@ #![warn(missing_docs)] +extern crate app_dirs; extern crate env_logger; extern crate ed25519; extern crate triehash; @@ -29,6 +30,7 @@ extern crate substrate_rpc_servers as rpc; extern crate polkadot_primitives; extern crate polkadot_executor; extern crate polkadot_runtime; +extern crate polkadot_keystore as keystore; #[macro_use] extern crate hex_literal; @@ -41,9 +43,12 @@ extern crate log; pub mod error; +use std::path::{Path, PathBuf}; + use codec::Slicable; use polkadot_runtime::genesismap::{additional_storage_with_genesis, GenesisConfig}; use client::genesis; +use keystore::Store as Keystore; /// Parse command line arguments and start the node. /// @@ -79,12 +84,19 @@ pub fn run(args: I) -> error::Result<()> where bonding_duration: 90, // 90 days per bond. approval_ratio: 667, // 66.7% approvals required for legislation. }; + let prepare_genesis = || { storage = genesis_config.genesis_map(); let block = genesis::construct_genesis_block(&storage); storage.extend(additional_storage_with_genesis(&block)); (primitives::block::Header::decode(&mut block.header.encode().as_ref()).expect("to_vec() always gives a valid serialisation; qed"), storage.into_iter().collect()) }; + + let keystore_path = matches.value_of("keystore") + .map(|x| Path::new(x).to_owned()) + .unwrap_or_else(default_keystore_path); + + let _keystore = Keystore::open(keystore_path).map_err(::error::ErrorKind::Keystore)?; let client = client::new_in_mem(executor, prepare_genesis)?; let address = "127.0.0.1:9933".parse().unwrap(); @@ -109,6 +121,21 @@ pub fn run(args: I) -> error::Result<()> where Ok(()) } +fn default_keystore_path() -> PathBuf { + use app_dirs::{AppInfo, AppDataType}; + + let app_info = AppInfo { + name: "Polkadot", + author: "Parity Technologies", + }; + + app_dirs::get_app_dir( + AppDataType::UserData, + &app_info, + "keystore", + ).expect("app directories exist on all supported platforms; qed") +} + fn init_logger(pattern: &str) { let mut builder = env_logger::LogBuilder::new(); // Disable info logging by default for some modules: diff --git a/polkadot/keystore/Cargo.toml b/polkadot/keystore/Cargo.toml new file mode 100644 index 0000000000..16ec981769 --- /dev/null +++ b/polkadot/keystore/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "polkadot-keystore" +version = "0.1.0" +authors = ["Parity Technologies "] + +[dependencies] +ethcrypto = { git = "https://github.com/paritytech/parity", default_features = false } +ed25519 = { path = "../../substrate/ed25519" } +error-chain = "0.11" +hex = "0.3" +rand = "0.4" +serde_json = "1.0" +serde = "1.0" +serde_derive = "1.0" +subtle = "0.5" + +[dev-dependencies] +tempdir = "0.3" diff --git a/polkadot/keystore/src/lib.rs b/polkadot/keystore/src/lib.rs new file mode 100644 index 0000000000..ece74d0021 --- /dev/null +++ b/polkadot/keystore/src/lib.rs @@ -0,0 +1,241 @@ +// Copyright 2017 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Keystore (and session key management) for polkadot. + +extern crate ethcrypto as crypto; +extern crate subtle; +extern crate ed25519; +extern crate rand; +extern crate serde_json; +extern crate serde; +extern crate hex; + +#[macro_use] +extern crate serde_derive; + +#[macro_use] +extern crate error_chain; + +#[cfg(test)] +extern crate tempdir; + +use std::path::PathBuf; +use std::fs::{self, File}; +use std::io::{self, Write}; + +use crypto::Keccak256; +use ed25519::{Pair, Public, PKCS_LEN}; + +pub use crypto::KEY_ITERATIONS; + +error_chain! { + foreign_links { + Io(io::Error); + Json(serde_json::Error); + } + + errors { + InvalidPassword { + description("Invalid password"), + display("Invalid password"), + } + InvalidPKCS8 { + description("Invalid PKCS#8 data"), + display("Invalid PKCS#8 data"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InvalidPassword; + +#[derive(Serialize, Deserialize)] +struct EncryptedKey { + mac: [u8; 32], + salt: [u8; 32], + ciphertext: Vec, // TODO: switch to fixed-size when serde supports + iv: [u8; 16], + iterations: u32, +} + +impl EncryptedKey { + fn encrypt(plain: &[u8; PKCS_LEN], password: &str, iterations: u32) -> Self { + use rand::{Rng, OsRng}; + + let mut rng = OsRng::new().expect("OS Randomness available on all supported platforms; qed"); + + let salt: [u8; 32] = rng.gen(); + let iv: [u8; 16] = rng.gen(); + + // two parts of derived key + // DK = [ DK[0..15] DK[16..31] ] = [derived_left_bits, derived_right_bits] + let (derived_left_bits, derived_right_bits) = crypto::derive_key_iterations(password, &salt, iterations); + + // preallocated (on-stack in case of `Secret`) buffer to hold cipher + // length = length(plain) as we are using CTR-approach + let mut ciphertext = vec![0; PKCS_LEN]; + + // aes-128-ctr with initial vector of iv + crypto::aes::encrypt(&derived_left_bits, &iv, plain, &mut *ciphertext); + + // KECCAK(DK[16..31] ++ ), where DK[16..31] - derived_right_bits + let mac = crypto::derive_mac(&derived_right_bits, &*ciphertext).keccak256(); + + EncryptedKey { + salt, + iv, + mac, + iterations, + ciphertext, + } + } + + fn decrypt(&self, password: &str) -> Result<[u8; PKCS_LEN]> { + let (derived_left_bits, derived_right_bits) = + crypto::derive_key_iterations(password, &self.salt, self.iterations); + + let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256(); + + if subtle::slices_equal(&mac[..], &self.mac[..]) != 1 { + return Err(ErrorKind::InvalidPassword.into()); + } + + let mut plain = [0; PKCS_LEN]; + crypto::aes::decrypt(&derived_left_bits, &self.iv, &self.ciphertext, &mut plain[..]); + Ok(plain) + } +} + +/// Key store. +pub struct Store { + path: PathBuf, +} + +impl Store { + /// Create a new store at the given path. + pub fn open(path: PathBuf) -> Result { + fs::create_dir_all(&path)?; + Ok(Store { path }) + } + + /// Generate a new key, placing it into the store. + pub fn generate(&self, password: &str) -> Result { + let (pair, pkcs_bytes) = Pair::generate_with_pkcs8(); + let key_file = EncryptedKey::encrypt(&pkcs_bytes, password, KEY_ITERATIONS as u32); + + let mut file = File::create(self.key_file_path(&pair.public()))?; + ::serde_json::to_writer(&file, &key_file)?; + + file.flush()?; + + Ok(pair) + } + + /// Load a key file with given public key. + pub fn load(&self, public: &Public, password: &str) -> Result { + let path = self.key_file_path(public); + let file = File::open(path)?; + + let encrypted_key: EncryptedKey = ::serde_json::from_reader(&file)?; + let pkcs_bytes = encrypted_key.decrypt(password)?; + + Pair::from_pkcs8(&pkcs_bytes[..]).map_err(|_| ErrorKind::InvalidPKCS8.into()) + } + + /// Get public keys of all stored keys. + pub fn contents(&self) -> Result> { + let mut public_keys = Vec::new(); + for entry in fs::read_dir(&self.path)? { + let entry = entry?; + let path = entry.path(); + + // skip directories and non-unicode file names (hex is unicode) + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if name.len() != 64 { continue } + + match hex::decode(name) { + Ok(ref hex) if hex.len() == 32 => { + let mut buf = [0; 32]; + buf.copy_from_slice(&hex[..]); + + public_keys.push(Public(buf)); + } + _ => continue, + } + } + } + + Ok(public_keys) + } + + fn key_file_path(&self, public: &Public) -> PathBuf { + let mut buf = self.path.clone(); + buf.push(hex::encode(public.as_slice())); + buf + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + + #[test] + fn encrypt_and_decrypt() { + let plain = [1; PKCS_LEN]; + let encrypted_key = EncryptedKey::encrypt(&plain, "thepassword", KEY_ITERATIONS as u32); + + let decrypted_key = encrypted_key.decrypt("thepassword").unwrap(); + + assert_eq!(&plain[..], &decrypted_key[..]); + } + + #[test] + fn decrypt_wrong_password_fails() { + let plain = [1; PKCS_LEN]; + let encrypted_key = EncryptedKey::encrypt(&plain, "thepassword", KEY_ITERATIONS as u32); + + assert!(encrypted_key.decrypt("thepassword2").is_err()); + } + + #[test] + fn decrypt_wrong_iterations_fails() { + let plain = [1; PKCS_LEN]; + let mut encrypted_key = EncryptedKey::encrypt(&plain, "thepassword", KEY_ITERATIONS as u32); + + encrypted_key.iterations -= 64; + + assert!(encrypted_key.decrypt("thepassword").is_err()); + } + + #[test] + fn basic_store() { + let temp_dir = TempDir::new("keystore").unwrap(); + let store = Store::open(temp_dir.path().to_owned()).unwrap(); + + assert!(store.contents().unwrap().is_empty()); + + let key = store.generate("thepassword").unwrap(); + let key2 = store.load(&key.public(), "thepassword").unwrap(); + + assert!(store.load(&key.public(), "notthepassword").is_err()); + + assert_eq!(key.public(), key2.public()); + + assert_eq!(store.contents().unwrap()[0], key.public()); + } +}