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 c50c6d1 commit a6dab1e
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 184 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;
83 changes: 0 additions & 83 deletions core/tests/configuration.rs

This file was deleted.

1 change: 1 addition & 0 deletions gen-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ serde = { workspace = true }
serde_json = { workspace = true }
sha2 = "0.8"
base64 = { workspace = true }
tokio = { workspace = true }
Loading

0 comments on commit a6dab1e

Please sign in to comment.