use std::fs;
use std::io::Read;
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{ArgAction, Parser, ValueEnum};
use serde::Serialize;
mod parse;
mod crypto;
mod util;
mod fourcc;
mod formatter;
#[cfg(feature = "lzfse")]
mod decompress_lzfse;
#[cfg(feature = "lzss")]
mod decompress_lzss;
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum KbagClass {
Prod, Dev, Any, }
#[derive(Parser, Debug)]
#[command(name = "img4-dump", version)]
struct Cli {
#[arg(value_name = "INPUT", required = true)]
input: PathBuf,
#[arg(short = 'o', long = "outdir", default_value = "img4_dump")]
outdir: PathBuf,
#[arg(short = 'f', long = "force", action = ArgAction::SetTrue)]
force: bool,
#[arg(short = 'v', long = "verbose", action = ArgAction::SetTrue)]
verbose: bool,
#[arg(long = "json", action = ArgAction::SetTrue)]
json: bool,
#[arg(long = "decrypt", action = ArgAction::SetTrue)]
decrypt: bool,
#[arg(long = "aes-mode", value_enum, default_value_t = AesMode::Cbc)]
aes_mode: AesMode,
#[arg(long = "auto", action = ArgAction::SetTrue)]
auto: bool,
#[arg(long = "iv")]
iv_hex: Option<String>,
#[arg(long = "key")]
key_hex: Option<String>,
#[arg(long = "keep-ciphertext", action = ArgAction::SetTrue)]
keep_ciphertext: bool,
#[arg(long = "decompress", action = ArgAction::SetTrue)]
decompress: bool,
#[arg(long = "dump-im4m", action = ArgAction::SetTrue)]
dump_im4m: bool,
#[arg(long = "dump-im4r", action = ArgAction::SetTrue)]
dump_im4r: bool,
#[arg(long = "dump-im4m-props", action = ArgAction::SetTrue)]
dump_im4m_props: bool,
#[arg(long = "dump-im4m-certs", action = ArgAction::SetTrue)]
dump_im4m_certs: bool,
#[arg(long = "kbag-class", value_enum, default_value_t = KbagClass::Prod)]
kbag_class: KbagClass,
#[arg(long = "kbag-index")]
kbag_index: Option<usize>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum AesMode {
Ctr,
Cbc,
}
#[derive(Debug, Serialize)]
struct OutputFile {
label: String,
path: String,
}
#[derive(Debug, Serialize)]
struct Summary {
container: parse::ContainerKind,
im4p: Option<parse::Im4pInfo>,
im4m: Option<parse::Im4mInfoSummary>,
im4r_len: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
im4r_properties: Option<Vec<parse::TypedIm4mProperty>>,
notes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
output_files: Vec<OutputFile>,
}
fn main() {
let cli = Cli::parse();
if std::env::var("RUST_LOG").is_err() {
let level = if cli.verbose { "debug" } else { "info" };
std::env::set_var("RUST_LOG", level);
}
env_logger::init();
let json = cli.json;
if let Err(e) = run(cli) {
if json {
let obj = serde_json::json!({ "error": format!("{e:#}") });
println!("{}", serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{\"error\":\"unknown\"}".into()));
} else {
eprintln!("Error: {e:#}");
}
std::process::exit(1);
}
}
fn run(cli: Cli) -> Result<()> {
log::debug!("Parsed CLI options: {:?}", cli);
log::debug!("Opening input file: {:?}", cli.input);
let mut f = fs::File::open(&cli.input).with_context(|| format!("open {:?}", cli.input))?;
let mut bytes = Vec::new();
f.read_to_end(&mut bytes)?;
log::debug!("Read {} bytes from input file", bytes.len());
log::debug!("Starting container parsing");
let parsed = parse::parse_img4_like(&bytes)?;
log::debug!(
"Parsed container kind: {:?}, im4p: {}, im4m: {}, im4r: {}",
parsed.kind,
parsed.im4p.is_some(),
parsed.im4m.is_some(),
parsed.im4r.is_some()
);
if cli.auto {
return run_auto(&cli, &parsed);
}
log::debug!("Ensuring output directory at {:?}", cli.outdir);
util::ensure_outdir(&cli.outdir, cli.force)?;
log::debug!("Output directory ready");
let mut notes = Vec::new();
let mut output_paths = formatter::OutputPaths::default();
let mut im4p_info = None;
if let Some(im4p) = &parsed.im4p {
log::debug!("Processing IM4P payload ({} bytes)", im4p.data.len());
let base = cli.outdir.join("im4p.bin");
fs::write(&base, &im4p.data).context("write im4p.bin")?;
output_paths.add("Payload", base.display().to_string());
if cli.verbose {
eprintln!("wrote {:?}", base);
}
if let Some(kbag_raw) = &im4p.kbag_der {
log::debug!("KBAG detected, writing DER blob ({} bytes)", kbag_raw.len());
let p = cli.outdir.join("im4p.kbag.der");
fs::write(&p, kbag_raw)?;
output_paths.add("KBAG", p.display().to_string());
if cli.verbose {
eprintln!("wrote {:?}", p);
}
}
if let Some(props) = &im4p.payload_properties {
let p = cli.outdir.join("im4p.payp.json");
fs::write(&p, serde_json::to_vec_pretty(props)?)?;
output_paths.add("Payload Properties", p.display().to_string());
if cli.verbose {
eprintln!("wrote {:?} ({} properties)", p, props.len());
}
}
if cli.decrypt && cli.keep_ciphertext {
log::debug!("Keeping ciphertext copy of IM4P");
let p = cli.outdir.join("im4p.ciphertext");
fs::write(&p, &im4p.data)?;
}
let mut clear = None;
if cli.decrypt {
log::debug!("Decryption requested, resolving IV and key");
let (iv, key) = util::resolve_iv_key(&cli, im4p)?;
log::debug!("IV resolved ({} bytes), Key resolved ({} bytes)", iv.len(), key.len());
let mode = cli.aes_mode;
log::debug!("Using AES mode: {:?}", mode);
let dec = crypto::decrypt_aes(&im4p.data, &iv, &key, mode)
.with_context(|| "AES decryption failed (check mode/IV/Key)")?;
log::debug!("Decryption succeeded, plaintext size {} bytes", dec.len());
let (valid, detected) = util::validate_decryption(&dec);
if valid {
if let Some(fmt) = detected {
log::debug!("Decryption validation: OK (detected: {})", fmt);
if cli.verbose {
eprintln!("Decryption appears valid: {}", fmt);
}
}
} else {
let reason = detected.unwrap_or_else(|| "unknown".into());
log::warn!("Decryption validation FAILED: {}", reason);
log::warn!("The output may be garbage (wrong key/IV/mode?)");
let other = match mode {
AesMode::Ctr => "cbc",
AesMode::Cbc => "ctr",
};
log::warn!(
"HINT: this looks like the wrong AES mode — retry with --aes-mode {other} \
(most Apple images, e.g. iBoot/iBEC/iBSS/LLB/SEP, are CBC)"
);
notes.push(format!("decryption validation failed: {}", reason));
notes.push(format!("hint: retry with --aes-mode {other}"));
}
let out = cli.outdir.join("im4p.decrypted");
fs::write(&out, &dec)?;
output_paths.add("Decrypted", out.display().to_string());
if cli.verbose {
eprintln!("wrote {:?}", out);
}
clear = Some(dec);
}
if cli.decompress {
log::debug!("Decompression requested");
let src: &[u8] = if let Some(ref d) = clear { d } else { &im4p.data };
log::debug!("Decompression source size {} bytes", src.len());
match util::try_decompress_with_metadata(src, im4p.compression.as_ref()) {
Ok(Some((name, dec))) => {
log::debug!("Decompression succeeded, generated {} ({} bytes)", name, dec.len());
let p = cli.outdir.join(name);
fs::write(&p, &dec)?;
output_paths.add("Decompressed", p.display().to_string());
if cli.verbose {
eprintln!("wrote {:?}", p);
}
}
Ok(None) => {
log::debug!("No known compression detected");
notes.push("no known compression detected".into())
}
Err(e) => {
log::debug!("Decompression error: {}", e);
notes.push(format!("decompress error: {e}"))
}
}
}
im4p_info = Some(parse::Im4pInfo {
r#type: im4p.r#type.clone(),
version: im4p.version.clone(),
data_len: im4p.data.len(),
kbag: im4p
.kbag_summary
.as_ref()
.map(|v| v.iter().map(parse::KbagEntryInfo::from).collect()),
compression: im4p.compression.as_ref().map(parse::CompressionInfo::from),
payload_properties: im4p.payload_properties.clone(),
});
log::debug!("IM4P info recorded");
}
let mut im4m_summary = None;
if let Some(im4m) = &parsed.im4m {
log::debug!("IM4M present, size {} bytes", im4m.raw.len());
if cli.dump_im4m {
let p = cli.outdir.join("im4m.der");
fs::write(&p, &im4m.raw)?;
output_paths.add("Manifest", p.display().to_string());
if cli.verbose {
eprintln!("wrote {:?}", p);
}
}
if cli.dump_im4m_certs {
let certs = parse::extract_im4m_cert_chain(&im4m.raw)?;
for (i, der) in certs.iter().enumerate() {
let der_path = cli.outdir.join(format!("im4m.cert.{i}.der"));
fs::write(&der_path, der)?;
output_paths.add(format!("Certificate {} (DER)", i), der_path.display().to_string());
let pem_path = cli.outdir.join(format!("im4m.cert.{i}.pem"));
write_pem_certificate(&pem_path, der)?;
output_paths.add(format!("Certificate {} (PEM)", i), pem_path.display().to_string());
if cli.verbose {
eprintln!("wrote {:?} and {:?}", der_path, pem_path);
}
}
}
if cli.dump_im4m_props {
let manifest = parse::extract_im4m_manifest(&im4m.raw)?;
let p = cli.outdir.join("im4m.props.json");
fs::write(&p, serde_json::to_vec_pretty(&manifest)?)?;
output_paths.add("Manifest Properties", p.display().to_string());
if cli.verbose {
eprintln!(
"wrote {:?} ({} manifest properties, {} image objects)",
p,
manifest.manifest_properties.len(),
manifest.images.len()
);
}
}
im4m_summary = Some(parse::summarize_im4m(im4m)?);
log::debug!("IM4M summary generated");
}
let mut im4r_len = None;
let mut im4r_properties = None;
if let Some(im4r) = &parsed.im4r {
log::debug!("IM4R present, length {} bytes", im4r.len());
match parse::extract_im4r_properties(im4r) {
Ok(props) => {
if let Some(bncn_prop) = props.iter().find(|p| p.key == "BNCN") {
if let parse::Im4mPropertyValue::OctetString { value: hex_nonce } = &bncn_prop.value {
if let Ok(nonce) = hex::decode(hex_nonce) {
let p = cli.outdir.join("im4r.bncn.bin");
fs::write(&p, &nonce)?;
output_paths.add("IM4R Nonce (BNCN)", p.display().to_string());
if cli.verbose {
eprintln!("extracted BNCN nonce ({} bytes) -> {:?}", nonce.len(), p);
}
}
}
} else {
log::debug!("IM4R contains no BNCN property");
}
let p = cli.outdir.join("im4r.props.json");
fs::write(&p, serde_json::to_vec_pretty(&props)?)?;
output_paths.add("IM4R Properties", p.display().to_string());
if cli.verbose {
eprintln!("wrote IM4R properties to {:?}", p);
}
im4r_properties = Some(props);
}
Err(e) => {
log::warn!("IM4R property parse error: {}", e);
}
}
if cli.dump_im4r {
let p = cli.outdir.join("im4r.der");
fs::write(&p, im4r)?;
output_paths.add("IM4R", p.display().to_string());
if cli.verbose {
eprintln!("wrote {:?}", p);
}
}
im4r_len = Some(im4r.len());
}
let summary = Summary {
container: parsed.kind,
im4p: im4p_info,
im4m: im4m_summary,
im4r_len,
im4r_properties,
notes,
output_files: output_paths
.files
.iter()
.map(|(label, path)| OutputFile { label: label.clone(), path: path.clone() })
.collect(),
};
log::debug!("Final summary prepared: {:#?}", summary);
if cli.json {
println!("{}", serde_json::to_string_pretty(&summary)?);
} else {
let formatted = formatter::format_summary(
summary.container,
summary.im4p.as_ref(),
summary.im4m.as_ref(),
summary.im4r_len,
&output_paths,
);
print!("{}", formatted);
}
Ok(())
}
fn mode_str(m: AesMode) -> &'static str {
match m {
AesMode::Cbc => "CBC",
AesMode::Ctr => "CTR",
}
}
fn run_auto(cli: &Cli, parsed: &parse::Parsed) -> Result<()> {
let im4p = parsed
.im4p
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--auto: input has no IM4P payload to decrypt"))?;
let (iv, key) = util::resolve_iv_key(cli, im4p)?;
let other = match cli.aes_mode {
AesMode::Cbc => AesMode::Ctr,
AesMode::Ctr => AesMode::Cbc,
};
let mut chosen: Option<(AesMode, Vec<u8>)> = None;
let mut fallback: Option<(AesMode, Vec<u8>)> = None;
for m in [cli.aes_mode, other] {
match crypto::decrypt_aes(&im4p.data, &iv, &key, m) {
Ok(dec) => {
let (valid, why) = util::validate_decryption(&dec);
log::debug!("--auto: {} -> valid={} ({:?})", mode_str(m), valid, why);
if valid {
chosen = Some((m, dec));
break;
}
fallback.get_or_insert((m, dec));
}
Err(e) => log::debug!("--auto: {} mode failed: {}", mode_str(m), e),
}
}
let (mode, dec, validated) = match chosen {
Some((m, d)) => (m, d, true),
None => {
let (m, d) = fallback.ok_or_else(|| {
anyhow::anyhow!(
"--auto: decryption failed in every AES mode — check the IV/key, \
or the payload may not be encrypted"
)
})?;
(m, d, false)
}
};
let mut out = cli.input.clone().into_os_string();
out.push(".decrypted");
let out = PathBuf::from(out);
fs::write(&out, &dec).with_context(|| format!("write {:?}", out))?;
if cli.json {
let obj = serde_json::json!({
"mode": mode_str(mode).to_lowercase(),
"validated": validated,
"bytes": dec.len(),
"output": out.display().to_string(),
});
println!("{}", serde_json::to_string_pretty(&obj)?);
} else if validated {
eprintln!(
"Decrypted with AES-{} -> {} ({} bytes)",
mode_str(mode),
out.display(),
dec.len()
);
} else {
eprintln!(
"WARNING: no AES mode produced a valid-looking result; wrote AES-{} output anyway \
-> {} ({} bytes). Verify the IV/key.",
mode_str(mode),
out.display(),
dec.len()
);
}
Ok(())
}
fn write_pem_certificate(path: &std::path::Path, der: &[u8]) -> Result<()> {
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
let b64 = STANDARD.encode(der);
let mut out = String::with_capacity(b64.len() * 4 / 3 + 128);
out.push_str("-----BEGIN CERTIFICATE-----\n");
for chunk in b64.as_bytes().chunks(64) {
out.push_str(std::str::from_utf8(chunk).unwrap());
out.push('\n');
}
out.push_str("-----END CERTIFICATE-----\n");
fs::write(path, out)?;
Ok(())
}