Skip to content

Commit

Permalink
refactor unlock function into utils and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
steveej committed Aug 21, 2024
1 parent b37e7d7 commit 19b3c59
Show file tree
Hide file tree
Showing 12 changed files with 238 additions and 133 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod config;
pub mod public_key;
pub mod types;
pub mod utils;

pub use config::{admin_keypair_from, Config};
22 changes: 22 additions & 0 deletions core/src/types.rs
Original file line number Diff line number Diff line change
@@ -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<T> = Result<T, SeedExplorerError>;
160 changes: 144 additions & 16 deletions core/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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<Seed, failure::Error> {
let mut seed = Seed::default();

let bundle_seed = device_bundle
.get_seed()
.read_lock()
.iter()
.cloned()
.collect::<Vec<_>>();

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<u32>,
) -> Result<Box<[u8]>, 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<Seed, failure::Error> {
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<SigningKey> {
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;
1 change: 1 addition & 0 deletions gen-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ serde_json = { workspace = true }
sha2 = "0.8"
clap = { version = "4.5.16", features = ["derive"] }
base64 = { workspace = true }
tokio = { workspace = true }
78 changes: 33 additions & 45 deletions gen-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -56,63 +57,50 @@ struct ClapArgs {
seed_from: Option<String>,
}

// 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<u32>,
// flag_device_bundle: Option<String>,
// flag_seed_from: Option<PathBuf>,
// }

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::<Seed>(),
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
} else {
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),
Expand Down
3 changes: 1 addition & 2 deletions into-base36-id/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {}",
Expand Down
2 changes: 1 addition & 1 deletion seed-bundle-explorer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading

0 comments on commit 19b3c59

Please sign in to comment.