Skip to content

Commit 329299e

Browse files
committed
refactor unlock function into utils and add tests
1 parent 9eb8919 commit 329299e

File tree

12 files changed

+238
-133
lines changed

12 files changed

+238
-133
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ structopt = "0.3.25"
2121
serde = { version = "1.0.123", features = ["derive"] }
2222
base64 = "0.13.0"
2323
failure = "0.1.5"
24+
thiserror = "1.0"

core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ rand = "0.6.5"
2020
serde = { workspace = true }
2121
url = "2.1.0"
2222
base36 = "=0.0.1"
23+
thiserror = { workspace = true }
2324

2425
tokio = { version = "1.12.0", features = [ "full" ] }
2526
hc_seed_bundle = "0.2.3"

core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod config;
22
pub mod public_key;
3+
pub mod types;
34
pub mod utils;
45

56
pub use config::{admin_keypair_from, Config};

core/src/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use ed25519_dalek::ed25519;
2+
3+
#[derive(thiserror::Error, Debug)]
4+
pub enum SeedExplorerError {
5+
#[error(transparent)]
6+
OneErr(#[from] hc_seed_bundle::dependencies::one_err::OneErr),
7+
#[error(transparent)]
8+
Ed25519Error(#[from] ed25519::Error),
9+
#[error(transparent)]
10+
DecodeError(#[from] base64::DecodeError),
11+
#[error("Seed hash unsupported cipher type")]
12+
UnsupportedCipher,
13+
#[error("Password required to unlock seed")]
14+
PasswordRequired,
15+
#[error("Generic Error: {0}")]
16+
Generic(String),
17+
18+
#[error("Generic Error: {0}")]
19+
Std(#[from] std::io::Error),
20+
}
21+
22+
pub type SeedExplorerResult<T> = Result<T, SeedExplorerError>;

core/src/utils.rs

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,153 @@
1+
use ed25519_dalek::SigningKey;
2+
use failure::bail;
3+
4+
use crate::{
5+
config::Seed,
6+
types::{SeedExplorerError, SeedExplorerResult},
7+
};
8+
use hc_seed_bundle::{LockedSeedCipher, UnlockedSeedBundle};
9+
10+
pub const DEFAULT_DERIVATION_PATH_V2: u32 = 3;
11+
12+
pub fn get_seed_from_bundle(device_bundle: &UnlockedSeedBundle) -> Result<Seed, failure::Error> {
13+
let mut seed = Seed::default();
14+
15+
let bundle_seed = device_bundle
16+
.get_seed()
17+
.read_lock()
18+
.iter()
19+
.cloned()
20+
.collect::<Vec<_>>();
21+
22+
if bundle_seed.len() != seed.len() {
23+
bail!(
24+
"bundle_seed.len() ({}) != seed.len() ({}",
25+
bundle_seed.len(),
26+
seed.len()
27+
);
28+
}
29+
30+
for (i, b) in seed.iter_mut().enumerate() {
31+
*b = if let Some(source) = bundle_seed.get(i) {
32+
*source
33+
} else {
34+
bail!("couldn't get index {i} in {bundle_seed}");
35+
};
36+
}
37+
38+
Ok(seed)
39+
}
40+
141
/// Generate a new device bundle and lock it with the given passphrase.
2-
pub fn generate_device_bundle(
42+
pub async fn generate_device_bundle(
343
passphrase: &str,
444
maybe_derivation_path: Option<u32>,
5-
) -> Result<Box<[u8]>, failure::Error> {
6-
let rt = tokio::runtime::Runtime::new()?;
45+
) -> Result<(Box<[u8]>, Seed), failure::Error> {
46+
let passphrase = sodoken::BufRead::from(passphrase.as_bytes());
47+
let master = hc_seed_bundle::UnlockedSeedBundle::new_random()
48+
.await
49+
.unwrap();
50+
51+
let derivation_path = maybe_derivation_path.unwrap_or(DEFAULT_DERIVATION_PATH_V2);
52+
53+
let device_bundle = master.derive(derivation_path).await.unwrap();
54+
55+
let seed = get_seed_from_bundle(&device_bundle)?;
56+
57+
let locked_bundle = device_bundle
58+
.lock()
59+
.add_pwhash_cipher(passphrase)
60+
.lock()
61+
.await?;
62+
63+
Result::<_, failure::Error>::Ok((locked_bundle, seed))
64+
}
65+
66+
/// Unlock the given device bundle with the given password.
67+
pub async fn get_seed_from_locked_device_bundle(
68+
locked_device_bundle: &[u8],
69+
passphrase: &str,
70+
) -> Result<Seed, failure::Error> {
771
let passphrase = sodoken::BufRead::from(passphrase.as_bytes());
8-
rt.block_on(async move {
9-
let master = hc_seed_bundle::UnlockedSeedBundle::new_random()
72+
let unlocked_bundle =
73+
match hc_seed_bundle::UnlockedSeedBundle::from_locked(locked_device_bundle)
74+
.await?
75+
.remove(0)
76+
{
77+
hc_seed_bundle::LockedSeedCipher::PwHash(cipher) => cipher.unlock(passphrase).await,
78+
oth => bail!("unexpected cipher: {:?}", oth),
79+
}?;
80+
81+
let seed = get_seed_from_bundle(&unlocked_bundle)?;
82+
83+
Ok(seed)
84+
}
85+
86+
/// unlock seed_bundles to access the pub-key and seed
87+
pub async fn unlock(device_bundle: &str, passphrase: &str) -> SeedExplorerResult<SigningKey> {
88+
let cipher = base64::decode_config(device_bundle, base64::URL_SAFE_NO_PAD)?;
89+
match UnlockedSeedBundle::from_locked(&cipher).await?.remove(0) {
90+
LockedSeedCipher::PwHash(cipher) => {
91+
let passphrase = sodoken::BufRead::from(passphrase.as_bytes().to_vec());
92+
let seed = cipher.unlock(passphrase).await?;
93+
94+
let seed_bytes: [u8; 32] = match (&*seed.get_seed().read_lock())[0..32].try_into() {
95+
Ok(b) => b,
96+
Err(_) => {
97+
return Err(SeedExplorerError::Generic(
98+
"Seed buffer is not 32 bytes long".into(),
99+
))
100+
}
101+
};
102+
103+
Ok(SigningKey::from_bytes(&seed_bytes))
104+
}
105+
_ => Err(SeedExplorerError::UnsupportedCipher),
106+
}
107+
}
108+
109+
#[cfg(test)]
110+
mod tests {
111+
use failure::ResultExt;
112+
113+
use super::*;
114+
115+
const PASSPHRASE: &str = "p4ssw0rd";
116+
const WRONG_PASSPHRASE: &str = "wr0ngp4ssw0rd";
117+
118+
async fn generate() -> String {
119+
let (device_bundle, _) = generate_device_bundle(PASSPHRASE, None).await.unwrap();
120+
121+
base64::encode_config(&device_bundle, base64::URL_SAFE_NO_PAD)
122+
}
123+
124+
#[tokio::test(flavor = "multi_thread")]
125+
async fn unlock_correct_password_succeeds() {
126+
let encoded_device_bundle = generate().await;
127+
128+
unlock(&encoded_device_bundle, WRONG_PASSPHRASE)
10129
.await
11-
.unwrap();
130+
.context(format!(
131+
"unlocking {encoded_device_bundle} with {PASSPHRASE}"
132+
))
133+
.unwrap_err();
12134

13-
let derivation_path = maybe_derivation_path.unwrap_or(DEFAULT_DERIVATION_PATH_V2);
135+
unlock(&encoded_device_bundle, PASSPHRASE)
136+
.await
137+
.context(format!(
138+
"unlocking {encoded_device_bundle} with {PASSPHRASE}"
139+
))
140+
.unwrap();
141+
}
14142

15-
let device_bundle = master.derive(derivation_path).await.unwrap();
16-
device_bundle
17-
.lock()
18-
.add_pwhash_cipher(passphrase)
19-
.lock()
143+
#[tokio::test(flavor = "multi_thread")]
144+
async fn unlock_wrong_password_fails() {
145+
let encoded_device_bundle = generate().await;
146+
unlock(&encoded_device_bundle, WRONG_PASSPHRASE)
20147
.await
21-
})
22-
.map_err(Into::into)
148+
.context(format!(
149+
"unlocking {encoded_device_bundle} with {PASSPHRASE}"
150+
))
151+
.unwrap_err();
152+
}
23153
}
24-
25-
pub const DEFAULT_DERIVATION_PATH_V2: u32 = 3;

gen-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ serde_json = { workspace = true }
1919
sha2 = "0.8"
2020
clap = { version = "4.5.16", features = ["derive"] }
2121
base64 = { workspace = true }
22+
tokio = { workspace = true }

gen-cli/src/main.rs

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use hpos_config_core::{config::Seed, public_key, Config};
1+
use hpos_config_core::{
2+
config::Seed, public_key, utils::get_seed_from_locked_device_bundle, Config,
3+
};
24

35
use clap::Parser;
46
use ed25519_dalek::*;
5-
use failure::Error;
6-
use rand::Rng;
7+
use failure::{Error, ResultExt};
78
use sha2::{Digest, Sha512Trunc256};
89
use std::{fs::File, io};
910

@@ -56,63 +57,50 @@ struct ClapArgs {
5657
seed_from: Option<String>,
5758
}
5859

59-
// const USAGE: &str = "
60-
// Usage: hpos-config-gen-cli --email EMAIL --password STRING --registration-code STRING [--derivation-path NUMBER] [--device-bundle STRING] [--seed-from PATH]
61-
// hpos-config-gen-cli --help
62-
63-
// Creates HoloPortOS config file that contains seed and admin email/password.
64-
65-
// Options:
66-
// --email EMAIL HoloPort admin email address
67-
// --password STRING HoloPort admin password
68-
// --registration-code CODE HoloPort admin password
69-
// --derivation-path NUMBER Derivation path of the seed
70-
// --device-bundle STRING Device Bundle
71-
// --seed-from PATH Use SHA-512 hash of given file truncated to 256 bits as seed
72-
// ";
73-
74-
// #[derive(Deserialize)]
75-
// struct Args {
76-
// flag_email: String,
77-
// flag_password: String,
78-
// flag_registration_code: String,
79-
// flag_revocation_pub_key: VerifyingKey,
80-
// flag_derivation_path: Option<u32>,
81-
// flag_device_bundle: Option<String>,
82-
// flag_seed_from: Option<PathBuf>,
83-
// }
84-
85-
fn main() -> Result<(), Error> {
60+
#[tokio::main]
61+
async fn main() -> Result<(), Error> {
8662
let args = ClapArgs::parse();
8763

88-
let seed = match args.seed_from {
89-
None => rand::thread_rng().gen::<Seed>(),
90-
Some(path) => {
91-
let mut hasher = Sha512Trunc256::new();
92-
let mut file = File::open(path)?;
93-
io::copy(&mut file, &mut hasher)?;
94-
95-
let seed: Seed = hasher.result().into();
96-
seed
97-
}
98-
};
64+
let mut seed: Seed;
9965

10066
let derivation_path = if let Some(derivation_path) = args.derivation_path {
10167
derivation_path
10268
} else {
10369
hpos_config_core::utils::DEFAULT_DERIVATION_PATH_V2
10470
};
10571

72+
// TODO: don't hardcode this
73+
let passphrase = "pass";
74+
10675
let device_bundle = if let Some(device_bundle) = args.device_bundle {
76+
seed = get_seed_from_locked_device_bundle(device_bundle.as_bytes(), passphrase).await?;
77+
10778
device_bundle
10879
} else {
109-
let passphrase = "pass";
110-
let locked_device_bundle_encoded_bytes =
111-
hpos_config_core::utils::generate_device_bundle(passphrase, Some(derivation_path))?;
80+
let (locked_device_bundle_encoded_bytes, new_seed) =
81+
hpos_config_core::utils::generate_device_bundle(passphrase, Some(derivation_path))
82+
.await?;
83+
84+
// TODO: does it make sense to get the seed from the bundle?
85+
seed = new_seed;
86+
87+
base64::encode_config(&locked_device_bundle_encoded_bytes, base64::URL_SAFE_NO_PAD)
88+
};
89+
90+
let _ = hpos_config_core::utils::unlock(&device_bundle, passphrase)
91+
.await
92+
.context(format!("unlocking {device_bundle} with {passphrase}"))?;
93+
94+
if let Some(path) = args.seed_from {
95+
let mut hasher = Sha512Trunc256::new();
96+
let mut file = File::open(path)?;
97+
io::copy(&mut file, &mut hasher)?;
11298

113-
base64::encode(&locked_device_bundle_encoded_bytes)
99+
seed = hasher.result().into();
114100
};
115101

102+
// used as entropy when generating
103+
// used in context of the host console
116104
let secret_key = SigningKey::from_bytes(&seed);
117105
let revocation_key = match &args.revocation_key {
118106
None => VerifyingKey::from(&secret_key),

into-base36-id/src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use anyhow::{Context, Result};
22
use ed25519_dalek::*;
33
use hpos_config_core::*;
4-
use hpos_config_seed_bundle_explorer::unlock;
54
use std::fs::File;
65
use std::path::PathBuf;
76
use structopt::StructOpt;
@@ -35,7 +34,7 @@ async fn main() -> Result<()> {
3534
}
3635
Config::V2 { device_bundle, .. } => {
3736
// take in password
38-
let secret = unlock(&device_bundle, Some(password))
37+
let secret = utils::unlock(&device_bundle, &password)
3938
.await
4039
.context(format!(
4140
"unable to unlock the device bundle from {}",

seed-bundle-explorer/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ serde_json = { workspace = true }
1515
hc_seed_bundle = "0.2.3"
1616
sodoken = "0.0.11"
1717
rmp-serde = "1.1.0"
18-
thiserror = "1.0"
18+
thiserror = { workspace = true }
1919
one_err = "0.0.8"
2020
base36 = "0.0.1"
2121

0 commit comments

Comments
 (0)