diff --git a/crates/cli/src/cli/dpki.rs b/crates/cli/src/cli/dpki.rs new file mode 100644 index 0000000000..fdae021fda --- /dev/null +++ b/crates/cli/src/cli/dpki.rs @@ -0,0 +1,486 @@ +use crate::{ + cli::keygen, + util::{self, get_secure_string_double_check, user_prompt, user_prompt_yes_no, WordCountable}, +}; +use holochain_core_types::error::HcResult; +use holochain_dpki::{ + key_bundle::KeyBundle, + keypair::KeyPair, + seed::{MnemonicableSeed, RootSeed, SeedTrait, SeedType, TypedSeed}, + utils::generate_random_seed_buf, +}; +use lib3h_sodium::secbuf::SecBuf; +use std::{path::PathBuf, str::FromStr, string::ParseError}; +use structopt::StructOpt; + +const MNEMONIC_WORD_COUNT: usize = 24; +const ENCRYPTED_MNEMONIC_WORD_COUNT: usize = 2 * MNEMONIC_WORD_COUNT; + +const DEFAULT_REVOCATION_KEY_DEV_INDEX: u64 = 1; +const DEFAULT_AUTH_KEY_DEV_INDEX: u64 = 1; + +pub enum SignType { + Revoke, + Auth, +} + +impl FromStr for SignType { + type Err = ParseError; + fn from_str(day: &str) -> Result { + match day { + "revoke" => Ok(SignType::Revoke), + "auth" => Ok(SignType::Auth), + _ => panic!(), + } + } +} + +#[derive(StructOpt)] +pub enum Dpki { + #[structopt( + name = "genroot", + about = "Generate a new random DPKI root seed. This is encrypyed with a passphrase and printed in BIP39 mnemonic form to stdout. Both the passphrase and mnemonic should be recorded and kept safe to be used later for key management." + )] + GenRoot { + passphrase: Option, + quiet: bool, + }, + + #[structopt( + name = "keygen", + about = "Identical to `hc keygen` but derives agent key pair from a DPKI root seed at a given derivation index. This allows the keys to be recovered provided the root seed is known." + )] + Keygen { + #[structopt(long, short, help = "Specify path of file")] + path: Option, + + #[structopt( + long, + short, + help = "Only print machine-readable output; intended for use by programs and scripts" + )] + quiet: bool, + + #[structopt( + long, + short, + help = "Set passphrase via argument and don't prompt for it (not reccomended)" + )] + keystore_passphrase: Option, + + #[structopt( + long, + short, + help = "Use insecure, hard-wired passphrase for testing and Don't ask for passphrase" + )] + nullpass: bool, + + #[structopt( + long, + short, + help = "Set root seed via argument and don't prompt for it (not reccomended). BIP39 mnemonic encoded root seed to derive device seed and agent key from" + )] + root_seed: Option, + + #[structopt( + long, + short, + help = "Set mnemonic passphrase via argument and don't prompt for it (not reccomended)" + )] + mnemonic_passphrase: Option, + + #[structopt(help = "Derive device seed from root seed with this index")] + device_derivation_index: u64, + }, + + #[structopt( + name = "genrevoke", + about = "Generate a revocation seed given an encrypted root seed mnemonic, passphrase and derivation index." + )] + GenRevoke { + #[structopt(help = "Derive revocation seed from root seed with this index")] + derivation_index: u64, + + #[structopt( + long, + short, + help = "unsecurely pass passphrase to decrypt root seed (not reccomended). Will prompt if encrypted seed provided." + )] + root_seed_passphrase: Option, + + #[structopt( + long, + short, + help = "unsecurely pass passphrase to encrypt revocation seed (not reccomended)." + )] + revocation_seed_passphrase: Option, + + #[structopt( + long, + short, + help = "Only print machine-readable output; intended for use by programs and scripts" + )] + quiet: bool, + }, + + #[structopt( + name = "genauth", + about = "Generate an auth seed given an encrypted root seed mnemonic, passphrase and derivation index." + )] + GenAuth { + #[structopt(help = "Derive auth seed from root seed with this index")] + derivation_index: u64, + + #[structopt( + long, + short, + help = "unsecurely pass passphrase to decrypt root seed (not reccomended). Will prompt if encrypted seed provided." + )] + root_seed_passphrase: Option, + + #[structopt( + long, + short, + help = "unsecurely pass passphrase to encrypt auth seed (not reccomended)." + )] + auth_seed_passphrase: Option, + + #[structopt( + long, + short, + help = "Only print machine-readable output; intended for use by programs and scripts" + )] + quiet: bool, + }, + + #[structopt( + name = "sign", + about = "Produce the signed string needed to revoke a key given a revocation seed mnemonic and passphrase." + )] + Sign { + #[structopt( + help = "Public key to revoke/authorize (or any other string you want to sign with an auth/revocation key)" + )] + key: String, + + #[structopt( + long, + short, + help = "unsecurely pass passphrase to decrypt revocation seed (not reccomended). Will prompt if encrypted seed provided." + )] + passphrase: Option, + + #[structopt( + long, + short, + help = "Only print machine-readable output; intended for use by programs and scripts" + )] + quiet: bool, + + #[structopt(long, short, help = "How to interpred seed (revoke/auth)")] + sign_type: SignType, + }, +} + +impl Dpki { + pub fn execute(self) -> HcResult { + match self { + Self::GenRoot { passphrase, quiet } => genroot(passphrase, quiet), + Self::Keygen { + path, + keystore_passphrase, + nullpass, + quiet, + root_seed, + mnemonic_passphrase, + device_derivation_index, + } => keygen( + path, + keystore_passphrase, + nullpass, + mnemonic_passphrase, + root_seed, + Some(device_derivation_index), + quiet, + ) + .map(|_| "success".to_string()), + Self::GenRevoke { + derivation_index, + root_seed_passphrase, + revocation_seed_passphrase, + quiet, + } => genrevoke( + root_seed_passphrase, + revocation_seed_passphrase, + derivation_index, + quiet, + ), + Self::GenAuth { + derivation_index, + root_seed_passphrase, + auth_seed_passphrase, + quiet, + } => genauth( + root_seed_passphrase, + auth_seed_passphrase, + derivation_index, + quiet, + ), + Self::Sign { + passphrase, + key, + sign_type, + quiet, + } => sign(passphrase, key, sign_type, quiet), + } + } +} + +fn genroot(passphrase: Option, quiet: bool) -> HcResult { + user_prompt( + "This will generate a new random DPKI root seed. +You should only have to do this once and you should keep the seed safe. +It will be printed out once as a mnemonic at the end of this process. +The root seed can be used to generate new device, revocation and auth keys.\n", + quiet, + ); + + let passphrase = passphrase.or_else(|| { + match user_prompt_yes_no("Would you like to encrypt the root seed?", quiet) { + true => Some( + get_secure_string_double_check("Root Seed Passphrase", quiet) + .expect("Could not read revocation passphrase"), + ), + false => None, + } + }); + println!(); + genroot_inner(passphrase) +} + +pub(crate) fn genroot_inner(passphrase: Option) -> HcResult { + let seed_buf = generate_random_seed_buf(); + let mut root_seed = RootSeed::new(seed_buf); + match passphrase { + Some(passphrase) => root_seed.encrypt(passphrase, None)?.get_mnemonic(), + None => root_seed.seed_mut().get_mnemonic(), + } +} + +fn genrevoke( + root_seed_passphrase: Option, + revocation_seed_passphrase: Option, + derivation_index: u64, + quiet: bool, +) -> HcResult { + user_prompt( + "This will generate a new revocation seed derived from a root seed. +This can be used to revoke access to keys you have previously authorized.\n", + quiet, + ); + + let root_seed_mnemonic = get_secure_string_double_check("Root Seed", quiet)?; + let root_seed_passphrase = match root_seed_mnemonic.word_count() { + MNEMONIC_WORD_COUNT => None, // ignore any passphrase passed if it is an unencrypted mnemonic + ENCRYPTED_MNEMONIC_WORD_COUNT => root_seed_passphrase.or_else(|| { + Some( + get_secure_string_double_check("Root Seed Passphrase", quiet) + .expect("Could not read passphrase"), + ) + }), + _ => panic!("Invalid word count for mnemonic"), + }; + let revocation_seed_passphrase = + revocation_seed_passphrase.or_else(|| { + match user_prompt_yes_no("Would you like to encrypt the revocation seed?", quiet) { + true => Some( + get_secure_string_double_check("Revocation Seed Passphrase", quiet) + .expect("Could not read revocation passphrase"), + ), + false => None, + } + }); + println!(); + let (mnemonic, pubkey) = genauth_genrevoke_inner( + root_seed_mnemonic, + root_seed_passphrase, + revocation_seed_passphrase, + derivation_index, + SignType::Revoke, + )?; + Ok(format!("Public Key: {}\n\nMnemonic: {}", pubkey, mnemonic)) +} + +fn genauth( + root_seed_passphrase: Option, + auth_seed_passphrase: Option, + derivation_index: u64, + quiet: bool, +) -> HcResult { + user_prompt( + "This will generate a new authorization seed derived from a root seed. +This can be used to authorize new keys in DPKI.\n", + quiet, + ); + + let root_seed_mnemonic = get_secure_string_double_check("Root Seed", quiet)?; + let root_seed_passphrase = match root_seed_mnemonic.word_count() { + MNEMONIC_WORD_COUNT => None, // ignore any passphrase passed if it is an unencrypted mnemonic + ENCRYPTED_MNEMONIC_WORD_COUNT => root_seed_passphrase.or_else(|| { + Some( + get_secure_string_double_check("Root Seed Passphrase", quiet) + .expect("Could not read passphrase"), + ) + }), + _ => panic!("Invalid word count for mnemonic"), + }; + let auth_seed_passphrase = auth_seed_passphrase.or_else(|| { + match user_prompt_yes_no("Would you like to encrypt the auth seed?", quiet) { + true => Some( + get_secure_string_double_check("Auth Seed Passphrase", quiet) + .expect("Could not read auth passphrase"), + ), + false => None, + } + }); + println!(); + let (mnemonic, pubkey) = genauth_genrevoke_inner( + root_seed_mnemonic, + root_seed_passphrase, + auth_seed_passphrase, + derivation_index, + SignType::Auth, + )?; + Ok(format!("Public Key: {}\n\nMnemonic: {}", pubkey, mnemonic)) +} + +fn genauth_genrevoke_inner( + root_seed_mnemonic: String, + root_seed_passphrase: Option, + new_seed_passphrase: Option, + derivation_index: u64, + key_type: SignType, +) -> HcResult<(String, String)> { + let mut root_seed = + match util::get_seed(root_seed_mnemonic, root_seed_passphrase, SeedType::Root)? { + TypedSeed::Root(s) => s, + _ => unreachable!(), + }; + + match key_type { + SignType::Revoke => { + let mut revocation_seed = root_seed.generate_revocation_seed(derivation_index)?; + let pubkey = revocation_seed + .generate_revocation_key(DEFAULT_REVOCATION_KEY_DEV_INDEX)? + .sign_keys + .public(); + match new_seed_passphrase { + Some(passphrase) => Ok(( + revocation_seed.encrypt(passphrase, None)?.get_mnemonic()?, + pubkey, + )), + None => Ok((revocation_seed.seed_mut().get_mnemonic()?, pubkey)), + } + } + SignType::Auth => { + // TODO: Allow different derivation paths for the auth key + let mut auth_seed = root_seed + .generate_device_seed(derivation_index)? + .generate_auth_seed(1)?; + let pubkey = auth_seed + .generate_auth_key(DEFAULT_AUTH_KEY_DEV_INDEX)? + .sign_keys + .public(); + match new_seed_passphrase { + Some(passphrase) => { + Ok((auth_seed.encrypt(passphrase, None)?.get_mnemonic()?, pubkey)) + } + None => Ok((auth_seed.seed_mut().get_mnemonic()?, pubkey)), + } + } + } +} + +fn sign( + passphrase: Option, + key_string: String, + sign_type: SignType, + quiet: bool, +) -> HcResult { + user_prompt("This will sign a given key/string with a auth/revocation key. +The resulting signed message can be used to publish a DPKI auth/revocation message which will auth/revoke a key.\n", quiet); + + let seed_mnemonic = get_secure_string_double_check("Seed", false)?; + let passphrase = match seed_mnemonic.word_count() { + MNEMONIC_WORD_COUNT => None, // ignore any passphrase passed if it is an unencrypted mnemonic + ENCRYPTED_MNEMONIC_WORD_COUNT => passphrase.or_else(|| { + Some( + get_secure_string_double_check("Seed Passphrase", quiet) + .expect("Could not read passphrase"), + ) + }), + _ => panic!("Invalid word count for mnemonic"), + }; + println!(); + sign_inner(seed_mnemonic, passphrase, key_string, sign_type) +} + +fn sign_inner( + seed_mnemonic: String, + passphrase: Option, + key_string: String, + sign_type: SignType, +) -> HcResult { + let mut keypair = match sign_type { + SignType::Revoke => { + let mut revocation_seed = + match util::get_seed(seed_mnemonic, passphrase, SeedType::Revocation)? { + TypedSeed::Revocation(s) => s, + _ => unreachable!(), + }; + revocation_seed.generate_revocation_key(DEFAULT_REVOCATION_KEY_DEV_INDEX)? + } + SignType::Auth => { + let mut auth_seed = match util::get_seed(seed_mnemonic, passphrase, SeedType::Auth)? { + TypedSeed::Auth(s) => s, + _ => unreachable!(), + }; + auth_seed.generate_auth_key(DEFAULT_AUTH_KEY_DEV_INDEX)? + } + }; + sign_with_key_from_seed(&mut keypair, key_string) +} + +fn sign_with_key_from_seed(keypair: &mut KeyBundle, key_string: String) -> HcResult { + let mut data_buf = SecBuf::with_insecure_from_string(key_string); + let mut signature_buf = keypair.sign(&mut data_buf)?; + let buf = signature_buf.read_lock(); + let signature_str = base64::encode(&**buf); + Ok(signature_str) +} + +#[cfg(test)] +pub mod tests { + + use super::*; + use holochain_core_types::signature::{Provenance, Signature}; + use holochain_dpki::{keypair::*, utils::Verify}; + use holochain_persistence_api::cas::content::Address; + + #[test] + fn can_verify_signature() { + let payload = "test signing payload"; + let mut seed = generate_random_seed_buf(); + let sign_keys = SigningKeyPair::new_from_seed(&mut seed).unwrap(); + let enc_keys = EncryptingKeyPair::new_from_seed(&mut seed).unwrap(); + let mut key_bundle = KeyBundle::new(sign_keys, enc_keys).unwrap(); + + let sig = sign_with_key_from_seed(&mut key_bundle, payload.to_string()).unwrap(); + + let prov = Provenance::new( + Address::from(key_bundle.sign_keys.public), + Signature::from(sig), + ); + assert!(prov.verify(payload.to_string()).unwrap()); + } +} diff --git a/crates/cli/src/cli/keygen.rs b/crates/cli/src/cli/keygen.rs index 696186a9e8..d4c2f960d0 100644 --- a/crates/cli/src/cli/keygen.rs +++ b/crates/cli/src/cli/keygen.rs @@ -1,47 +1,106 @@ -use error::DefaultResult; use holochain_common::paths::keys_directory; -use holochain_conductor_lib::{key_loaders::mock_passphrase_manager, keystore::Keystore}; -use rpassword; +use holochain_conductor_lib::{ + key_loaders::mock_passphrase_manager, + keystore::{Keystore, PRIMARY_KEYBUNDLE_ID}, +}; +use holochain_core_types::error::HcResult; +use holochain_dpki::seed::{SeedType, TypedSeed}; +use holochain_locksmith::Mutex; use std::{ fs::create_dir_all, io::{self, Write}, path::PathBuf, + sync::Arc, }; +use util::{get_secure_string_double_check, get_seed, user_prompt}; -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_CLI)] -pub fn keygen(path: Option, passphrase: Option, quiet: bool) -> DefaultResult<()> { - let passphrase = passphrase.unwrap_or_else(|| { - if !quiet { - println!( - " -This will create a new agent keystore and populate it with an agent keybundle +pub fn keygen( + path: Option, + keystore_passphrase: Option, + nullpass: bool, + mnemonic_passphrase: Option, + root_seed_mnemonic: Option, + device_derivation_index: Option, + quiet: bool, +) -> HcResult<()> { + user_prompt( + "This will create a new agent keystore and populate it with an agent keybundle containing a public and a private key, for signing and encryption by the agent. This keybundle will be stored encrypted by passphrase within the keystore file. The passphrase is securing the keys and will be needed, together with the file, -in order to use the key. -Please enter a secret passphrase below. You will have to enter it again -when unlocking the keybundle to use within a Holochain conductor." +in order to use the key.\n", + quiet, + ); + + let keystore_passphrase = match (keystore_passphrase, nullpass) { + (None, true) => String::from(holochain_common::DEFAULT_PASSPHRASE), + (Some(s), false) => s, + (Some(_), true) => panic!( + "Invalid combination of args. Cannot pass --nullpass and also provide a passphrase" + ), + (None, false) => { + // prompt for the passphrase + user_prompt( + "Please enter a secret passphrase below. You will have to enter it again +when unlocking the keybundle to use within a Holochain conductor.\n", + quiet, ); - print!("Passphrase: "); - io::stdout().flush().expect("Could not flush stdout"); - } - let passphrase1 = rpassword::read_password().unwrap(); - if !quiet { - print!("Re-enter passphrase: "); io::stdout().flush().expect("Could not flush stdout"); + get_secure_string_double_check("keystore Passphrase", quiet) + .expect("Could not retrieve passphrase") } - let passphrase2 = rpassword::read_password().unwrap(); - if passphrase1 != passphrase2 { - println!("Passphrases do not match. Please retry..."); - ::std::process::exit(1); - } - passphrase1 - }); + }; - if !quiet { - println!("Generating keystore (this will take a few moments)..."); - } - let (keystore, pub_key) = Keystore::new_standalone(mock_passphrase_manager(passphrase), None)?; + let (keystore, pub_key) = if let Some(derivation_index) = device_derivation_index { + user_prompt("This keystore is to be generated from a DPKI root seed. You can regenerate this keystore at any time by using the same root key mnemonic and device derivation index.", quiet); + + let root_seed_mnemonic = root_seed_mnemonic.unwrap_or_else(|| { + get_secure_string_double_check("Root Seed Mnemonic", quiet) + .expect("Could not retrieve mnemonic") + }); + + match root_seed_mnemonic.split(' ').count() { + 24 => { + // unencrypted mnemonic + user_prompt( + "Generating keystore (this will take a few moments)...", + quiet, + ); + keygen_dpki( + root_seed_mnemonic, + None, + derivation_index, + keystore_passphrase, + )? + } + 48 => { + // encrypted mnemonic + let mnemonic_passphrase = mnemonic_passphrase.unwrap_or_else(|| { + get_secure_string_double_check("Root Seed Mnemonic passphrase", quiet) + .expect("Could not retrieve mnemonic passphrase") + }); + user_prompt( + "Generating keystore (this will take a few moments)...", + quiet, + ); + keygen_dpki( + root_seed_mnemonic, + Some(mnemonic_passphrase), + derivation_index, + keystore_passphrase, + )? + } + _ => panic!( + "Invalid number of words in mnemonic. Must be 24 (unencrypted) or 48 (encrypted)" + ), + } + } else { + user_prompt( + "Generating keystore (this will take a few moments)...", + quiet, + ); + keygen_standalone(keystore_passphrase)? + }; let path = if None == path { let p = keys_directory(); @@ -69,9 +128,31 @@ when unlocking the keybundle to use within a Holochain conductor." Ok(()) } +fn keygen_standalone(keystore_passphrase: String) -> HcResult<(Keystore, String)> { + Keystore::new_standalone(mock_passphrase_manager(keystore_passphrase), None) +} + +fn keygen_dpki( + root_seed_mnemonic: String, + root_seed_passphrase: Option, + derivation_index: u64, + keystore_passphrase: String, +) -> HcResult<(Keystore, String)> { + let mut root_seed = match get_seed(root_seed_mnemonic, root_seed_passphrase, SeedType::Root)? { + TypedSeed::Root(s) => s, + _ => unreachable!(), + }; + let mut keystore = Keystore::new(mock_passphrase_manager(keystore_passphrase), None)?; + let device_seed = root_seed.generate_device_seed(derivation_index)?; + keystore.add("device_seed", Arc::new(Mutex::new(device_seed.into())))?; + let (pub_key, _) = keystore.add_keybundle_from_seed("device_seed", PRIMARY_KEYBUNDLE_ID)?; + Ok((keystore, pub_key)) +} + #[cfg(test)] pub mod test { use super::*; + use cli::dpki; use holochain_conductor_lib::{ key_loaders::mock_passphrase_manager, keystore::{Keystore, PRIMARY_KEYBUNDLE_ID}, @@ -79,11 +160,20 @@ pub mod test { use std::{fs::remove_file, path::PathBuf}; #[test] - fn keygen_roundtrip() { + fn keygen_roundtrip_no_dpki() { let path = PathBuf::new().join("test.key"); let passphrase = String::from("secret"); - keygen(Some(path.clone()), Some(passphrase.clone()), true).expect("Keygen should work"); + keygen( + Some(path.clone()), + Some(passphrase.clone()), + false, + None, + None, + None, + true, + ) + .expect("Keygen should work"); let mut keystore = Keystore::new_from_file(path.clone(), mock_passphrase_manager(passphrase), None) @@ -95,4 +185,38 @@ pub mod test { let _ = remove_file(path); } + + #[test] + fn keygen_roundtrip_with_dpki() { + let path = PathBuf::new().join("test_dpki.key"); + let keystore_passphrase = String::from("secret_dpki"); + let mnemonic_passphrase = String::from("dummy passphrase"); + + let mnemonic = dpki::genroot_inner(Some(mnemonic_passphrase.clone())) + .expect("Could not generate root seed mneomonic"); + + keygen( + Some(path.clone()), + Some(keystore_passphrase.clone()), + false, + Some(mnemonic_passphrase), + Some(mnemonic), + Some(1), + true, + ) + .expect("Keygen should work"); + + let mut keystore = Keystore::new_from_file( + path.clone(), + mock_passphrase_manager(keystore_passphrase), + None, + ) + .unwrap(); + + let keybundle = keystore.get_keybundle(PRIMARY_KEYBUNDLE_ID); + + assert!(keybundle.is_ok()); + + let _ = remove_file(path); + } } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index f606459e65..532d418aad 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,4 +1,5 @@ mod chain_log; +mod dpki; mod generate; mod hash_dna; pub mod init; @@ -10,6 +11,7 @@ pub mod test; pub use self::{ chain_log::{chain_list, chain_log}, + dpki::Dpki, generate::generate, hash_dna::hash_dna, init::init, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index b01bce5ba5..8ae4b543af 100755 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -27,6 +27,7 @@ extern crate serde_json; extern crate dns_lookup; extern crate flate2; extern crate glob; +extern crate holochain_dpki; extern crate ignore; extern crate in_stream; extern crate rpassword; @@ -40,7 +41,10 @@ mod config_files; mod error; mod util; -use crate::error::{HolochainError, HolochainResult}; +use crate::{ + cli::Dpki, + error::{HolochainError, HolochainResult}, +}; use holochain_conductor_lib::happ_bundle::HappBundle; use std::{fs::File, io::Read, path::PathBuf, str::FromStr}; use structopt::{clap::arg_enum, StructOpt}; @@ -129,10 +133,25 @@ enum Cli { #[structopt(long, short)] /// Only print machine-readable output; intended for use by programs and scripts quiet: bool, - #[structopt(long, short)] + #[structopt( + long, + short, + help = "Use insecure, hard-wired passphrase for testing and Don't ask for passphrase" + )] /// Don't ask for passphrase nullpass: bool, + #[structopt( + long, + short, + help = "Set passphrase via argument and don't prompt for it (not reccomended)" + )] + passphrase: Option, }, + #[structopt( + name = "dpki-init", + alias = "d", + about = "Generates a new DPKI root seed and outputs the encrypted key as a BIP39 mnemonic" + )] #[structopt(name = "chain")] /// View the contents of a source chain ChainLog { @@ -156,6 +175,12 @@ enum Cli { /// Property (in the form 'name=value') that gets set/overwritten before calculating hash property: Option>, }, + #[structopt( + name = "dpki", + alias = "d", + about = "Operations to manage keys for DPKI" + )] + Dpki(Dpki), Sim2hClient { #[structopt(long, short = "u")] /// url of the sim2h server @@ -294,15 +319,14 @@ fn run() -> HolochainResult<()> { path, quiet, nullpass, - } => { - let passphrase = if nullpass { - Some(String::from(holochain_common::DEFAULT_PASSPHRASE)) - } else { - None - }; - cli::keygen(path, passphrase, quiet) - .map_err(|e| HolochainError::Default(format_err!("{}", e)))? - } + passphrase, + } => cli::keygen(path, passphrase, nullpass, None, None, None, quiet) + .map_err(|e| HolochainError::Default(format_err!("{}", e)))?, + + Cli::Dpki(dpki) => dpki + .execute() + .map(|result| println!("{}", result)) + .map_err(|e| HolochainError::Default(format_err!("{}", e)))?, Cli::ChainLog { instance_id, diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index b8ba23d905..17c420fd4c 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -1,13 +1,78 @@ use crate::error::DefaultResult; use colored::*; pub use holochain_common::paths::DNA_EXTENSION; +use holochain_core_types::error::HcResult; +use holochain_dpki::seed::{EncryptedSeed, MnemonicableSeed, Seed, SeedType, TypedSeed}; +use rpassword; use std::{ fs, - io::ErrorKind, + io::{self, stdin, ErrorKind, Write}, path::PathBuf, process::{Command, Stdio}, }; +pub fn get_secure_string_double_check(name: &str, quiet: bool) -> HcResult { + if !quiet { + print!("Enter {}: ", name); + io::stdout().flush()?; + } + let retrieved_str_1 = rpassword::read_password()?; + if !quiet { + print!("Re-enter {}: ", name); + io::stdout().flush()?; + } + let retrieved_str_2 = rpassword::read_password()?; + if retrieved_str_1 != retrieved_str_2 { + panic!("Root seeds do not match. Aborting"); + } + Ok(retrieved_str_1) +} + +pub fn user_prompt(message: &str, quiet: bool) { + if !quiet { + println!("{}", message); + } +} + +pub fn user_prompt_yes_no(message: &str, quiet: bool) -> bool { + user_prompt(format!("{} (Y/n)", message).as_str(), quiet); + let mut input = String::new(); + stdin() + .read_line(&mut input) + .expect("Could not read from stdin"); + match input.as_str() { + "Y\n" => true, + "n\n" => false, + _ => panic!(format!("Invalid response: {}", input)), + } +} + +/// Retrieve a seed from a BIP39 mnemonic +/// If a passphrase is provided assume it is encrypted and decrypt it +/// If not then assume it is unencrypted +pub fn get_seed( + seed_mnemonic: String, + passphrase: Option, + seed_type: SeedType, +) -> HcResult { + match passphrase { + Some(passphrase) => { + EncryptedSeed::new_with_mnemonic(seed_mnemonic, seed_type)?.decrypt(passphrase, None) + } + None => Seed::new_with_mnemonic(seed_mnemonic, seed_type)?.into_typed(), + } +} + +pub trait WordCountable { + fn word_count(&self) -> usize; +} + +impl WordCountable for String { + fn word_count(&self) -> usize { + self.split(' ').count() + } +} + #[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_CLI)] pub fn run_cmd(base_path: &PathBuf, bin: String, args: &[&str]) -> DefaultResult<()> { let pretty_command = format!("{} {}", bin.green(), args.join(" ").cyan()); diff --git a/crates/conductor_lib/src/keystore.rs b/crates/conductor_lib/src/keystore.rs index bde7a2e0f6..678c2337d5 100644 --- a/crates/conductor_lib/src/keystore.rs +++ b/crates/conductor_lib/src/keystore.rs @@ -7,7 +7,7 @@ use holochain_dpki::{ key_blob::{BlobType, Blobbable, KeyBlob}, key_bundle::KeyBundle, keypair::{EncryptingKeyPair, KeyPair, SigningKeyPair}, - seed::Seed, + seed::{Seed, SeedTrait}, utils::{ decrypt_with_passphrase_buf, encrypt_with_passphrase_buf, generate_derived_seed_buf, generate_random_buf, SeedContext, @@ -46,6 +46,12 @@ pub enum Secret { Seed(SecBuf), } +impl From for Secret { + fn from(s: S) -> Self { + Secret::Seed(s.seed().buf.clone()) + } +} + pub enum KeyType { Signing, Encrypting, @@ -668,7 +674,7 @@ pub mod tests { } #[test] - fn test_keystore_add_random_seed() { + fn test_keystore_add_seed_functions() { let mut keystore = new_test_keystore(random_test_passphrase()); assert_eq!(keystore.add_random_seed("my_root_seed", SEED_SIZE), Ok(())); @@ -679,6 +685,26 @@ pub mod tests { "identifier already exists".to_string() )) ); + // Confirm we can round-trip a specific seed value through the Keystore + let seed: [u8; SEED_SIZE] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + ]; + assert_eq!(keystore.add_seed("my_custom_seed", &seed), Ok(())); + assert_eq!( + keystore.list(), + vec!["my_custom_seed".to_string(), "my_root_seed".to_string(),] + ); + + let got_seed = match *keystore.get("my_custom_seed").unwrap().lock().unwrap() { + Secret::Seed(ref mut buf) => { + let lock = buf.read_lock(); + format!("{:?}", *lock) + } + _ => unreachable!(), + }; + assert_eq!(got_seed, + "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]"); } #[test] diff --git a/crates/core/src/nucleus/validation/agent_entry.rs b/crates/core/src/nucleus/validation/agent_entry.rs index 2f849ccaeb..7ad7a46325 100644 --- a/crates/core/src/nucleus/validation/agent_entry.rs +++ b/crates/core/src/nucleus/validation/agent_entry.rs @@ -14,7 +14,7 @@ use holochain_core_types::{ use holochain_persistence_api::cas::content::AddressableContent; use holochain_wasm_utils::api_serialization::validation::AgentIdValidationArgs; -use futures::{future, future::FutureExt}; +use futures::future::{self, FutureExt}; use std::sync::Arc; #[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_CORE)] diff --git a/crates/dpki/src/keypair.rs b/crates/dpki/src/keypair.rs index 5577ded098..96e086dcff 100755 --- a/crates/dpki/src/keypair.rs +++ b/crates/dpki/src/keypair.rs @@ -329,4 +329,28 @@ mod tests { let succeeded = sign_keys.verify(&mut message, &mut signature); assert!(!succeeded); } + + #[test] + fn keypair_should_generate_consistent_keys() { + let mut seed = SecBuf::with_insecure(32); + seed.from_array(&[ + 0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8, 10u8, 11u8, 12u8, 13u8, 14u8, 15u8, + 16u8, 17u8, 18u8, 19u8, 20u8, 21u8, 22u8, 23u8, 24u8, 25u8, 26u8, 27u8, 28u8, 29u8, + 30u8, 255u8, + ]) + .unwrap(); + let mut keypair = + SigningKeyPair::new_from_seed(&mut seed).expect("Failed to generate keypair"); + + assert_eq!( + keypair.public(), + "HcSciPgAEa7N4e6os7X7zK4JdbXnmxygkVVkHChDT3cbuh3wByfwzx9SNuo9xbz" + ); + + // ed25519 Private Keys + let pk = keypair.private().read_lock(); + assert_eq!(format!("{:?}", *pk), + "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 255, 56, 192, 32, 58, 205, 19, 141, 143, 109, 220, 43, 73, 24, 108, 197, 218, 230, 85, 40, 163, 136, 227, 150, 68, 25, 159, 53, 13, 203, 92, 91, 241]" + ); + } } diff --git a/crates/dpki/src/lib.rs b/crates/dpki/src/lib.rs index 725035c58a..bf80df65e5 100644 --- a/crates/dpki/src/lib.rs +++ b/crates/dpki/src/lib.rs @@ -12,6 +12,9 @@ extern crate holochain_common; pub const CONTEXT_SIZE: usize = 8; pub const SEED_SIZE: usize = 32; pub const AGENT_ID_CTX: [u8; 8] = *b"HCAGNTID"; +pub const DEVICE_CTX: [u8; 8] = *b"HCDEVICE"; +pub const REVOKE_CTX: [u8; 8] = *b"HCREVOKE"; +pub const AUTH_CTX: [u8; 8] = *b"HCAUTHRZ"; pub(crate) const SIGNATURE_SIZE: usize = 64; lazy_static! { diff --git a/crates/dpki/src/seed.rs b/crates/dpki/src/seed.rs index c97756dbdb..b7eb066a77 100644 --- a/crates/dpki/src/seed.rs +++ b/crates/dpki/src/seed.rs @@ -2,7 +2,7 @@ use crate::{ key_bundle::KeyBundle, password_encryption::*, utils::{generate_derived_seed_buf, SeedContext}, - AGENT_ID_CTX, SEED_SIZE, + AGENT_ID_CTX, AUTH_CTX, REVOKE_CTX, SEED_SIZE, }; use bip39::{Language, Mnemonic, MnemonicType}; use holochain_core_types::error::{HcResult, HolochainError}; @@ -31,6 +31,8 @@ pub enum SeedType { Root, /// Revocation seed Revocation, + /// Auth seed + Auth, /// Device seed Device, /// Derivative of a Device seed with a PIN @@ -48,6 +50,8 @@ pub enum TypedSeed { Root(RootSeed), Device(DeviceSeed), DevicePin(DevicePinSeed), + Revocation(RevocationSeed), + Auth(AuthSeed), } /// Common Trait for TypedSeeds @@ -69,12 +73,15 @@ pub trait SeedTrait { } } +/// Implement the API to create new Seeds from BIP39 Mnemonics, and to output various Seeds as BIP32 +/// Mnemonics. Some formats require mutability in order to perform this conversion; for example, a +/// Seed w/ a SecBuf will require it to be set to readable before its contents can be accessed. pub trait MnemonicableSeed where Self: Sized, { fn new_with_mnemonic(phrase: String, seed_type: SeedType) -> HcResult; - fn get_mnemonic(&mut self) -> HcResult; + fn get_mnemonic(&self) -> HcResult; } //-------------------------------------------------------------------------------------------------- @@ -88,7 +95,6 @@ pub struct Seed { pub buf: SecBuf, } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl Seed { pub fn new(seed_buf: SecBuf, seed_type: SeedType) -> Self { assert_eq!(seed_buf.len(), SEED_SIZE); @@ -114,8 +120,10 @@ impl Seed { SeedType::Root => Ok(TypedSeed::Root(RootSeed::new(self.buf))), SeedType::Device => Ok(TypedSeed::Device(DeviceSeed::new(self.buf))), SeedType::DevicePin => Ok(TypedSeed::DevicePin(DevicePinSeed::new(self.buf))), + SeedType::Revocation => Ok(TypedSeed::Revocation(RevocationSeed::new(self.buf))), + SeedType::Auth => Ok(TypedSeed::Auth(AuthSeed::new(self.buf))), _ => Err(HolochainError::ErrorGeneric( - "Seed does have specific behavior for its type".to_string(), + "Seed does not have specific behavior for its type".to_string(), )), } } @@ -141,8 +149,9 @@ impl MnemonicableSeed for Seed { /// Generate a mnemonic for the seed. // TODO: We need some way of zeroing the internal memory used by mnemonic - fn get_mnemonic(&mut self) -> HcResult { - let entropy = self.buf.read_lock(); + fn get_mnemonic(&self) -> HcResult { + let mut buf = self.buf.clone(); + let entropy = buf.read_lock(); let e = &*entropy; let mnemonic = Mnemonic::from_entropy(e, Language::English).map_err(|e| { HolochainError::ErrorGeneric(format!("Error generating Mnemonic phrase: {}", e)) @@ -160,7 +169,6 @@ pub struct RootSeed { inner: Seed, } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl SeedTrait for RootSeed { fn seed(&self) -> &Seed { &self.inner @@ -170,7 +178,6 @@ impl SeedTrait for RootSeed { } } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl RootSeed { /// Construct from a 32 bytes seed buffer pub fn new(seed_buf: SecBuf) -> Self { @@ -179,17 +186,23 @@ impl RootSeed { } } - /// Generate Device Seed + // Generate Device Seed /// @param {number} index - the index number in this seed group, must not be zero - pub fn generate_device_seed( - &mut self, - seed_context: &SeedContext, - index: u64, - ) -> HcResult { + pub fn generate_device_seed(&mut self, index: u64) -> HcResult { + let device_ctx = SeedContext::new(REVOKE_CTX); let device_seed_buf = - generate_derived_seed_buf(&mut self.inner.buf, seed_context, index, SEED_SIZE)?; + generate_derived_seed_buf(&mut self.inner.buf, &device_ctx, index, SEED_SIZE)?; Ok(DeviceSeed::new(device_seed_buf)) } + + /// Generate Revocation Seed + /// @param {number} index - the index number in this seed group, must not be zero + pub fn generate_revocation_seed(&mut self, index: u64) -> HcResult { + let seed_context = SeedContext::new(REVOKE_CTX); + let seed_buf = + generate_derived_seed_buf(&mut self.inner.buf, &seed_context, index, SEED_SIZE)?; + Ok(RevocationSeed::new(seed_buf)) + } } //-------------------------------------------------------------------------------------------------- @@ -201,7 +214,6 @@ pub struct DeviceSeed { inner: Seed, } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl SeedTrait for DeviceSeed { fn seed(&self) -> &Seed { &self.inner @@ -211,7 +223,6 @@ impl SeedTrait for DeviceSeed { } } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl DeviceSeed { /// Construct from a 32 bytes seed buffer pub fn new(seed_buf: SecBuf) -> Self { @@ -221,7 +232,7 @@ impl DeviceSeed { } /// generate a device pin seed by applying pwhash of pin with this seed as the salt - /// @param {string} pin - should be >= 4 characters 1-9 + /// @param {string} pin - should be >= 4 characters 0-9 /// @return {DevicePinSeed} Resulting Device Pin Seed pub fn generate_device_pin_seed( &mut self, @@ -232,6 +243,15 @@ impl DeviceSeed { pw_hash(pin, &mut self.inner.buf, &mut hash, config)?; Ok(DevicePinSeed::new(hash)) } + + /// Generate Auth Seed + /// @param {number} index - the index number in this seed group, must not be zero + pub fn generate_auth_seed(&mut self, index: u64) -> HcResult { + let seed_context = SeedContext::new(AUTH_CTX); + let seed_buf = + generate_derived_seed_buf(&mut self.inner.buf, &seed_context, index, SEED_SIZE)?; + Ok(AuthSeed::new(seed_buf)) + } } //-------------------------------------------------------------------------------------------------- @@ -243,7 +263,6 @@ pub struct DevicePinSeed { inner: Seed, } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl SeedTrait for DevicePinSeed { fn seed(&self) -> &Seed { &self.inner @@ -253,7 +272,6 @@ impl SeedTrait for DevicePinSeed { } } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl DevicePinSeed { /// Construct from a 32 bytes seed buffer pub fn new(seed_buf: SecBuf) -> Self { @@ -278,6 +296,91 @@ impl DevicePinSeed { } } +//-------------------------------------------------------------------------------------------------- +// Revocation Seed +//-------------------------------------------------------------------------------------------------- + +#[derive(Debug)] +pub struct RevocationSeed { + inner: Seed, +} + +impl SeedTrait for RevocationSeed { + fn seed(&self) -> &Seed { + &self.inner + } + fn seed_mut(&mut self) -> &mut Seed { + &mut self.inner + } +} + +impl RevocationSeed { + /// Construct from a 32 bytes seed buffer + pub fn new(seed_buf: SecBuf) -> Self { + RevocationSeed { + inner: Seed::new_with_initializer( + SeedInitializer::Seed(seed_buf), + SeedType::Revocation, + ), + } + } + + /// Generate a revocation key + pub fn generate_revocation_key(&mut self, derivation_index: u64) -> HcResult { + let mut ref_seed_buf = SecBuf::with_secure(SEED_SIZE); + let context = SeedContext::new(REVOKE_CTX); + let mut context = context.to_sec_buf(); + kdf::derive( + &mut ref_seed_buf, + derivation_index, + &mut context, + &mut self.inner.buf, + )?; + Ok(KeyBundle::new_from_seed_buf(&mut ref_seed_buf)?) + } +} + +//-------------------------------------------------------------------------------------------------- +// Auth Seed +//-------------------------------------------------------------------------------------------------- + +#[derive(Debug)] +pub struct AuthSeed { + inner: Seed, +} + +impl SeedTrait for AuthSeed { + fn seed(&self) -> &Seed { + &self.inner + } + fn seed_mut(&mut self) -> &mut Seed { + &mut self.inner + } +} + +impl AuthSeed { + /// Construct from a 32 bytes seed buffer + pub fn new(seed_buf: SecBuf) -> Self { + AuthSeed { + inner: Seed::new_with_initializer(SeedInitializer::Seed(seed_buf), SeedType::Auth), + } + } + + /// Generate a revocation key + pub fn generate_auth_key(&mut self, derivation_index: u64) -> HcResult { + let mut ref_seed_buf = SecBuf::with_secure(SEED_SIZE); + let context = SeedContext::new(AUTH_CTX); + let mut context = context.to_sec_buf(); + kdf::derive( + &mut ref_seed_buf, + derivation_index, + &mut context, + &mut self.inner.buf, + )?; + Ok(KeyBundle::new_from_seed_buf(&mut ref_seed_buf)?) + } +} + //-------------------------------------------------------------------------------------------------- // Encrypted Seed //-------------------------------------------------------------------------------------------------- @@ -287,7 +390,6 @@ pub struct EncryptedSeed { data: EncryptedData, } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl EncryptedSeed { fn new(data: EncryptedData, kind: SeedType) -> Self { Self { kind, data } @@ -308,7 +410,6 @@ impl EncryptedSeed { } } -#[holochain_tracing_macros::newrelic_autotrace(HOLOCHAIN_DPKI)] impl MnemonicableSeed for EncryptedSeed { fn new_with_mnemonic(phrase: String, seed_type: SeedType) -> HcResult { // split out the two phrases, decode then combine the bytes @@ -341,7 +442,7 @@ impl MnemonicableSeed for EncryptedSeed { /// Generate a mnemonic for the seed. /// Encrypted seeds produce a 48 word mnemonic as the encrypted output also contains auth bytes and salt bytes /// which adds an extra 32 bytes. This fits nicely into two 24 word BIP39 mnemonics. - fn get_mnemonic(&mut self) -> HcResult { + fn get_mnemonic(&self) -> HcResult { let bytes: Vec = self .data .cipher @@ -398,14 +499,13 @@ mod tests { #[test] fn it_should_create_a_device_seed() { let seed_buf = generate_random_seed_buf(); - let context = SeedContext::new(*b"HCDEVICE"); let mut root_seed = RootSeed::new(seed_buf); - let mut device_seed_3 = root_seed.generate_device_seed(&context, 3).unwrap(); + let mut device_seed_3 = root_seed.generate_device_seed(3).unwrap(); assert_eq!(SeedType::Device, device_seed_3.seed().kind); - let _ = root_seed.generate_device_seed(&context, 0).unwrap_err(); - let mut device_seed_1 = root_seed.generate_device_seed(&context, 1).unwrap(); - let mut device_seed_3_b = root_seed.generate_device_seed(&context, 3).unwrap(); + let _ = root_seed.generate_device_seed(0).unwrap_err(); + let mut device_seed_1 = root_seed.generate_device_seed(1).unwrap(); + let mut device_seed_3_b = root_seed.generate_device_seed(3).unwrap(); assert!( device_seed_3 .seed_mut() @@ -427,9 +527,8 @@ mod tests { let seed_buf = generate_random_seed_buf(); let mut pin = generate_random_seed_buf(); - let context = SeedContext::new(*b"HCDEVICE"); let mut root_seed = RootSeed::new(seed_buf); - let mut device_seed = root_seed.generate_device_seed(&context, 3).unwrap(); + let mut device_seed = root_seed.generate_device_seed(3).unwrap(); let device_pin_seed = device_seed .generate_device_pin_seed(&mut pin, TEST_CONFIG) .unwrap(); @@ -441,9 +540,8 @@ mod tests { let seed_buf = generate_random_seed_buf(); let mut pin = generate_random_seed_buf(); - let context = SeedContext::new(*b"HCDEVICE"); let mut rs = RootSeed::new(seed_buf); - let mut ds = rs.generate_device_seed(&context, 3).unwrap(); + let mut ds = rs.generate_device_seed(3).unwrap(); let mut dps = ds.generate_device_pin_seed(&mut pin, TEST_CONFIG).unwrap(); let mut keybundle_5 = dps.generate_dna_key(5).unwrap(); @@ -537,7 +635,7 @@ mod tests { TypedSeed::Root(s) => s, _ => unreachable!(), }; - let mut enc_seed = seed.encrypt("some passphrase".to_string(), None).unwrap(); + let enc_seed = seed.encrypt("some passphrase".to_string(), None).unwrap(); let mnemonic = enc_seed.get_mnemonic().unwrap(); println!("mnemonic: {:?}", mnemonic); assert_eq!( diff --git a/crates/metrics/src/stats.rs b/crates/metrics/src/stats.rs index 2ceccb7962..b33cccd087 100644 --- a/crates/metrics/src/stats.rs +++ b/crates/metrics/src/stats.rs @@ -6,8 +6,7 @@ use stats::Commute; use std::{ collections::HashMap, error::Error, - fmt, - fmt::{Display, Formatter}, + fmt::{self, Display, Formatter}, io, iter::FromIterator, };