use crate::ui;
use anyhow::{anyhow, bail, Context, Result};
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser)]
pub(crate) struct DpapiMkArgs {
#[arg(long)]
pub file: PathBuf,
#[arg(long)]
pub sid: String,
#[arg(long, conflicts_with = "pwdkey")]
pub password: Option<adhammer_core::SecretString>,
#[arg(long, conflicts_with = "password")]
pub pwdkey: Option<adhammer_core::SecretString>,
}
pub(crate) async fn dpapi_master_key(mut a: DpapiMkArgs) -> Result<()> {
let mut checklist = ui::StageChecklist::new([
"read masterkey file",
"parse subfield header",
"derive pwdkey (if --password)",
"decrypt with ms_derive_key + AES256-CBC",
"verify HMAC + emit 64-byte masterkey",
]);
if let Some(pw) = a.password.as_ref() {
a.password = Some(crate::resolve_secret(pw, "ADHAMMER_DPAPI_PASSWORD")?);
}
let result = dpapi_impl(a, &mut checklist).await;
match &result {
Ok(()) => checklist.render("dpapi-master-key stages"),
Err(e) => {
let brief = format!("{e:#}")
.lines()
.next()
.unwrap_or("failed")
.chars()
.take(80)
.collect::<String>();
checklist.mark_current_failed(brief);
checklist.render("dpapi-master-key stages (failed)");
}
}
result
}
async fn dpapi_impl(a: DpapiMkArgs, checklist: &mut ui::StageChecklist) -> Result<()> {
let bytes = std::fs::read(&a.file)
.with_context(|| format!("read masterkey file {}", a.file.display()))?;
checklist.record_ok("read masterkey file", format!("{} bytes", bytes.len()));
let mkf = dpapi_offline::MasterKeyFile::parse(&bytes)
.map_err(|e| anyhow!("parse masterkey file: {e}"))?;
let mk = mkf
.master_key
.ok_or_else(|| anyhow!("masterkey file has no MasterKey subfield (empty or corrupt)"))?;
checklist.record_ok(
"parse subfield header",
format!(
"guid={}, rounds={}, hash={:?}, cipher={:?}",
mkf.guid, mk.rounds, mk.hash_algo, mk.cipher_algo
),
);
let (master_key, path_label) = match (a.password.as_deref(), a.pwdkey.as_deref()) {
(Some(password), _) => {
checklist.record_ok(
"derive pwdkey (if --password)",
"will try standalone SHA1 → domain MD4 → Protected-Users PBKDF2-SHA256".to_string(),
);
let key = dpapi_offline::unlock_masterkey(&bytes, password, &a.sid)
.map_err(|e| anyhow!("all three pre-key paths failed HMAC-verify: {e} — wrong password / SID / account type?"))?;
(key, "password (auto-tried 3 pre-keys)")
}
(None, Some(hex)) => {
checklist.record_ok(
"derive pwdkey (if --password)",
"skipped — --pwdkey supplied externally".to_string(),
);
let raw = hex_decode(hex).context("--pwdkey must be 40 hex chars")?;
if raw.len() != 20 {
bail!(
"--pwdkey must be exactly 20 bytes (40 hex chars); got {}",
raw.len()
);
}
let key = mk
.decrypt_with_key(&raw)
.map_err(|e| anyhow!("pwdkey decrypt failed HMAC-verify: {e}"))?;
(key, "external pwdkey")
}
(None, None) => bail!("either --password or --pwdkey is required"),
};
checklist.record_ok(
"decrypt with ms_derive_key + AES256-CBC",
format!("via {path_label}"),
);
let hex_key = master_key
.iter()
.fold(String::with_capacity(128), |mut s, b| {
use std::fmt::Write;
write!(s, "{b:02x}").unwrap();
s
});
println!();
println!("[+] masterkey file : {}", a.file.display());
println!("[+] guid : {}", mkf.guid);
println!("[+] sid : {}", a.sid);
println!("[+] master-key : {hex_key}");
println!();
println!("The 64-byte master-key above unlocks every DPAPI blob owned by this SID.");
println!("Follow-up: parse individual `CryptProtectData` blobs with this key (WS-DPAPI-BLOB, deferred).");
checklist.record_ok(
"verify HMAC + emit 64-byte masterkey",
"HMAC verified".to_string(),
);
Ok(())
}
fn hex_decode(s: &str) -> Result<Vec<u8>> {
let s = s.trim();
if !s.len().is_multiple_of(2) {
bail!("hex string has odd length");
}
(0..s.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&s[i..i + 2], 16)
.with_context(|| format!("invalid hex at position {i}"))
})
.collect()
}