diff --git a/CHANGELOG.md b/CHANGELOG.md index 4970d86435..0d642c41d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- Added command `hc keygen` which creates a new key pair, asks for a passphrase and writes an encrypted key bundle file to `~/.holochain/keys`. - `hash` properties for `UiBundleConfiguration` and `DnaConfiguration` in Conductor config files is now optional - core now depends on `pretty_assertions` crate - `ChainHeader::sources()` is now `ChainHeader::provenances()` diff --git a/Cargo.toml b/Cargo.toml index c6ea0c5fe7..311093c432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "cli", + "common", "conductor", "conductor_api", "core_api_c_binding", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a88a4dfd44..74f0b34dea 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -5,10 +5,13 @@ authors = ["Holochain Core Dev Team "] [dependencies] holochain_net = { path = "../net" } +holochain_common = { path = "../common" } holochain_core_types = { path = "../core_types" } holochain_core = { path = "../core" } holochain_cas_implementations = { path = "../cas_implementations" } holochain_conductor_api = { path = "../conductor_api" } +holochain_dpki = { path = "../hc_dpki" } +holochain_sodium = { path = "../sodium" } holochain_wasm_utils = { path = "../wasm_utils" } structopt = "0.2" failure = "^0.1" @@ -25,3 +28,5 @@ dir-diff = "0.3.1" colored = "1.6" ignore = "0.4.3" rustyline = "^2.1" +rpassword = "2.1.0" +directories = "1.0" \ No newline at end of file diff --git a/cli/README.md b/cli/README.md index 7021bf1d5c..d0d6fb4fb0 100644 --- a/cli/README.md +++ b/cli/README.md @@ -69,6 +69,7 @@ If you want to use `hc run` with real (as opposed to mock) networking, you will | unpack | Unpacks a Holochain bundle into its original file system structure | | test | Runs tests written in the test folder | | run | Starts a websocket server for the current Holochain app | +| keygen | Creates a new passphrase encrypted agent key bundle | | agent (u) | Starts a Holochain node as an agent | ### hc init & hc generate: How To Get Started Building An App @@ -89,6 +90,11 @@ To read about `hc test`, used for running tests over your source code, see [http To read about `hc run`, used for spinning up a quick developement version of your app with an HTTP or Websocket interface, that you can connect to from a UI, or any client, see [https://developer.holochain.org/guide/latest/development_conductor.html](https://developer.holochain.org/guide/latest/development_conductor.html). +### hc keygen: Create agent key pair + +Every agent is represented by a private/public key pair, which are used to author source chains. +This command creates a new key pair by asking for a passphrase and writing a key bundle file that a Holochain Conductor +can read when starting up an instance. ## Contribute Holochain is an open source project. We welcome all sorts of participation and are actively working on increasing surface area to accept it. Please see our [contributing guidelines](../CONTRIBUTING.md) for our general practices and protocols on participating in the community. diff --git a/cli/src/cli/keygen.rs b/cli/src/cli/keygen.rs new file mode 100644 index 0000000000..aac1d67466 --- /dev/null +++ b/cli/src/cli/keygen.rs @@ -0,0 +1,92 @@ +use error::DefaultResult; +use holochain_common::paths::keys_directory; +use holochain_dpki::{ + bundle::KeyBundle, + keypair::{Keypair, SEEDSIZE}, + util::PwHashConfig, +}; +use holochain_sodium::{pwhash, random::random_secbuf, secbuf::SecBuf}; +use rpassword; +use std::{ + fs::{create_dir_all, File}, + io::prelude::*, + path::PathBuf, +}; + +pub fn keygen(path: Option, passphrase: Option) -> DefaultResult<()> { + let passphrase = passphrase + .unwrap_or_else(|| rpassword::read_password_from_tty(Some("Passphrase: ")).unwrap()); + + let mut seed = SecBuf::with_secure(SEEDSIZE); + random_secbuf(&mut seed); + let mut keypair = Keypair::new_from_seed(&mut seed).unwrap(); + let passphrase_bytes = passphrase.as_bytes(); + let mut passphrase_buf = SecBuf::with_insecure(passphrase_bytes.len()); + passphrase_buf + .write(0, passphrase_bytes) + .expect("SecBuf must be writeable"); + + let bundle: KeyBundle = keypair + .get_bundle( + &mut passphrase_buf, + "hint".to_string(), + Some(PwHashConfig( + pwhash::OPSLIMIT_INTERACTIVE, + pwhash::MEMLIMIT_INTERACTIVE, + pwhash::ALG_ARGON2ID13, + )), + ) + .unwrap(); + + let path = if None == path { + let p = keys_directory(); + create_dir_all(p.clone())?; + p.join(keypair.pub_keys.clone()) + } else { + path.unwrap() + }; + + let mut file = File::create(path.clone())?; + file.write_all(serde_json::to_string(&bundle).unwrap().as_bytes())?; + println!("Agent keys with public address: {}", keypair.pub_keys); + println!("written to: {}.", path.to_str().unwrap()); + Ok(()) +} + +#[cfg(test)] +pub mod test { + use super::*; + use holochain_dpki::bundle::KeyBundle; + use std::{ + fs::{remove_file, File}, + path::PathBuf, + }; + + #[test] + fn keygen_roundtrip() { + let path = PathBuf::new().join("test.key"); + let passphrase = String::from("secret"); + + keygen(Some(path.clone()), Some(passphrase.clone())).expect("Keygen should work"); + + let mut file = File::open(path.clone()).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + + let bundle: KeyBundle = serde_json::from_str(&contents).unwrap(); + let mut passphrase = SecBuf::with_insecure_from_string(passphrase); + let keypair = Keypair::from_bundle( + &bundle, + &mut passphrase, + Some(PwHashConfig( + pwhash::OPSLIMIT_INTERACTIVE, + pwhash::MEMLIMIT_INTERACTIVE, + pwhash::ALG_ARGON2ID13, + )), + ); + + assert!(keypair.is_ok()); + + let _ = remove_file(path); + } +} diff --git a/cli/src/cli/mod.rs b/cli/src/cli/mod.rs index d8fe0eb6d9..10c4f631e2 100644 --- a/cli/src/cli/mod.rs +++ b/cli/src/cli/mod.rs @@ -1,6 +1,7 @@ mod agent; mod generate; mod init; +mod keygen; pub mod package; mod run; mod scaffold; @@ -11,6 +12,7 @@ pub use self::{ agent::agent, generate::generate, init::init, + keygen::keygen, package::{package, unpack}, run::run, test::{test, TEST_DIR_NAME}, diff --git a/cli/src/main.rs b/cli/src/main.rs index 55b63760b5..634962bce7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,8 +1,11 @@ extern crate holochain_cas_implementations; +extern crate holochain_common; extern crate holochain_conductor_api; extern crate holochain_core; extern crate holochain_core_types; +extern crate holochain_dpki; extern crate holochain_net; +extern crate holochain_sodium; extern crate holochain_wasm_utils; extern crate structopt; #[macro_use] @@ -19,6 +22,7 @@ extern crate toml; #[macro_use] extern crate serde_json; extern crate ignore; +extern crate rpassword; extern crate rustyline; extern crate tempfile; extern crate uuid; @@ -142,6 +146,12 @@ enum Cli { #[structopt(long = "skip-package", short = "s", help = "Skip packaging DNA")] skip_build: bool, }, + #[structopt( + name = "keygen", + alias = "k", + about = "Creates a new agent key pair, asks for a passphrase and writes an encrypted key bundle to ~/.config/holochain/keys" + )] + KeyGen, } fn main() { @@ -183,6 +193,9 @@ fn run() -> HolochainResult<()> { cli::test(¤t_path, &dir, &testfile, skip_build) } .map_err(HolochainError::Default)?, + Cli::KeyGen => { + cli::keygen(None, None).map_err(|e| HolochainError::Default(format_err!("{}", e)))? + } } Ok(()) diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000000..8906c89d69 --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "holochain_common" +version = "0.0.3" +authors = ["Holochain Core Dev Team "] +edition = "2018" + +[dependencies] +directories = "1.0" \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000000..8118b2968e --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1 @@ +pub mod paths; diff --git a/common/src/paths.rs b/common/src/paths.rs new file mode 100644 index 0000000000..3de7d21db3 --- /dev/null +++ b/common/src/paths.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; + +pub const QUALIFIER: &'static str = "org"; +pub const ORGANIZATION: &'static str = "holochain"; +pub const APPLICATION: &'static str = "holochain"; +pub const KEYS_DIRECTORY: &'static str = "keys"; + +/// Returns the path to the root config directory for all of Holochain. +/// If we can get a user directory it will be an XDG compliant path +/// like "/home/peter/.config/holochain". +/// If it can't get a user directory it will default to "/etc/holochain". +pub fn config_root() -> PathBuf { + directories::ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) + .map(|dirs| dirs.config_dir().to_owned()) + .unwrap_or_else(|| PathBuf::from("/etc").join(APPLICATION)) +} + +/// Returns the path to where agent keys are stored and looked for by default. +/// Something like "~/.config/holochain/keys". +pub fn keys_directory() -> PathBuf { + config_root().join(KEYS_DIRECTORY) +} diff --git a/hc_dpki/src/bundle.rs b/hc_dpki/src/bundle.rs index 19555b6619..2024f9e798 100644 --- a/hc_dpki/src/bundle.rs +++ b/hc_dpki/src/bundle.rs @@ -3,6 +3,7 @@ /// The bundle_type tells if the bundle is a RootSeed bundle | DeviceSeed bundle | DevicePINSeed Bundle | ApplicationKeys Bundle /// /// the data includes a base64 encoded string of the ReturnBundleData Struct that was created by combining all the keys in one SecBuf +#[derive(Serialize, Deserialize)] pub struct KeyBundle { pub bundle_type: String, pub hint: String, diff --git a/hc_dpki/src/lib.rs b/hc_dpki/src/lib.rs index fd4f684e2b..4896eb14ff 100644 --- a/hc_dpki/src/lib.rs +++ b/hc_dpki/src/lib.rs @@ -4,10 +4,11 @@ extern crate holochain_sodium; #[macro_use] extern crate arrayref; extern crate base64; -extern crate rustc_serialize; - extern crate bip39; extern crate boolinator; +extern crate rustc_serialize; +#[macro_use] +extern crate serde; pub mod bundle; pub mod error;