From 19b3c593f9a14db04197d4b95fc21517d1bfaa24 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Fri, 16 Aug 2024 18:38:32 +0000 Subject: [PATCH] refactor unlock function into utils and add tests --- Cargo.lock | 2 + Cargo.toml | 1 + core/Cargo.toml | 1 + core/src/lib.rs | 1 + core/src/types.rs | 22 +++++ core/src/utils.rs | 160 ++++++++++++++++++++++++++++---- gen-cli/Cargo.toml | 1 + gen-cli/src/main.rs | 78 +++++++--------- into-base36-id/src/main.rs | 3 +- seed-bundle-explorer/Cargo.toml | 2 +- seed-bundle-explorer/src/lib.rs | 77 +++++---------- seed-encoder/src/main.rs | 23 ++--- 12 files changed, 238 insertions(+), 133 deletions(-) create mode 100644 core/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index c593f44..80ade83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1025,6 +1025,7 @@ dependencies = [ "serde", "serde_json", "sodoken", + "thiserror", "tokio", "url", ] @@ -1043,6 +1044,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.8.2", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c4d7d39..44f7c46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ structopt = "0.3.25" serde = { version = "1.0.123", features = ["derive"] } base64 = "0.13.0" failure = "0.1.5" +thiserror = "1.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index aeba8af..225fa57 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,6 +20,7 @@ rand = "0.6.5" serde = { workspace = true } url = "2.1.0" base36 = "=0.0.1" +thiserror = { workspace = true } tokio = { version = "1.12.0", features = [ "full" ] } hc_seed_bundle = "0.2.3" diff --git a/core/src/lib.rs b/core/src/lib.rs index 7c1766a..484bd6f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,5 +1,6 @@ pub mod config; pub mod public_key; +pub mod types; pub mod utils; pub use config::{admin_keypair_from, Config}; diff --git a/core/src/types.rs b/core/src/types.rs new file mode 100644 index 0000000..3513939 --- /dev/null +++ b/core/src/types.rs @@ -0,0 +1,22 @@ +use ed25519_dalek::ed25519; + +#[derive(thiserror::Error, Debug)] +pub enum SeedExplorerError { + #[error(transparent)] + OneErr(#[from] hc_seed_bundle::dependencies::one_err::OneErr), + #[error(transparent)] + Ed25519Error(#[from] ed25519::Error), + #[error(transparent)] + DecodeError(#[from] base64::DecodeError), + #[error("Seed hash unsupported cipher type")] + UnsupportedCipher, + #[error("Password required to unlock seed")] + PasswordRequired, + #[error("Generic Error: {0}")] + Generic(String), + + #[error("Generic Error: {0}")] + Std(#[from] std::io::Error), +} + +pub type SeedExplorerResult = Result; diff --git a/core/src/utils.rs b/core/src/utils.rs index 10a26f6..90d079a 100644 --- a/core/src/utils.rs +++ b/core/src/utils.rs @@ -1,25 +1,153 @@ +use ed25519_dalek::SigningKey; +use failure::bail; + +use crate::{ + config::Seed, + types::{SeedExplorerError, SeedExplorerResult}, +}; +use hc_seed_bundle::{LockedSeedCipher, UnlockedSeedBundle}; + +pub const DEFAULT_DERIVATION_PATH_V2: u32 = 3; + +pub fn get_seed_from_bundle(device_bundle: &UnlockedSeedBundle) -> Result { + let mut seed = Seed::default(); + + let bundle_seed = device_bundle + .get_seed() + .read_lock() + .iter() + .cloned() + .collect::>(); + + if bundle_seed.len() != seed.len() { + bail!( + "bundle_seed.len() ({}) != seed.len() ({}", + bundle_seed.len(), + seed.len() + ); + } + + for (i, b) in seed.iter_mut().enumerate() { + *b = if let Some(source) = bundle_seed.get(i) { + *source + } else { + bail!("couldn't get index {i} in {bundle_seed}"); + }; + } + + Ok(seed) +} + /// Generate a new device bundle and lock it with the given passphrase. -pub fn generate_device_bundle( +pub async fn generate_device_bundle( passphrase: &str, maybe_derivation_path: Option, -) -> Result, failure::Error> { - let rt = tokio::runtime::Runtime::new()?; +) -> Result<(Box<[u8]>, Seed), failure::Error> { + let passphrase = sodoken::BufRead::from(passphrase.as_bytes()); + let master = hc_seed_bundle::UnlockedSeedBundle::new_random() + .await + .unwrap(); + + let derivation_path = maybe_derivation_path.unwrap_or(DEFAULT_DERIVATION_PATH_V2); + + let device_bundle = master.derive(derivation_path).await.unwrap(); + + let seed = get_seed_from_bundle(&device_bundle)?; + + let locked_bundle = device_bundle + .lock() + .add_pwhash_cipher(passphrase) + .lock() + .await?; + + Result::<_, failure::Error>::Ok((locked_bundle, seed)) +} + +/// Unlock the given device bundle with the given password. +pub async fn get_seed_from_locked_device_bundle( + locked_device_bundle: &[u8], + passphrase: &str, +) -> Result { let passphrase = sodoken::BufRead::from(passphrase.as_bytes()); - rt.block_on(async move { - let master = hc_seed_bundle::UnlockedSeedBundle::new_random() + let unlocked_bundle = + match hc_seed_bundle::UnlockedSeedBundle::from_locked(locked_device_bundle) + .await? + .remove(0) + { + hc_seed_bundle::LockedSeedCipher::PwHash(cipher) => cipher.unlock(passphrase).await, + oth => bail!("unexpected cipher: {:?}", oth), + }?; + + let seed = get_seed_from_bundle(&unlocked_bundle)?; + + Ok(seed) +} + +/// unlock seed_bundles to access the pub-key and seed +pub async fn unlock(device_bundle: &str, passphrase: &str) -> SeedExplorerResult { + let cipher = base64::decode_config(device_bundle, base64::URL_SAFE_NO_PAD)?; + match UnlockedSeedBundle::from_locked(&cipher).await?.remove(0) { + LockedSeedCipher::PwHash(cipher) => { + let passphrase = sodoken::BufRead::from(passphrase.as_bytes().to_vec()); + let seed = cipher.unlock(passphrase).await?; + + let seed_bytes: [u8; 32] = match (&*seed.get_seed().read_lock())[0..32].try_into() { + Ok(b) => b, + Err(_) => { + return Err(SeedExplorerError::Generic( + "Seed buffer is not 32 bytes long".into(), + )) + } + }; + + Ok(SigningKey::from_bytes(&seed_bytes)) + } + _ => Err(SeedExplorerError::UnsupportedCipher), + } +} + +#[cfg(test)] +mod tests { + use failure::ResultExt; + + use super::*; + + const PASSPHRASE: &str = "p4ssw0rd"; + const WRONG_PASSPHRASE: &str = "wr0ngp4ssw0rd"; + + async fn generate() -> String { + let (device_bundle, _) = generate_device_bundle(PASSPHRASE, None).await.unwrap(); + + base64::encode_config(&device_bundle, base64::URL_SAFE_NO_PAD) + } + + #[tokio::test(flavor = "multi_thread")] + async fn unlock_correct_password_succeeds() { + let encoded_device_bundle = generate().await; + + unlock(&encoded_device_bundle, WRONG_PASSPHRASE) .await - .unwrap(); + .context(format!( + "unlocking {encoded_device_bundle} with {PASSPHRASE}" + )) + .unwrap_err(); - let derivation_path = maybe_derivation_path.unwrap_or(DEFAULT_DERIVATION_PATH_V2); + unlock(&encoded_device_bundle, PASSPHRASE) + .await + .context(format!( + "unlocking {encoded_device_bundle} with {PASSPHRASE}" + )) + .unwrap(); + } - let device_bundle = master.derive(derivation_path).await.unwrap(); - device_bundle - .lock() - .add_pwhash_cipher(passphrase) - .lock() + #[tokio::test(flavor = "multi_thread")] + async fn unlock_wrong_password_fails() { + let encoded_device_bundle = generate().await; + unlock(&encoded_device_bundle, WRONG_PASSPHRASE) .await - }) - .map_err(Into::into) + .context(format!( + "unlocking {encoded_device_bundle} with {PASSPHRASE}" + )) + .unwrap_err(); + } } - -pub const DEFAULT_DERIVATION_PATH_V2: u32 = 3; diff --git a/gen-cli/Cargo.toml b/gen-cli/Cargo.toml index ffb5451..3f809e3 100644 --- a/gen-cli/Cargo.toml +++ b/gen-cli/Cargo.toml @@ -19,3 +19,4 @@ serde_json = { workspace = true } sha2 = "0.8" clap = { version = "4.5.16", features = ["derive"] } base64 = { workspace = true } +tokio = { workspace = true } diff --git a/gen-cli/src/main.rs b/gen-cli/src/main.rs index 895442f..5e8e304 100644 --- a/gen-cli/src/main.rs +++ b/gen-cli/src/main.rs @@ -1,9 +1,10 @@ -use hpos_config_core::{config::Seed, public_key, Config}; +use hpos_config_core::{ + config::Seed, public_key, utils::get_seed_from_locked_device_bundle, Config, +}; use clap::Parser; use ed25519_dalek::*; -use failure::Error; -use rand::Rng; +use failure::{Error, ResultExt}; use sha2::{Digest, Sha512Trunc256}; use std::{fs::File, io}; @@ -56,46 +57,11 @@ struct ClapArgs { seed_from: Option, } -// const USAGE: &str = " -// Usage: hpos-config-gen-cli --email EMAIL --password STRING --registration-code STRING [--derivation-path NUMBER] [--device-bundle STRING] [--seed-from PATH] -// hpos-config-gen-cli --help - -// Creates HoloPortOS config file that contains seed and admin email/password. - -// Options: -// --email EMAIL HoloPort admin email address -// --password STRING HoloPort admin password -// --registration-code CODE HoloPort admin password -// --derivation-path NUMBER Derivation path of the seed -// --device-bundle STRING Device Bundle -// --seed-from PATH Use SHA-512 hash of given file truncated to 256 bits as seed -// "; - -// #[derive(Deserialize)] -// struct Args { -// flag_email: String, -// flag_password: String, -// flag_registration_code: String, -// flag_revocation_pub_key: VerifyingKey, -// flag_derivation_path: Option, -// flag_device_bundle: Option, -// flag_seed_from: Option, -// } - -fn main() -> Result<(), Error> { +#[tokio::main] +async fn main() -> Result<(), Error> { let args = ClapArgs::parse(); - let seed = match args.seed_from { - None => rand::thread_rng().gen::(), - Some(path) => { - let mut hasher = Sha512Trunc256::new(); - let mut file = File::open(path)?; - io::copy(&mut file, &mut hasher)?; - - let seed: Seed = hasher.result().into(); - seed - } - }; + let mut seed: Seed; let derivation_path = if let Some(derivation_path) = args.derivation_path { derivation_path @@ -103,16 +69,38 @@ fn main() -> Result<(), Error> { hpos_config_core::utils::DEFAULT_DERIVATION_PATH_V2 }; + // TODO: don't hardcode this + let passphrase = "pass"; + let device_bundle = if let Some(device_bundle) = args.device_bundle { + seed = get_seed_from_locked_device_bundle(device_bundle.as_bytes(), passphrase).await?; + device_bundle } else { - let passphrase = "pass"; - let locked_device_bundle_encoded_bytes = - hpos_config_core::utils::generate_device_bundle(passphrase, Some(derivation_path))?; + let (locked_device_bundle_encoded_bytes, new_seed) = + hpos_config_core::utils::generate_device_bundle(passphrase, Some(derivation_path)) + .await?; + + // TODO: does it make sense to get the seed from the bundle? + seed = new_seed; + + base64::encode_config(&locked_device_bundle_encoded_bytes, base64::URL_SAFE_NO_PAD) + }; + + let _ = hpos_config_core::utils::unlock(&device_bundle, passphrase) + .await + .context(format!("unlocking {device_bundle} with {passphrase}"))?; + + if let Some(path) = args.seed_from { + let mut hasher = Sha512Trunc256::new(); + let mut file = File::open(path)?; + io::copy(&mut file, &mut hasher)?; - base64::encode(&locked_device_bundle_encoded_bytes) + seed = hasher.result().into(); }; + // used as entropy when generating + // used in context of the host console let secret_key = SigningKey::from_bytes(&seed); let revocation_key = match &args.revocation_key { None => VerifyingKey::from(&secret_key), diff --git a/into-base36-id/src/main.rs b/into-base36-id/src/main.rs index cbcd140..d86a0b6 100644 --- a/into-base36-id/src/main.rs +++ b/into-base36-id/src/main.rs @@ -1,7 +1,6 @@ use anyhow::{Context, Result}; use ed25519_dalek::*; use hpos_config_core::*; -use hpos_config_seed_bundle_explorer::unlock; use std::fs::File; use std::path::PathBuf; use structopt::StructOpt; @@ -35,7 +34,7 @@ async fn main() -> Result<()> { } Config::V2 { device_bundle, .. } => { // take in password - let secret = unlock(&device_bundle, Some(password)) + let secret = utils::unlock(&device_bundle, &password) .await .context(format!( "unable to unlock the device bundle from {}", diff --git a/seed-bundle-explorer/Cargo.toml b/seed-bundle-explorer/Cargo.toml index 077cecb..654009c 100644 --- a/seed-bundle-explorer/Cargo.toml +++ b/seed-bundle-explorer/Cargo.toml @@ -15,7 +15,7 @@ serde_json = { workspace = true } hc_seed_bundle = "0.2.3" sodoken = "0.0.11" rmp-serde = "1.1.0" -thiserror = "1.0" +thiserror = { workspace = true } one_err = "0.0.8" base36 = "0.0.1" diff --git a/seed-bundle-explorer/src/lib.rs b/seed-bundle-explorer/src/lib.rs index 37eacba..8c8f7d0 100644 --- a/seed-bundle-explorer/src/lib.rs +++ b/seed-bundle-explorer/src/lib.rs @@ -1,11 +1,10 @@ -use ed25519_dalek::{ed25519, SigningKey, VerifyingKey}; -use hc_seed_bundle::*; -use hpos_config_core::Config; +use ed25519_dalek::{SigningKey, VerifyingKey}; +use hpos_config_core::{types::*, utils::unlock, Config}; /// get pub key for the device bundle in the config pub async fn holoport_public_key( config: &Config, - passphrase: Option, + maybe_passphrase: Option, ) -> SeedExplorerResult { match config { Config::V1 { seed, .. } => { @@ -18,7 +17,11 @@ pub async fn holoport_public_key( password is pass for now unlock it and get the signPubKey */ - let secret = unlock(device_bundle, passphrase).await?; + let secret = unlock( + device_bundle, + &maybe_passphrase.ok_or(SeedExplorerError::PasswordRequired)?, + ) + .await?; Ok(secret.verifying_key()) } Config::V3 { holoport_id, .. } => { @@ -41,7 +44,7 @@ pub async fn holoport_public_key( /// get key for the device bundle in the config pub async fn holoport_key( config: &Config, - passphrase: Option, + maybe_passphrase: Option, ) -> SeedExplorerResult { match config { Config::V1 { seed, .. } => Ok(SigningKey::from_bytes(seed)), @@ -51,7 +54,12 @@ pub async fn holoport_key( password is pass for now unlock it and get the signPubKey */ - unlock(device_bundle, passphrase).await + + unlock( + device_bundle, + &maybe_passphrase.ok_or(SeedExplorerError::PasswordRequired)?, + ) + .await } } } @@ -59,7 +67,7 @@ pub async fn holoport_key( /// encode the ed25519 keypair making it compatible with lair (, + maybe_passphrase: Option, ) -> SeedExplorerResult { match config { Config::V1 { seed, .. } => { @@ -73,7 +81,11 @@ pub async fn encoded_ed25519_keypair( unlock it and get the signPubKey Pass the Seed and VerifyingKey into `encrypt_key(seed, pubKey)` */ - let secret = unlock(device_bundle, passphrase).await?; + let secret = unlock( + device_bundle, + &maybe_passphrase.ok_or(SeedExplorerError::PasswordRequired)?, + ) + .await?; Ok(encrypt_key(&secret, &secret.verifying_key())) } } @@ -106,50 +118,3 @@ pub fn encrypt_key(seed: &SigningKey, public_key: &VerifyingKey) -> String { encrypted_key.extend(seed.to_bytes()); base64::encode(&encrypted_key) } - -/// unlock seed_bundles to access the pub-key and seed -pub async fn unlock( - device_bundle: &String, - passphrase: Option, -) -> SeedExplorerResult { - let cipher = base64::decode_config(device_bundle, base64::URL_SAFE_NO_PAD)?; - match UnlockedSeedBundle::from_locked(&cipher).await?.remove(0) { - LockedSeedCipher::PwHash(cipher) => { - let passphrase = passphrase - .as_ref() - .ok_or(SeedExplorerError::PasswordRequired)?; - let passphrase = sodoken::BufRead::from(passphrase.as_bytes().to_vec()); - let seed = cipher.unlock(passphrase).await?; - - let seed_bytes: [u8; 32] = match (&*seed.get_seed().read_lock())[0..32].try_into() { - Ok(b) => b, - Err(_) => { - return Err(SeedExplorerError::Generic( - "Seed buffer is not 32 bytes long".into(), - )) - } - }; - - Ok(SigningKey::from_bytes(&seed_bytes)) - } - _ => Err(SeedExplorerError::UnsupportedCipher), - } -} - -#[derive(thiserror::Error, Debug)] -pub enum SeedExplorerError { - #[error(transparent)] - OneErr(#[from] hc_seed_bundle::dependencies::one_err::OneErr), - #[error(transparent)] - Ed25519Error(#[from] ed25519::Error), - #[error(transparent)] - DecodeError(#[from] base64::DecodeError), - #[error("Seed hash unsupported cipher type")] - UnsupportedCipher, - #[error("Password required to unlock seed")] - PasswordRequired, - #[error("Generic Error: {0}")] - Generic(String), -} - -pub type SeedExplorerResult = Result; diff --git a/seed-encoder/src/main.rs b/seed-encoder/src/main.rs index fc0b305..81e1024 100644 --- a/seed-encoder/src/main.rs +++ b/seed-encoder/src/main.rs @@ -5,9 +5,10 @@ use anyhow::{Context, Result}; use ed25519_dalek::*; use hpos_config_core::*; -use hpos_config_seed_bundle_explorer::{encrypt_key, unlock}; +use hpos_config_seed_bundle_explorer::encrypt_key; use std::path::PathBuf; use structopt::StructOpt; +use utils::unlock; #[tokio::main] async fn main() -> Result<()> { @@ -38,22 +39,18 @@ async fn main() -> Result<()> { } Config::V2 { device_bundle, .. } => { // take in password - let secret = unlock(&device_bundle, Some(password)) - .await - .context(format!( - "unable to unlock the device bundle from {}", - &config_path.to_string_lossy() - ))?; + let secret = unlock(&device_bundle, &password).await.context(format!( + "unable to unlock the device bundle from {}", + &config_path.to_string_lossy() + ))?; println!("{}", encrypt_key(&secret, &secret.verifying_key())); } Config::V3 { device_bundle, .. } => { // take in password - let secret = unlock(&device_bundle, Some(password)) - .await - .context(format!( - "unable to unlock the device bundle from {}", - &config_path.to_string_lossy() - ))?; + let secret = unlock(&device_bundle, &password).await.context(format!( + "unable to unlock the device bundle from {}", + &config_path.to_string_lossy() + ))?; println!("{}", encrypt_key(&secret, &secret.verifying_key())); } }