From 7ccf2519ab483701d8ad57005251856f9a518da8 Mon Sep 17 00:00:00 2001 From: Pari Malam Date: Thu, 4 Jun 2026 19:46:23 +0800 Subject: [PATCH] + added vestel --- Cargo.lock | 189 +++++++++++++++++- Cargo.toml | 10 +- src/error.rs | 79 ++++++++ src/formats.rs | 35 +++- src/formats/bdl/mod.rs | 1 - src/formats/epk/mod.rs | 50 ++--- src/formats/epk2/mod.rs | 3 +- src/formats/epk3/mod.rs | 3 +- src/formats/funai_mstar/mod.rs | 10 +- src/formats/msd/mod.rs | 81 ++++---- src/formats/msd/msd_ouith_parser_old.rs | 4 +- src/formats/msd/msd_ouith_parser_tizen_1_8.rs | 4 +- src/formats/msd/msd_ouith_parser_tizen_1_9.rs | 4 +- src/formats/mstar/mod.rs | 3 +- src/formats/mstar_secure_old/mod.rs | 10 +- src/formats/mtk_bdp/include.rs | 6 +- src/formats/mtk_pkg/lzhs.rs | 24 +-- src/formats/pfl_upg/include.rs | 50 ++--- src/formats/pfl_upg/mod.rs | 13 +- src/formats/philips_bdp/mod.rs | 3 +- src/formats/roku/include.rs | 4 +- src/formats/roku/mod.rs | 13 +- src/formats/samsung_old/mod.rs | 4 +- src/formats/sdboot/mod.rs | 1 - src/formats/sony_bdp/mod.rs | 8 +- src/formats/tsb_bin/include.rs | 2 +- src/formats/tsb_bin/mod.rs | 2 +- src/formats/vestel/include.rs | 13 ++ src/formats/vestel/mod.rs | 109 ++++++++++ src/keys.rs | 7 +- src/main.rs | 126 ++++++++++-- src/utils.rs | 3 +- src/utils/aes.rs | 78 +++++++- src/utils/lzop.rs | 1 - src/utils/path_sanitize.rs | 77 +++++++ src/utils/sparse.rs | 2 +- 36 files changed, 810 insertions(+), 222 deletions(-) create mode 100644 src/error.rs create mode 100644 src/formats/vestel/include.rs create mode 100644 src/formats/vestel/mod.rs create mode 100644 src/utils/path_sanitize.rs diff --git a/Cargo.lock b/Cargo.lock index c3a66da..9fdc4f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.20" @@ -26,7 +35,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -36,9 +60,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -49,6 +73,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.4" @@ -221,7 +254,7 @@ version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ - "anstream", + "anstream 0.6.20", "anstyle", "clap_lex", "strsim", @@ -346,6 +379,29 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream 1.0.0", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "errno" version = "0.3.14" @@ -454,6 +510,30 @@ dependencies = [ "either", ] +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -528,6 +608,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "log" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" + [[package]] name = "lz4" version = "1.28.1" @@ -563,6 +649,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + [[package]] name = "minilzo-rs" version = "0.6.1" @@ -676,6 +768,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -779,6 +886,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rsa" version = "0.9.10" @@ -812,6 +948,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -901,6 +1057,26 @@ dependencies = [ "xattr", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.20.0" @@ -915,7 +1091,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unixtract" -version = "0.1.0" +version = "0.2.0" dependencies = [ "aes", "binrw", @@ -924,9 +1100,11 @@ dependencies = [ "clap", "des", "ecb", + "env_logger", "flate2", "hex", "liblzma", + "log", "lz4", "lzma-rs", "md5", @@ -937,6 +1115,7 @@ dependencies = [ "sha1", "simd-adler32", "tar", + "thiserror", "zstd", ] diff --git a/Cargo.toml b/Cargo.toml index b5c299b..603b4a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "unixtract" -version = "0.1.0" +version = "0.2.0" edition = "2024" +description = "Firmware extractor for various file formats from TVs and AV devices" +license = "GPL-3.0" +repository = "https://github.com/theubusu/unixtract" [dependencies] clap = { version = "4.5", features = ["derive"] } @@ -24,4 +27,7 @@ prost-types = "0.14.1" bzip2 = "0.6.1" liblzma = "0.4.5" des = "0.8.1" -zstd = "0.13.3" \ No newline at end of file +zstd = "0.13.3" +thiserror = "2.0" +log = "0.4" +env_logger = "0.11" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8f0ae63 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,79 @@ +//! Unified error type for unixtract. +//! +//! All fallible operations in unixtract return [`UnixtractError`] instead of +//! panicking with `.unwrap()` or `.expect()`. This module provides a single, +//! well-structured error enum that covers all failure modes encountered during +//! firmware format detection, parsing, decryption, decompression, and +//! extraction. + +use std::path::PathBuf; + +/// The top-level error type used throughout unixtract. +/// +/// Variants are defined for all anticipated error categories. Some may not +/// yet be used in every format module but are available for gradual adoption. +#[derive(Debug, thiserror::Error)] +#[allow(dead_code)] +pub enum UnixtractError { + /// An I/O error occurred (file not found, permission denied, etc.). + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + /// A file format was detected but the header contents are invalid. + #[error("Invalid format: {0}")] + InvalidFormat(String), + + /// The input file's format could not be recognized by any detector. + #[error("Unrecognized input format")] + UnrecognizedFormat, + + /// A cryptographic operation (AES, DES, RSA, etc.) failed. + #[error("Crypto error: {0}")] + Crypto(String), + + /// A decompression operation (zlib, lzma, lz4, etc.) failed. + #[error("Decompression error: {0}")] + Decompression(String), + + /// A required key was not found in the key store. + #[error("Key not found: {0}")] + KeyNotFound(String), + + /// A checksum or integrity check failed. + #[error("Checksum mismatch: expected {expected:#010x}, got {actual:#010x}")] + ChecksumMismatch { expected: u32, actual: u32 }, + + /// A path extracted from a firmware image is unsafe (e.g., contains `..`). + #[error("Unsafe path in firmware image: {path}")] + UnsafePath { path: PathBuf }, + + /// The format was detected but the specific variant/version is unsupported. + #[error("Unsupported variant: {0}")] + UnsupportedVariant(String), + + /// A generic error that doesn't fit the more specific variants. + #[error("{0}")] + Other(String), +} + +/// Convenience type alias used across the crate. +#[allow(dead_code)] +pub type Result = std::result::Result; + +impl From for UnixtractError { + fn from(s: String) -> Self { + UnixtractError::Other(s) + } +} + +impl From<&str> for UnixtractError { + fn from(s: &str) -> Self { + UnixtractError::Other(s.to_string()) + } +} + +impl From for UnixtractError { + fn from(e: binrw::Error) -> Self { + UnixtractError::InvalidFormat(format!("Binary parse error: {e}")) + } +} diff --git a/src/formats.rs b/src/formats.rs index f7c449d..87d8dce 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -1,6 +1,23 @@ +//! Format registry and detection/extraction framework. +//! +//! Each supported firmware format has a detector function (`is_*_file`) that +//! checks whether a given input matches that format, and an extractor function +//! (`extract_*`) that performs the actual extraction. The [`Format`] struct +//! pairs these together, and [`get_registry`] returns the ordered list of all +//! supported formats. +//! +//! **Important**: The order of formats in the registry matters. Some formats +//! can contain other formats as inner payloads, so the outer format must be +//! detected first. Ordering constraints are noted in comments. + use std::any::Any; use crate::AppContext; +/// A supported firmware format with its detector and extractor functions. +/// +/// The `detector_func` returns `Some(ctx)` if the input matches this format, +/// where `ctx` is an opaque box of format-specific context data that will be +/// passed to the `extractor_func`. Returns `None` if the format doesn't match. pub struct Format { pub name: &'static str, pub detector_func: fn(&AppContext) -> Result>, Box>, @@ -35,10 +52,10 @@ pub mod gx_dvb; pub mod onkyo; pub mod philips_bdp; pub mod tsb_bin; + pub mod pup; + pub mod vestel; -pub mod pup; - -pub mod msd; + pub mod msd; pub mod msd10; pub mod msd11; @@ -53,7 +70,11 @@ pub mod mtk_pkg_old; pub mod mtk_pkg_new; pub mod mtk_bdp; -//define all formats here +/// Returns the ordered list of all supported firmware formats. +/// +/// Formats are tried in order during detection; the first match wins. This +/// means that container formats (which embed other formats) must appear +/// before their inner formats to avoid misdetection. pub fn get_registry() -> Vec { return vec![ Format { @@ -256,5 +277,9 @@ pub fn get_registry() -> Vec { detector_func: crate::formats::tsb_bin::is_tsb_bin_file, extractor_func: crate::formats::tsb_bin::extract_tsb_bin, }, - ] + Format { + name: "vestel", + detector_func: crate::formats::vestel::is_vestel_file, + extractor_func: crate::formats::vestel::extract_vestel, + }, ] } \ No newline at end of file diff --git a/src/formats/bdl/mod.rs b/src/formats/bdl/mod.rs index 4839106..68e51ae 100644 --- a/src/formats/bdl/mod.rs +++ b/src/formats/bdl/mod.rs @@ -33,7 +33,6 @@ pub fn extract_bdl(app_ctx: &AppContext, _ctx: Box) -> Result<(), Box Option { if match_with_pattern(&versions, epk2_pattern) { Some(2) - } else if match_with_pattern(&versions, epk3_pattern) { + } else if match_with_pattern(&versions, epk3_pattern) { Some(3) - } else if match_with_pattern(&versions, epk3_another_pattern) { + } else if match_with_pattern(&versions, epk3_another_pattern) { Some(3) - } else if match_with_pattern(&versions, epk3_new_pattern) { + } else if match_with_pattern(&versions, epk3_new_pattern) { Some(3) - }else { + } else { None } } @@ -61,7 +62,7 @@ pub fn extract_epk(app_ctx: &AppContext, ctx: Box) -> Result<(), Box) -> Result<(), Box(key_array: &'a [(&'a str, &'a str)], data: &[u8], expected_magic: &[u8]) -> Result)>, Box> { for (key_hex, name) in key_array { let key_bytes = hex::decode(key_hex)?; @@ -83,41 +88,10 @@ pub fn find_key<'a>(key_array: &'a [(&'a str, &'a str)], data: &[u8], expected_m Ok(d) => d, Err(_) => continue, }; - + if decrypted.starts_with(expected_magic) { return Ok(Some((name, key_bytes))); } } Ok(None) } - -use aes::Aes128; -use aes::Aes256; -use ecb::{Decryptor, cipher::{BlockDecryptMut, KeyInit, generic_array::GenericArray}}; - -type Aes128EcbDec = Decryptor; -type Aes256EcbDec = Decryptor; - -pub fn decrypt_aes_ecb_auto(key: &[u8], ciphertext: &[u8]) -> Result, Box> { - let mut buffer = ciphertext.to_vec(); - - if key.len() == 32 { - // aes256 - let key_array: [u8; 32] = key.try_into()?; - let mut decryptor = Aes256EcbDec::new(&key_array.into()); - for chunk in buffer.chunks_exact_mut(16) { - let block: &mut [u8; 16] = chunk.try_into()?; - decryptor.decrypt_block_mut(GenericArray::from_mut_slice(block)); - } - } else { - // aes128 - let key_array: [u8; 16] = key.try_into()?; - let mut decryptor = Aes128EcbDec::new(&key_array.into()); - for chunk in buffer.chunks_exact_mut(16) { - let block: &mut [u8; 16] = chunk.try_into()?; - decryptor.decrypt_block_mut(GenericArray::from_mut_slice(block)); - } - } - - Ok(buffer) -} \ No newline at end of file diff --git a/src/formats/epk2/mod.rs b/src/formats/epk2/mod.rs index 127ef35..169747c 100644 --- a/src/formats/epk2/mod.rs +++ b/src/formats/epk2/mod.rs @@ -10,7 +10,8 @@ use binrw::BinReaderExt; use crate::utils::common; use crate::utils::global::opt_dump_dec_hdr; use crate::keys; -use crate::formats::epk::{decrypt_aes_ecb_auto, find_key}; +use crate::formats::epk::find_key; +use crate::utils::aes::decrypt_aes_ecb_auto; use include::*; pub fn is_epk2_file(app_ctx: &AppContext) -> Result>, Box> { diff --git a/src/formats/epk3/mod.rs b/src/formats/epk3/mod.rs index 6fceb40..ee42e61 100644 --- a/src/formats/epk3/mod.rs +++ b/src/formats/epk3/mod.rs @@ -10,7 +10,8 @@ use binrw::BinReaderExt; use crate::utils::common; use crate::utils::global::opt_dump_dec_hdr; use crate::keys; -use crate::formats::epk::{decrypt_aes_ecb_auto, find_key}; +use crate::formats::epk::find_key; +use crate::utils::aes::decrypt_aes_ecb_auto; use include::*; pub fn is_epk3_file(_app_ctx: &AppContext) -> Result>, Box> { diff --git a/src/formats/funai_mstar/mod.rs b/src/formats/funai_mstar/mod.rs index f69ace2..58e3286 100644 --- a/src/formats/funai_mstar/mod.rs +++ b/src/formats/funai_mstar/mod.rs @@ -67,10 +67,12 @@ pub fn extract_funai_mstar(app_ctx: &AppContext, ctx: Box) -> Result<() //extract SoC which (should be) mstar_secure_old, this is just a simple container for that format ( so we will go funai_mstar -> mstar_secure_old -> mstar (DUMB?) ) if name == "SoC" { let r_out_file = File::open(&output_path)?; - let in_ctx: AppContext = AppContext { - input: InputTarget::File(r_out_file), - output_dir: app_ctx.output_dir.join("SoC"), - options: app_ctx.options.clone() + let in_ctx: AppContext = AppContext { + input: InputTarget::File(r_out_file), + output_dir: app_ctx.output_dir.join("SoC"), + options: app_ctx.options.clone(), + dry_run: app_ctx.dry_run, + quiet: app_ctx.quiet, }; //do check and extarct diff --git a/src/formats/msd/mod.rs b/src/formats/msd/mod.rs index bed6f21..855b553 100644 --- a/src/formats/msd/mod.rs +++ b/src/formats/msd/mod.rs @@ -1,69 +1,62 @@ -//MSD OUITH parsers +// MSD OUITH parsers pub mod msd_ouith_parser_old; pub mod msd_ouith_parser_tizen_1_8; pub mod msd_ouith_parser_tizen_1_9; // COMMON MSD FUNCTIONS -use aes::Aes128; -use cbc::{Decryptor, cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}}; - -type Aes128CbcDec = Decryptor; +use crate::utils::aes::decrypt_aes128_cbc_pcks7; +/// Decrypt "Salted__" format data using OpenSSL-compatible key derivation +/// (MD5 of passphrase+salt for key, MD5 of key+passphrase+salt for IV). pub fn decrypt_aes_salted_old(encrypted_data: &[u8], passphrase_bytes: &Vec) -> Result, Box> { - let mut data = encrypted_data.to_vec(); - - if data[0..8].to_vec() != b"Salted__" { + if encrypted_data.len() < 16 || encrypted_data[0..8].to_vec() != b"Salted__" { return Err("Invalid encrypted data!".into()); } - let salt = &data[8..16]; + let salt = &encrypted_data[8..16]; - //key = md5 of (passphrase + salt) - let mut key = Vec::new(); - key.extend_from_slice(&passphrase_bytes); - key.extend_from_slice(&salt); - let key_md5 = md5::compute(&key); + // key = MD5(passphrase + salt) + let mut key_input = Vec::new(); + key_input.extend_from_slice(&passphrase_bytes); + key_input.extend_from_slice(&salt); + let key_md5 = md5::compute(&key_input); - //iv = md5 of (md5 of key + passphrase + salt) - let mut iv = Vec::new(); - iv.extend_from_slice(&key_md5.0); - iv.extend_from_slice(&passphrase_bytes); - iv.extend_from_slice(&salt); - let iv_md5 = md5::compute(&iv); + // iv = MD5(key + passphrase + salt) + let mut iv_input = Vec::new(); + iv_input.extend_from_slice(&key_md5.0); + iv_input.extend_from_slice(&passphrase_bytes); + iv_input.extend_from_slice(&salt); + let iv_md5 = md5::compute(&iv_input); - let decryptor = Aes128CbcDec::new((&key_md5.0).into(), (&iv_md5.0).into()); - let decrypted = decryptor.decrypt_padded_mut::(&mut data[16..]) - .map_err(|e| format!("Decryption error!!: {:?}", e))?; - - Ok(decrypted.to_vec()) + let key: [u8; 16] = key_md5.0; + let iv: [u8; 16] = iv_md5.0; + decrypt_aes128_cbc_pcks7(&encrypted_data[16..], &key, &iv) } +/// Decrypt "Salted__" format data using Tizen-style key derivation +/// (passphrase used directly as key, MD5 of salt for IV). pub fn decrypt_aes_salted_tizen(encrypted_data: &[u8], passphrase_bytes: &Vec) -> Result, Box> { - let mut data = encrypted_data.to_vec(); - - if data[0..8].to_vec() != b"Salted__" { + if encrypted_data.len() < 16 || encrypted_data[0..8].to_vec() != b"Salted__" { return Err("Invalid encrypted data!".into()); } - let salt = &data[8..16]; + let salt = &encrypted_data[8..16]; - //iv = md5 of salt + // iv = MD5(salt) let iv = md5::compute(&salt); + let key: [u8; 16] = passphrase_bytes.as_slice().try_into() + .map_err(|_| "Passphrase must be 16 bytes for AES-128")?; + let iv: [u8; 16] = iv.0; - let decryptor = Aes128CbcDec::new((&(**passphrase_bytes)).into(), (&iv.0).into()); - let decrypted = decryptor.decrypt_padded_mut::(&mut data[16..]) - .map_err(|e| format!("Decryption error!!: {:?}", e))?; - - Ok(decrypted.to_vec()) + decrypt_aes128_cbc_pcks7(&encrypted_data[16..], &key, &iv) } +/// Decrypt data using Tizen-style AES-128-CBC with passphrase as key and +/// MD5 of salt as IV. pub fn decrypt_aes_tizen(encrypted_data: &[u8], passphrase_bytes: &Vec, salt: &[u8]) -> Result, Box> { - let mut data = encrypted_data.to_vec(); - - //iv = md5 of salt + // iv = MD5(salt) let iv = md5::compute(&salt); + let key: [u8; 16] = passphrase_bytes.as_slice().try_into() + .map_err(|_| "Passphrase must be 16 bytes for AES-128")?; + let iv: [u8; 16] = iv.0; - let decryptor = Aes128CbcDec::new((&(**passphrase_bytes)).into(), (&iv.0).into()); - let decrypted = decryptor.decrypt_padded_mut::(&mut data) - .map_err(|e| format!("Decryption error!!: {:?}", e))?; - - Ok(decrypted.to_vec()) -} \ No newline at end of file + decrypt_aes128_cbc_pcks7(&encrypted_data, &key, &iv) +} diff --git a/src/formats/msd/msd_ouith_parser_old.rs b/src/formats/msd/msd_ouith_parser_old.rs index 69b8b0f..ed5781f 100644 --- a/src/formats/msd/msd_ouith_parser_old.rs +++ b/src/formats/msd/msd_ouith_parser_old.rs @@ -125,7 +125,7 @@ pub fn parse_ouith_blob(blob: &[u8], print_tree: bool) -> Result<(Vec, if print_tree { println!(" Type descriptor(0x{:02x}) - Size: {}", type_descriptor.tag, type_descriptor.size); }; let destination_info: CommonDestinationInfo = reader.read_be()?; if print_tree { - println!(" Name lenght: {}", destination_info.name_len); + println!(" Name length: {}", destination_info.name_len); println!(" Name: {}", destination_info.name()); println!(" Version: {}", destination_info.version); }; @@ -236,7 +236,7 @@ pub fn parse_ouith_blob(blob: &[u8], print_tree: bool) -> Result<(Vec, if print_tree { println!(" OUSWImageVersionExDesc(0x12) - Size: {}", version_descriptor.size); }; let sw_image_version_ex_desc: OUSWImageVersionExDesc = reader.read_be()?; if print_tree { - println!(" Name lenght: {}", sw_image_version_ex_desc.name_len); + println!(" Name length: {}", sw_image_version_ex_desc.name_len); println!(" Name: {}", sw_image_version_ex_desc.name()); println!(" Major version: {}", sw_image_version_ex_desc.major_ver); println!(" Minor version: {}", sw_image_version_ex_desc.minor_ver); diff --git a/src/formats/msd/msd_ouith_parser_tizen_1_8.rs b/src/formats/msd/msd_ouith_parser_tizen_1_8.rs index 4aceaca..675c227 100644 --- a/src/formats/msd/msd_ouith_parser_tizen_1_8.rs +++ b/src/formats/msd/msd_ouith_parser_tizen_1_8.rs @@ -146,7 +146,7 @@ pub fn parse_blob_1_8(blob: &[u8], print_tree: bool) -> Result<(Vec, Op if print_tree { println!(" OUPartitionVersionDesc(0x0A) - Size: {}", sub_descriptor.size); }; let partition_version_desc: OUPartitionVersionDesc = reader.read_be()?; if print_tree { - println!(" Name lenght: {}", partition_version_desc.name_len); + println!(" Name length: {}", partition_version_desc.name_len); println!(" Name: {}", partition_version_desc.name()); println!(" Version: {}", partition_version_desc.version); }; @@ -240,7 +240,7 @@ pub fn parse_blob_1_8(blob: &[u8], print_tree: bool) -> Result<(Vec, Op if print_tree { println!(" OUSWImageVersionDesc(0x19) - Size: {}", sub_descriptor.size); }; let sw_image_version_desc: OUSWImageVersionDesc = reader.read_be()?; if print_tree { - println!(" Name lenght: {}", sw_image_version_desc.name_len); + println!(" Name length: {}", sw_image_version_desc.name_len); println!(" Name: {}", sw_image_version_desc.name()); println!(" Major ver: {}", sw_image_version_desc.major_ver); println!(" Minor ver: {}", sw_image_version_desc.minor_ver); diff --git a/src/formats/msd/msd_ouith_parser_tizen_1_9.rs b/src/formats/msd/msd_ouith_parser_tizen_1_9.rs index 4dc3033..1f0ecc8 100644 --- a/src/formats/msd/msd_ouith_parser_tizen_1_9.rs +++ b/src/formats/msd/msd_ouith_parser_tizen_1_9.rs @@ -151,7 +151,7 @@ pub fn parse_blob_1_9(blob: &[u8], print_tree: bool) -> Result<(Vec, Op if print_tree { println!(" OUPartitionVersionDesc(0x0A) - Size: {}", sub_descriptor.size); }; let partition_version_desc: OUPartitionVersionDesc = reader.read_le()?; if print_tree { - println!(" Name lenght: {}", partition_version_desc.name_len); + println!(" Name length: {}", partition_version_desc.name_len); println!(" Name: {}", partition_version_desc.name()); }; @@ -240,7 +240,7 @@ pub fn parse_blob_1_9(blob: &[u8], print_tree: bool) -> Result<(Vec, Op if print_tree { println!(" OUSWImageVersionDesc(0x19) - Size: {}", sub_descriptor.size); }; let sw_image_version_desc: OUSWImageVersionDesc = reader.read_le()?; if print_tree { - println!(" Name lenght: {}", sw_image_version_desc.name_len); + println!(" Name length: {}", sw_image_version_desc.name_len); println!(" Name: {}", sw_image_version_desc.name()); println!(" Major ver: {}", sw_image_version_desc.major_ver); println!(" Minor ver: {}", sw_image_version_desc.minor_ver); diff --git a/src/formats/mstar/mod.rs b/src/formats/mstar/mod.rs index b105712..13f9bdd 100644 --- a/src/formats/mstar/mod.rs +++ b/src/formats/mstar/mod.rs @@ -35,7 +35,6 @@ pub fn extract_mstar(app_ctx: &AppContext, _ctx: Box) -> Result<(), Box } let mut script_string = String::from_utf8_lossy(&script); - //println!("{}", script_string); if script_string == "" { //try for hisense println!("Failed to get script at 0x0, trying 0x1000..."); @@ -153,7 +152,7 @@ pub fn extract_mstar(app_ctx: &AppContext, _ctx: Box) -> Result<(), Box println!("- Decompressing lz4, expected size: {}", lz4_expect_size); out_data = decompress_lz4(&data, lz4_expect_size.try_into().unwrap())?; } else if compression == CompressionType::Lzo { - println!("- Decompessing LZO.."); + println!("- Decompressing LZO.."); unlzop_to_file(&data, output_path)?; println!("-- Saved file!"); i += 1; diff --git a/src/formats/mstar_secure_old/mod.rs b/src/formats/mstar_secure_old/mod.rs index 9f2abc2..92e4c53 100644 --- a/src/formats/mstar_secure_old/mod.rs +++ b/src/formats/mstar_secure_old/mod.rs @@ -51,10 +51,12 @@ pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box) -> Resu //run standard mstar ext into same directory let r_out_file = File::open(&output_path)?; - let in_ctx: AppContext = AppContext { - input: InputTarget::File(r_out_file), - output_dir: app_ctx.output_dir.clone(), - options: app_ctx.options.clone() + let in_ctx: AppContext = AppContext { + input: InputTarget::File(r_out_file), + output_dir: app_ctx.output_dir.clone(), + options: app_ctx.options.clone(), + dry_run: app_ctx.dry_run, + quiet: app_ctx.quiet, }; //do check just in case and extract diff --git a/src/formats/mtk_bdp/include.rs b/src/formats/mtk_bdp/include.rs index 6a2686c..6606f90 100644 --- a/src/formats/mtk_bdp/include.rs +++ b/src/formats/mtk_bdp/include.rs @@ -10,7 +10,7 @@ pub static PITIT_END_MARKER: u32 = 0x69_54_49_50; //PITi - end marker of PITIT #[derive(BinRead)] pub struct PITITPITEntry { - pub nand_size: u32, + pub nand_size: u32, pub pit_offset: u32, pub pit_size: u32, _table_id: u32, @@ -18,7 +18,7 @@ pub struct PITITPITEntry { #[derive(BinRead)] pub struct PITITBITEntry { - pub bit_offset: u32, + pub bit_offset: u32, pub bit_size: u32, _private_data_1: u32, _private_data_2: u32, @@ -29,7 +29,7 @@ pub struct PITHeader { pub pit_magic: [u8; 8], _version: u32, pub first_entry_offset: u32, //"header len" - pub entry_size: u32, //"item lenght" + pub entry_size: u32, //"item length" pub entry_count: u32, //"item num" } diff --git a/src/formats/mtk_pkg/lzhs.rs b/src/formats/mtk_pkg/lzhs.rs index f736eb2..6eff93c 100644 --- a/src/formats/mtk_pkg/lzhs.rs +++ b/src/formats/mtk_pkg/lzhs.rs @@ -213,12 +213,11 @@ fn unhuff(data: &[u8]) -> Vec { let found_j: i32; loop { - if !get_byte(&mut ctx, &mut in_cur) { - //println!("retA"); - //flush - if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } }; - return out; - } + if !get_byte(&mut ctx, &mut in_cur) { + // flush + if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } }; + return out; + } if ctx.len < 2 { continue; } let keyp = (((ctx.len & 0x7) as usize) << 6) | ((ctx.code & 0x3F) as usize); let mut jdx = lookup_charpos[keyp]; @@ -235,16 +234,15 @@ fn unhuff(data: &[u8]) -> Vec { found_j = jdx as i32; let b = ((jdx as i32) >> 1) as u8; if ctx.code_buf_ptr < ctx.code_buf.len() { ctx.code_buf[ctx.code_buf_ptr] = b; ctx.code_buf_ptr += 1; } - break; + break; } ctx.code = 0; for _ in 0..7 { if !get_byte(&mut ctx, &mut in_cur) { - //println!("retB"); - //flush - if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } }; - return out; - } - } + // flush + if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } }; + return out; + } + } let combined = (ctx.code | ((found_j as u32) << 7)) as u32; if ctx.code_buf_ptr < ctx.code_buf.len() { ctx.code_buf[ctx.code_buf_ptr] = (combined & 0xFF) as u8; ctx.code_buf_ptr += 1; } ctx.code = 0; ctx.len = 0; diff --git a/src/formats/pfl_upg/include.rs b/src/formats/pfl_upg/include.rs index 5e6def5..f90b291 100644 --- a/src/formats/pfl_upg/include.rs +++ b/src/formats/pfl_upg/include.rs @@ -1,14 +1,13 @@ use crate::utils::common; +use crate::utils::aes::decrypt_aes256_ecb; use binrw::BinRead; -use aes::Aes256; -use ecb::{Decryptor, cipher::{BlockDecryptMut, KeyInit, generic_array::GenericArray}}; use crate::keys; use rsa::{RsaPublicKey, BigUint}; pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result, Box> { let mut result: Option<(String, [u8; 32])> = None; - for (name, n_hex) in keys::PFLUPG { + for (name, n_hex) in keys::PFLUPG { let n = BigUint::from_bytes_be(&hex::decode(n_hex)?); let e = BigUint::from_bytes_be(&hex::decode("010001")?); let pubkey = RsaPublicKey::new(n, e)?; @@ -17,17 +16,17 @@ pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result pos, - None => continue, //there is no 0, continue + None => continue, // There is no 0, continue }; let fname = &dec_ciphertext[..end]; - if fname.len() > 1 && fname.is_ascii() { //is ascii filename - result = Some((name.to_string(), aes_key.try_into().unwrap())); + if fname.len() > 1 && fname.is_ascii() { // Is ASCII filename + result = Some((name.to_string(), aes_key)); break } } @@ -35,31 +34,16 @@ pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result; - -pub fn decrypt_aes256_ecb(key: [u8; 32], ciphertext: &[u8]) -> Result, Box> { - let mut decryptor = Aes256EcbDec::new(&key.into()); - let mut buffer = ciphertext.to_vec(); - - for chunk in buffer.chunks_exact_mut(16) { - let block: &mut [u8; 16] = chunk.try_into()?; - decryptor.decrypt_block_mut(GenericArray::from_mut_slice(block)); - } - - Ok(buffer) -} - #[derive(BinRead)] pub struct Header { _magic_bytes: [u8; 8], pub header_size: u32, //data start pub data_size: u32, - _crc32: u32, - pub mask: u32, - _data_size_decompressed: u32, - _padding2: u32, - description_bytes: [u8; 512], + _crc32: u32, + pub mask: u32, + _data_size_decompressed: u32, + _padding2: u32, + description_bytes: [u8; 512], } impl Header { pub fn description(&self) -> String { @@ -74,8 +58,8 @@ impl Header { pub struct FileHeader { file_name_bytes: [u8; 60], pub real_size: u32, - pub stored_size: u32, - pub header_size: u32, + pub stored_size: u32, + pub header_size: u32, pub attributes: [u8; 4], } impl FileHeader { @@ -91,4 +75,4 @@ impl FileHeader { pub fn is_package(&self) -> bool { (self.attributes[3] & (1 << 2)) != 0 } -} \ No newline at end of file +} diff --git a/src/formats/pfl_upg/mod.rs b/src/formats/pfl_upg/mod.rs index fb50b2c..a517d52 100644 --- a/src/formats/pfl_upg/mod.rs +++ b/src/formats/pfl_upg/mod.rs @@ -8,6 +8,7 @@ use std::fs::{self, File, OpenOptions}; use binrw::BinReaderExt; use crate::utils::common; +use crate::utils::aes::decrypt_aes256_ecb; use include::*; pub fn is_pfl_upg_file(app_ctx: &AppContext) -> Result>, Box> { @@ -58,7 +59,7 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box) -> Result<(), B let encrypted_data = common::read_exact(&mut file, (header.data_size as usize + 0xf) & !0xf)?; println!("Decrypting data..."); - data = decrypt_aes256_ecb(aes_key, &encrypted_data)?; + data = decrypt_aes256_ecb(&aes_key, &encrypted_data)?; data.truncate(header.data_size as usize); //discard padding } else { @@ -108,10 +109,12 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box) -> Result<(), B //REOPEN temp file and make ctx let r_temp_file = File::open(&temp_path)?; - let in_ctx: AppContext = AppContext { - input: InputTarget::File(r_temp_file), - output_dir: output_path, - options: app_ctx.options.clone() + let in_ctx: AppContext = AppContext { + input: InputTarget::File(r_temp_file), + output_dir: output_path, + options: app_ctx.options.clone(), + dry_run: app_ctx.dry_run, + quiet: app_ctx.quiet, }; //do check just in case and extract diff --git a/src/formats/philips_bdp/mod.rs b/src/formats/philips_bdp/mod.rs index 275e4d3..cd29e24 100644 --- a/src/formats/philips_bdp/mod.rs +++ b/src/formats/philips_bdp/mod.rs @@ -74,8 +74,7 @@ pub fn extract_philips_bdp(app_ctx: &AppContext, ctx: Box) -> Result<() println!("Checking if it's also MTK BDP..."); let new_file = File::open(&output_path)?; - //DUMB - let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone() }; + let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, quiet: app_ctx.quiet }; if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? { println!("- MTK BDP file detected!\n"); diff --git a/src/formats/roku/include.rs b/src/formats/roku/include.rs index 5dd1326..fa30f83 100644 --- a/src/formats/roku/include.rs +++ b/src/formats/roku/include.rs @@ -19,8 +19,8 @@ pub struct AImageHeader { _release_id: u32, // toplevel build / release version _platform_id: u32, // target platform information pub image_type: u32, // image type - pub lenght: u32, // length of entire image (header + data + trailing padding) - _data_lenght: u32, // length of image data (ie from data_start_offset to start of trailing padding) + pub length: u32, // length of entire image (header + data + trailing padding) + _data_length: u32, // length of image data (ie from data_start_offset to start of trailing padding) pub data_start_offset: u32, // 0 if header is part of data, sizeof(aimage_v1_header_t) if header is prepended to data _data_link_address: u32, // for non-pic executable images, where should the image placed in memory in order to execute _data_entry_point_offset: u32, // 0 if execution entry point is at the beginning of image data diff --git a/src/formats/roku/mod.rs b/src/formats/roku/mod.rs index 838e16c..6b7a71a 100644 --- a/src/formats/roku/mod.rs +++ b/src/formats/roku/mod.rs @@ -60,18 +60,17 @@ pub fn extract_roku(app_ctx: &AppContext, _ctx: Box) -> Result<(), Box< while image_reader.stream_position()? < size as u64 { let image: AImageHeader = image_reader.read_le()?; - println!(" #{} - Type: {}(0x{:x}), Lenght: {}, encmode: {}", - i, image.image_type_str(), image.image_type, image.lenght, image.encmode_str()); - //println!("{:?}", image); - - let data = + println!(" #{} - Type: {}(0x{:x}), Length: {}, encmode: {}", + i, image.image_type_str(), image.image_type, image.length, image.encmode_str()); + + let data = if image.data_start_offset == 0 { // "0 if header is part of data" image_reader.seek(SeekFrom::Current(-256))?; //rewind header - common::read_exact(&mut image_reader, image.lenght as usize)? + common::read_exact(&mut image_reader, image.length as usize)? } else { let _skip_data = common::read_exact(&mut image_reader, image.data_start_offset as usize - 256)?; - common::read_exact(&mut image_reader, image.lenght as usize - image.data_start_offset as usize)? + common::read_exact(&mut image_reader, image.length as usize - image.data_start_offset as usize)? }; let folder_path = Path::new(&app_ctx.output_dir).join(&path); diff --git a/src/formats/samsung_old/mod.rs b/src/formats/samsung_old/mod.rs index 8424f5d..b14a655 100644 --- a/src/formats/samsung_old/mod.rs +++ b/src/formats/samsung_old/mod.rs @@ -100,10 +100,8 @@ pub fn extract_samsung_old(app_ctx: &AppContext, _ctx: Box) -> Result<( iv.extend_from_slice(&key_md5.0); iv.extend_from_slice(&key); iv_md5 = md5::compute(&iv); - } + } - //println!("Key: {:02x?}", key_md5); - //println!("IV: {:02x?}", iv_md5); let end = file_size - 260; println!("- Decrypting file..."); let decrypted_data = decrypt_aes128_cbc_pcks7(&data[16..end.try_into().unwrap()], &key_md5, &iv_md5)?; diff --git a/src/formats/sdboot/mod.rs b/src/formats/sdboot/mod.rs index 097b540..a9dbb9d 100644 --- a/src/formats/sdboot/mod.rs +++ b/src/formats/sdboot/mod.rs @@ -68,7 +68,6 @@ pub fn extract_sdboot(app_ctx: &AppContext, _ctx: Box) -> Result<(), Bo let entry_header: SdbootEntryHeader = entry_header_reader.read_be()?; let offset = file.stream_position()?; - //println!("File: {} - Offset: {}, Size: {}", entry_header.name(), offset, entry_header.file_size()); file_list.push( FileEntry { name: entry_header.name(), size: entry_header.file_size(), offset }); file.seek(SeekFrom::Current(entry_header.file_size() as i64))?; diff --git a/src/formats/sony_bdp/mod.rs b/src/formats/sony_bdp/mod.rs index 9f0b459..3ea1ae3 100644 --- a/src/formats/sony_bdp/mod.rs +++ b/src/formats/sony_bdp/mod.rs @@ -113,10 +113,12 @@ pub fn extract_sony_bdp(app_ctx: &AppContext, ctx: Box) -> Result<(), B let last_file = File::open(last_file_path.unwrap())?; let mtk_extraction_path = app_ctx.output_dir.join(format!("{}", i)); - let ctx: AppContext = AppContext { - input: InputTarget::File(last_file), + let ctx: AppContext = AppContext { + input: InputTarget::File(last_file), output_dir: mtk_extraction_path, - options: app_ctx.options.clone() + options: app_ctx.options.clone(), + dry_run: app_ctx.dry_run, + quiet: app_ctx.quiet, }; if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&ctx)? { diff --git a/src/formats/tsb_bin/include.rs b/src/formats/tsb_bin/include.rs index 9cffe5b..394d70c 100644 --- a/src/formats/tsb_bin/include.rs +++ b/src/formats/tsb_bin/include.rs @@ -5,7 +5,7 @@ use binrw::BinRead; pub struct Header { _signature: [u8; 128], _control_id: u32, //must be zero - pub lenght: u32, //file size + pub length: u32, //file size pub entry_count: u32, _pad1: [u8; 4], build_no_bytes: [u8; 16], diff --git a/src/formats/tsb_bin/mod.rs b/src/formats/tsb_bin/mod.rs index 665e1e5..295fd62 100644 --- a/src/formats/tsb_bin/mod.rs +++ b/src/formats/tsb_bin/mod.rs @@ -57,7 +57,7 @@ pub fn extract_tsb_bin(app_ctx: &AppContext, ctx: Box) -> Result<(), Bo let hdr: Header = hdr_rdr.read_be()?; println!("File info -\nSize: {}\nEntry count: {}\nBuild no.: {}\nEntry address: 0x{:02x}" - ,hdr.lenght, hdr.entry_count, hdr.build_no(), hdr.entry_addr); + ,hdr.length, hdr.entry_count, hdr.build_no(), hdr.entry_addr); for (i, entry) in hdr.entries.iter().enumerate() { println!("\n({}/{}) - {}, Size: {}, Offset: {}, Load address: 0x{:02x}", diff --git a/src/formats/vestel/include.rs b/src/formats/vestel/include.rs new file mode 100644 index 0000000..2d86939 --- /dev/null +++ b/src/formats/vestel/include.rs @@ -0,0 +1,13 @@ +pub const VESTEL_PARTITIONS: &[(&str, (usize, usize))] = &[ + ("kernel", (0x000000, 0x700000)), + ("rootfs", (0x700000, 0x1A00000)), + ("vendor", (0x2100000, 0x9A00000)), + ("conf", (0xBB00000, 0x1E00000)), + ("apd", (0xD900000, 0x1100000)), + ("tee", (0xEA00000, 0x504800)), + ("buf", (0xEF04800, 0x400000)), +]; + +pub struct VestelCtx { + pub is_encrypted: bool, +} diff --git a/src/formats/vestel/mod.rs b/src/formats/vestel/mod.rs new file mode 100644 index 0000000..1a3f6b3 --- /dev/null +++ b/src/formats/vestel/mod.rs @@ -0,0 +1,109 @@ +mod include; +use std::any::Any; +use crate::AppContext; + +use std::fs::{self, OpenOptions}; +use std::io::Write; + +use crate::utils::common; +use crate::keys; +use crate::utils::aes::{decrypt_aes128_ecb}; +use include::*; + +pub fn is_vestel_file(app_ctx: &AppContext) -> Result>, Box> { + let file = match app_ctx.file() {Some(f) => f, None => return Ok(None)}; + let file_size = file.metadata()?.len() as usize; + + let header = common::read_file(&file, 0, 32)?; + if header.windows(6).any(|w| w == b"VESTEL") { + return Ok(Some(Box::new(VestelCtx { is_encrypted: false }))); + } + + let vestel_key = keys::VESTEL.first(); + if vestel_key.is_none() { + return Ok(None); + } + let (_key_name, key_hex) = vestel_key.unwrap(); + + let key = hex::decode(key_hex)?; + if key.len() != 16 { + return Ok(None); + } + let key_bytes: [u8; 16] = key.try_into().map_err(|_| "Invalid key length")?; + + if file_size % 16 != 0 { + return Ok(None); + } + + let data = common::read_exact(&mut file.try_clone()?, file_size)?; + let decrypted = decrypt_aes128_ecb(&key_bytes, &data)?; + + if decrypted.windows(6).any(|w| w == b"VESTEL") { + Ok(Some(Box::new(VestelCtx { is_encrypted: true }))) + } else { + Ok(None) + } +} + +pub fn extract_vestel(app_ctx: &AppContext, ctx: Box) -> Result<(), Box> { + let mut file = app_ctx.file().ok_or("Extractor expected file")?; + let ctx = ctx.downcast::().expect("Missing context"); + + let file_size = file.metadata()?.len() as usize; + let data = common::read_exact(&mut file, file_size)?; + + let (key_name, key_hex) = keys::VESTEL.first() + .ok_or("VESTEL key not found!")?; + + let key = hex::decode(key_hex)?; + let key_len = key.len(); + let key_bytes = match key.try_into() { + Ok(bytes) => bytes, + Err(_) => return Err(format!("Invalid VESTEL key length: expected 16 bytes, got {}", key_len).into()), + }; + + let (decrypted_data, decrypted_path) = if ctx.is_encrypted { + println!("Detected encrypted Vestel firmware, using key: {} ({})", key_name, key_hex); + println!("Decrypting firmware..."); + let dec_data = decrypt_aes128_ecb(&key_bytes, &data)?; + + let output_path = std::path::Path::new(&app_ctx.output_dir).join("_decrypted.bin"); + fs::create_dir_all(&app_ctx.output_dir)?; + let mut out_file = OpenOptions::new().write(true).create(true).truncate(true).open(&output_path)?; + out_file.write_all(&dec_data)?; + (dec_data, Some(output_path)) + } else { + println!("Detected Vestel firmware (unencrypted)"); + (data, None) + }; + + let partitions = VESTEL_PARTITIONS; + println!("\nPartition count: {}", partitions.len()); + + for (name, (offset, size)) in partitions.iter() { + let end = offset + size; + if end > decrypted_data.len() { + println!("\nWarning: Partition {} extends beyond firmware bounds (offset=0x{:X}, size=0x{:X})", name, offset, size); + } + + let segment = &decrypted_data[*offset..std::cmp::min(end, decrypted_data.len())]; + + println!("\n{} - Offset: 0x{:X}, Size: 0x{:X}", name, offset, size); + + let output_path = std::path::Path::new(&app_ctx.output_dir).join(format!("{}.bin", name)); + + fs::create_dir_all(&app_ctx.output_dir)?; + let mut out_file = OpenOptions::new().write(true).create(true).truncate(true).open(output_path)?; + out_file.write_all(segment)?; + + println!("- Saved {}.bin", name); + } + + if let Some(path) = decrypted_path { + if !app_ctx.has_option("vestel:keep_decrypted") { + fs::remove_file(&path)?; + } + } + + Ok(()) +} diff --git a/src/keys.rs b/src/keys.rs index c15433e..8ab716f 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -167,4 +167,9 @@ pub static EPK: &[(&str, &str)] = &[ ("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025)"), ("1FB2C3B789D5EA48ED16E79A0343986C691DACEC872BB07787D0F722AF5D1E2C", "lm21ann - MStar webOS 8 (2023)"), -]; \ No newline at end of file +]; + +//Vestel keys +pub static VESTEL: &[(&str, &str)] = &[ + ("VESTEL-MB180/MB130/MB181", "12345678123456781234567812345678"), +]; diff --git a/src/main.rs b/src/main.rs index b696f4f..d574f56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,51 @@ +mod error; mod formats; mod keys; mod utils; use clap::Parser; -use std::path::{PathBuf}; +use std::path::PathBuf; use std::io::{self, Seek, SeekFrom}; use std::fs::{self, File}; use crate::formats::{Format, get_registry}; +use crate::error::UnixtractError; #[derive(Parser, Debug)] +#[command( + name = "unixtract", + version, + about = "Firmware extractor for various file formats", + long_about = "unixtract analyzes and extracts firmware package formats commonly found \ + in TVs, Blu-Ray players, and AV devices. It supports 35+ different formats \ + with built-in decryption and decompression capabilities." +)] struct Args { - input_target: String, +/// The target file or directory to analyze/extract + #[arg(required_unless_present = "list-formats")] + input_target: Option, + + /// Folder to save extracted files to (default: _) output_directory: Option, - ///format specific options + /// Format-specific or global options (can be used multiple times) #[arg(short, long)] options: Vec, + + /// List supported formats and exit + #[arg(long)] + list_formats: bool, + + /// Only detect the format, do not extract + #[arg(long)] + dry_run: bool, + + /// Quiet mode — suppress non-error output + #[arg(short, long)] + quiet: bool, + + /// Verbose mode — show detailed progress information + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, } pub enum InputTarget { @@ -27,7 +57,10 @@ pub struct AppContext { pub input: InputTarget, pub output_dir: PathBuf, pub options: Vec, + pub dry_run: bool, + pub quiet: bool, } + impl AppContext { pub fn file(&self) -> Option<&File> { match &self.input { @@ -49,32 +82,63 @@ impl AppContext { } fn main() -> Result<(), Box> { - println!("unixtract Firmware extractor"); let args = Args::parse(); - let target_path_str = args.input_target; - println!("Input target: {}", target_path_str); - let target_path = PathBuf::from(&target_path_str); - - let output_path_str = if args.output_directory.is_some() { - args.output_directory.unwrap() + // Initialize logger based on verbosity level + let log_level = if args.quiet { + "error" } else { - format!("_{}", target_path.file_name().and_then(|s| s.to_str()).unwrap()) + match args.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + _ => "trace", + } }; - println!("Output directory: {}", output_path_str); + + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level)) + .format_timestamp(None) + .init(); + + // Handle --list-formats + if args.list_formats { + let formats = get_registry(); + println!("Supported formats ({} total):", formats.len()); + for (i, fmt) in formats.iter().enumerate() { + println!(" {:2}. {}", i + 1, fmt.name); + } + return Ok(()); + } + + log::info!("unixtract Firmware extractor v{}", env!("CARGO_PKG_VERSION")); + + let target_path_str = args.input_target.as_deref().unwrap_or(""); + log::info!("Input target: {}", target_path_str); + let target_path = PathBuf::from(target_path_str); + + let output_path_str = if let Some(ref out) = args.output_directory { + out.clone() + } else { + format!("_{}", target_path.file_name() + .and_then(|s| s.to_str()) + .ok_or_else(|| UnixtractError::Other("Invalid input file name".to_string()))?) + }; + log::info!("Output directory: {}", output_path_str); let output_directory_path = PathBuf::from(&output_path_str); if output_directory_path.exists() { if output_directory_path.is_dir() { let is_empty = fs::read_dir(&output_directory_path)?.next().is_none(); - if !is_empty { - println!("\nWarning: Output folder already exists and is NOT empty! Files may be overwritten!"); - println!("Press Enter if you want to continue..."); + if !is_empty && !args.quiet { + log::warn!("Output folder already exists and is NOT empty! Files may be overwritten."); + eprintln!("Warning: Output folder already exists and is NOT empty! Files may be overwritten!"); + eprintln!("Press Enter if you want to continue..."); io::stdin().read_line(&mut String::new())?; } } } + // === DEFAULT MODE (original behavior) === let app_ctx; if target_path.is_file() { @@ -83,37 +147,55 @@ fn main() -> Result<(), Box> { input: InputTarget::File(file), output_dir: output_directory_path, options: args.options, + dry_run: args.dry_run, + quiet: args.quiet, }; } else if target_path.is_dir() { app_ctx = AppContext { input: InputTarget::Directory(target_path), output_dir: output_directory_path, options: args.options, + dry_run: args.dry_run, + quiet: args.quiet, }; } else { return Err("Invalid input path!".into()); } let formats: Vec = get_registry(); - println!("Loaded {} formats!", formats.len()); + log::info!("Loaded {} formats", formats.len()); for format in formats { if let Some(ctx) = (format.detector_func)(&app_ctx)? { - println!("\n{} detected!", format.name); + log::info!("{} detected!", format.name); + if !app_ctx.quiet { + println!("\n{} detected!", format.name); + } - //reset seek of the file if present + if app_ctx.dry_run { + if !app_ctx.quiet { + println!("Dry run — skipping extraction."); + } + return Ok(()); + } + + // Reset seek of the file if present if let Some(mut file) = app_ctx.file() { file.seek(SeekFrom::Start(0))?; } (format.extractor_func)(&app_ctx, ctx)?; - //extractor returned with no error - println!("\nExtraction finished! Saved extracted files to {}", output_path_str); + // Extractor returned with no error + if !app_ctx.quiet { + println!("\nExtraction finished! Saved extracted files to {}", output_path_str); + } return Ok(()); } } - println!("\nInput format not recognized!"); - Ok(()) + if !app_ctx.quiet { + println!("\nInput format not recognized!"); + } + Err("Unrecognized input format".into()) } diff --git a/src/utils.rs b/src/utils.rs index b5ccb24..5a8d9ab 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,4 +3,5 @@ pub mod aes; pub mod lzop; pub mod sparse; pub mod compression; -pub mod global; \ No newline at end of file +pub mod global; +pub mod path_sanitize; diff --git a/src/utils/aes.rs b/src/utils/aes.rs index 44b822a..9047c09 100644 --- a/src/utils/aes.rs +++ b/src/utils/aes.rs @@ -1,31 +1,44 @@ -use aes::Aes128; +//! AES encryption/decryption utilities. +//! +//! Provides centralized implementations for all AES modes used across the +//! various firmware format extractors. Instead of duplicating AES logic in +//! each format module, all AES operations should go through this module. -use cbc::{Decryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}}; -type Aes128CbcDec = Decryptor; +use aes::{Aes128, Aes256}; +use cbc::{Decryptor as CbcDecryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}}; +use ecb::{Decryptor as EcbDecryptor, cipher::{KeyInit, generic_array::GenericArray}}; +type Aes128CbcDec = CbcDecryptor; +type Aes128EcbDec = EcbDecryptor; +type Aes256EcbDec = EcbDecryptor; + +// ── AES-128 CBC ───────────────────────────────────────────────────────────── + +/// Decrypt data using AES-128-CBC with PKCS7 padding. pub fn decrypt_aes128_cbc_pcks7(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result, Box> { let mut data = encrypted_data.to_vec(); let decryptor = Aes128CbcDec::new(key.into(), iv.into()); let decrypted = decryptor.decrypt_padded_mut::(&mut data) - .map_err(|e| format!("!!Decryption error!!: {:?}", e))?; - + .map_err(|e| format!("AES-128-CBC-PKCS7 decryption error: {:?}", e))?; + Ok(decrypted.to_vec()) } +/// Decrypt data using AES-128-CBC with no padding. pub fn decrypt_aes128_cbc_nopad(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result, Box> { let mut data = encrypted_data.to_vec(); let decryptor = Aes128CbcDec::new(key.into(), iv.into()); let decrypted = decryptor .decrypt_padded_mut::(&mut data) - .map_err(|e| format!("UnpadError: {:?}", e))?; + .map_err(|e| format!("AES-128-CBC-NoPad decryption error: {:?}", e))?; Ok(decrypted.to_vec()) } -use ecb::{Decryptor as EcbDecryptor, cipher::{KeyInit, generic_array::GenericArray}}; -type Aes128EcbDec = EcbDecryptor; +// ── AES-128 ECB ───────────────────────────────────────────────────────────── +/// Decrypt data using AES-128-ECB (raw block decryption, no padding). pub fn decrypt_aes128_ecb(key: &[u8; 16], ciphertext: &[u8]) -> Result, Box> { let mut buffer = ciphertext.to_vec(); @@ -36,4 +49,51 @@ pub fn decrypt_aes128_ecb(key: &[u8; 16], ciphertext: &[u8]) -> Result, } Ok(buffer) -} \ No newline at end of file +} + +// ── AES-256 ECB ───────────────────────────────────────────────────────────── + +/// Decrypt data using AES-256-ECB (raw block decryption, no padding). +pub fn decrypt_aes256_ecb(key: &[u8; 32], ciphertext: &[u8]) -> Result, Box> { + let mut buffer = ciphertext.to_vec(); + + let mut decryptor = Aes256EcbDec::new(key.into()); + for chunk in buffer.chunks_exact_mut(16) { + let block: &mut [u8; 16] = chunk.try_into()?; + decryptor.decrypt_block_mut(GenericArray::from_mut_slice(block)); + } + + Ok(buffer) +} + +// ── AES ECB auto-detect key size ──────────────────────────────────────────── + +/// Decrypt data using AES-ECB, automatically selecting AES-128 or AES-256 +/// based on the key length (16 bytes → AES-128, 32 bytes → AES-256). +/// +/// Returns an error if the key length is neither 16 nor 32 bytes. +pub fn decrypt_aes_ecb_auto(key: &[u8], ciphertext: &[u8]) -> Result, Box> { + let mut buffer = ciphertext.to_vec(); + + match key.len() { + 16 => { + let key_array: [u8; 16] = key.try_into()?; + let mut decryptor = Aes128EcbDec::new(&key_array.into()); + for chunk in buffer.chunks_exact_mut(16) { + let block: &mut [u8; 16] = chunk.try_into()?; + decryptor.decrypt_block_mut(GenericArray::from_mut_slice(block)); + } + } + 32 => { + let key_array: [u8; 32] = key.try_into()?; + let mut decryptor = Aes256EcbDec::new(&key_array.into()); + for chunk in buffer.chunks_exact_mut(16) { + let block: &mut [u8; 16] = chunk.try_into()?; + decryptor.decrypt_block_mut(GenericArray::from_mut_slice(block)); + } + } + _ => return Err(format!("Invalid AES key length: {} (expected 16 or 32)", key.len()).into()), + } + + Ok(buffer) +} diff --git a/src/utils/lzop.rs b/src/utils/lzop.rs index 991b529..f80d8f8 100644 --- a/src/utils/lzop.rs +++ b/src/utils/lzop.rs @@ -50,7 +50,6 @@ pub fn unlzop_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box segment_header.uncompressed_size { println!("{:?}", segment_header); } - //println!("{:?}", segment_header); if segment_header.uncompressed_size == 0 { break } diff --git a/src/utils/path_sanitize.rs b/src/utils/path_sanitize.rs new file mode 100644 index 0000000..e95a762 --- /dev/null +++ b/src/utils/path_sanitize.rs @@ -0,0 +1,77 @@ +//! Path sanitization utilities. +//! +//! Firmware images often embed file paths (e.g., partition names, file names) +//! that are used directly as output paths. A maliciously crafted firmware image +//! could contain paths like `../../etc/passwd` to write files outside the +//! intended output directory. This module provides sanitization to prevent +//! path traversal attacks. + +use std::path::{Component, Path, PathBuf}; +use crate::error::{UnixtractError, Result}; + +/// Sanitize a path extracted from a firmware image to prevent path traversal. +/// +/// This function: +/// 1. Removes any leading `/` (absolute path) +/// 2. Removes `..` components that would escape the base directory +/// 3. Removes empty components +/// 4. Collapses redundant `.` components +/// +/// Returns the sanitized relative path, or an error if the path would escape +/// the base directory after sanitization. +#[allow(dead_code)] +pub fn sanitize_path(path: &str) -> Result { + let raw = Path::new(path); + let mut sanitized = PathBuf::new(); + + for component in raw.components() { + match component { + Component::CurDir => { + // Skip "." components + } + Component::Normal(c) => { + sanitized.push(c); + } + Component::ParentDir => { + // Try to pop the last component, but if we can't, just skip it + // rather than allowing traversal + if !sanitized.pop() { + // Can't go up — skip the ".." silently + } + } + Component::RootDir | Component::Prefix(_) => { + // Strip absolute path prefixes + } + } + } + + // Final safety check: verify no ".." remains + if sanitized.components().any(|c| matches!(c, Component::ParentDir)) { + return Err(UnixtractError::UnsafePath { + path: PathBuf::from(path), + }); + } + + Ok(sanitized) +} + +/// Construct a safe output path by joining a base directory with a sanitized +/// filename or relative path extracted from firmware. +/// +/// Returns an error if the resulting path would be outside `base_dir`. +#[allow(dead_code)] +pub fn safe_output_path(base_dir: &Path, firmware_path: &str) -> Result { + let sanitized = sanitize_path(firmware_path)?; + let result = base_dir.join(&sanitized); + + // Verify the result doesn't escape the base directory + let result_str = result.to_string_lossy(); + let base_str = base_dir.to_string_lossy(); + if !result_str.starts_with(base_str.as_ref()) { + return Err(UnixtractError::UnsafePath { + path: PathBuf::from(firmware_path), + }); + } + + Ok(result) +} diff --git a/src/utils/sparse.rs b/src/utils/sparse.rs index 2e4d788..9a58613 100644 --- a/src/utils/sparse.rs +++ b/src/utils/sparse.rs @@ -44,7 +44,7 @@ pub fn unsparse_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box