mirror of
https://github.com/Ap0dexMe0/unixtract.git
synced 2026-07-15 21:00:03 +02:00
+ added vestel
This commit is contained in:
Generated
+184
-5
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
+8
-2
@@ -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"
|
||||
zstd = "0.13.3"
|
||||
thiserror = "2.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
@@ -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<T> = std::result::Result<T, UnixtractError>;
|
||||
|
||||
impl From<String> 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<binrw::Error> for UnixtractError {
|
||||
fn from(e: binrw::Error) -> Self {
|
||||
UnixtractError::InvalidFormat(format!("Binary parse error: {e}"))
|
||||
}
|
||||
}
|
||||
+30
-5
@@ -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<Option<Box<dyn Any>>, Box<dyn std::error::Error>>,
|
||||
@@ -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<Format> {
|
||||
return vec![
|
||||
Format {
|
||||
@@ -256,5 +277,9 @@ pub fn get_registry() -> Vec<Format> {
|
||||
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,
|
||||
}, ]
|
||||
}
|
||||
@@ -33,7 +33,6 @@ pub fn extract_bdl(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), Box<d
|
||||
|
||||
for _i in 0..header.pkg_count {
|
||||
let pkg_entry: PkgListEntry = file.read_le()?;
|
||||
//println!("Package {} - Offset: {}, Size: {}", i + 1, pkg_entry.offset, pkg_entry.size);
|
||||
pkgs.push(pkg_entry);
|
||||
}
|
||||
|
||||
|
||||
+12
-38
@@ -3,6 +3,7 @@ use std::io::Seek;
|
||||
use crate::AppContext;
|
||||
|
||||
use crate::utils::common;
|
||||
use crate::utils::aes::decrypt_aes_ecb_auto;
|
||||
use crate::formats;
|
||||
|
||||
pub struct EpkContext {
|
||||
@@ -29,13 +30,13 @@ fn check_epk_version(versions: &[u8]) -> Option<u8> {
|
||||
|
||||
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<dyn Any>) -> Result<(), Box<dy
|
||||
let platform_version = common::string_from_bytes(&versions[4..20]);
|
||||
let sdk_version = common::string_from_bytes(&versions[20..36]);
|
||||
println!("Platform version: {}\nSDK version: {}", platform_version, sdk_version);
|
||||
|
||||
|
||||
file.seek(std::io::SeekFrom::Start(0))?;
|
||||
|
||||
if ctx.epk_version == 2 {
|
||||
@@ -75,7 +76,11 @@ pub fn extract_epk(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), Box<dy
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//COMMON EPK FUNCTIONS
|
||||
// COMMON EPK FUNCTIONS
|
||||
|
||||
/// Try each key in `key_array` to find one that decrypts `data` with a header
|
||||
/// matching `expected_magic`. Returns the key name and the raw key bytes on
|
||||
/// success, or `None` if no key works.
|
||||
pub fn find_key<'a>(key_array: &'a [(&'a str, &'a str)], data: &[u8], expected_magic: &[u8]) -> Result<Option<(&'a str, Vec<u8>)>, Box<dyn std::error::Error>> {
|
||||
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<Aes128>;
|
||||
type Aes256EcbDec = Decryptor<Aes256>;
|
||||
|
||||
pub fn decrypt_aes_ecb_auto(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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)
|
||||
}
|
||||
@@ -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<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
||||
|
||||
@@ -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<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
||||
|
||||
@@ -67,10 +67,12 @@ pub fn extract_funai_mstar(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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
|
||||
|
||||
+37
-44
@@ -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<Aes128>;
|
||||
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<u8>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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::<Pkcs7>(&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<u8>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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::<Pkcs7>(&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<u8>, salt: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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::<Pkcs7>(&mut data)
|
||||
.map_err(|e| format!("Decryption error!!: {:?}", e))?;
|
||||
|
||||
Ok(decrypted.to_vec())
|
||||
}
|
||||
decrypt_aes128_cbc_pcks7(&encrypted_data, &key, &iv)
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ pub fn parse_ouith_blob(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>,
|
||||
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<MSDItem>,
|
||||
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);
|
||||
|
||||
@@ -146,7 +146,7 @@ pub fn parse_blob_1_8(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>, 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<MSDItem>, 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);
|
||||
|
||||
@@ -151,7 +151,7 @@ pub fn parse_blob_1_9(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>, 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<MSDItem>, 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);
|
||||
|
||||
@@ -35,7 +35,6 @@ pub fn extract_mstar(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> 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<dyn Any>) -> 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;
|
||||
|
||||
@@ -51,10 +51,12 @@ pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
+11
-13
@@ -213,12 +213,11 @@ fn unhuff(data: &[u8]) -> Vec<u8> {
|
||||
|
||||
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<u8> {
|
||||
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;
|
||||
|
||||
@@ -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<Option<(String, [u8; 32])>, Box<dyn std::error::Error>> {
|
||||
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<Option<(String, [u8
|
||||
let dec_int = rsa::hazmat::rsa_encrypt(&pubkey, &sig_int)?;
|
||||
let dec_sig = dec_int.to_bytes_le();
|
||||
|
||||
let aes_key = &dec_sig[20..52];
|
||||
let dec_ciphertext = decrypt_aes256_ecb(aes_key.try_into().unwrap(), &ciphertext)?;
|
||||
|
||||
//needs to start with null-termninated filename string
|
||||
let aes_key: [u8; 32] = dec_sig[20..52].try_into().unwrap();
|
||||
let dec_ciphertext = decrypt_aes256_ecb(&aes_key, &ciphertext)?;
|
||||
|
||||
// Needs to start with null-terminated filename string
|
||||
let end = match dec_ciphertext.iter().position(|&b| b == 0) {
|
||||
Some(pos) => 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<Option<(String, [u8
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
||||
type Aes256EcbDec = Decryptor<Aes256>;
|
||||
|
||||
pub fn decrypt_aes256_ecb(key: [u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
||||
@@ -58,7 +59,7 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> 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<dyn Any>) -> 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
|
||||
|
||||
@@ -74,8 +74,7 @@ pub fn extract_philips_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,18 +60,17 @@ pub fn extract_roku(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> 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);
|
||||
|
||||
@@ -100,10 +100,8 @@ pub fn extract_samsung_old(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> 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)?;
|
||||
|
||||
@@ -68,7 +68,6 @@ pub fn extract_sdboot(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> 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))?;
|
||||
|
||||
@@ -113,10 +113,12 @@ pub fn extract_sony_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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)? {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -57,7 +57,7 @@ pub fn extract_tsb_bin(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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}",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
||||
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<dyn Any>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut file = app_ctx.file().ok_or("Extractor expected file")?;
|
||||
let ctx = ctx.downcast::<VestelCtx>().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(())
|
||||
}
|
||||
+6
-1
@@ -167,4 +167,9 @@ pub static EPK: &[(&str, &str)] = &[
|
||||
("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025)"),
|
||||
("1FB2C3B789D5EA48ED16E79A0343986C691DACEC872BB07787D0F722AF5D1E2C", "lm21ann - MStar webOS 8 (2023)"),
|
||||
|
||||
];
|
||||
];
|
||||
|
||||
//Vestel keys
|
||||
pub static VESTEL: &[(&str, &str)] = &[
|
||||
("VESTEL-MB180/MB130/MB181", "12345678123456781234567812345678"),
|
||||
];
|
||||
|
||||
+104
-22
@@ -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<String>,
|
||||
|
||||
/// Folder to save extracted files to (default: _<INPUT_TARGET>)
|
||||
output_directory: Option<String>,
|
||||
|
||||
///format specific options
|
||||
/// Format-specific or global options (can be used multiple times)
|
||||
#[arg(short, long)]
|
||||
options: Vec<String>,
|
||||
|
||||
/// 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<String>,
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<Format> = 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())
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,4 +3,5 @@ pub mod aes;
|
||||
pub mod lzop;
|
||||
pub mod sparse;
|
||||
pub mod compression;
|
||||
pub mod global;
|
||||
pub mod global;
|
||||
pub mod path_sanitize;
|
||||
|
||||
+69
-9
@@ -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<Aes128>;
|
||||
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<Aes128>;
|
||||
type Aes128EcbDec = EcbDecryptor<Aes128>;
|
||||
type Aes256EcbDec = EcbDecryptor<Aes256>;
|
||||
|
||||
// ── 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<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let mut data = encrypted_data.to_vec();
|
||||
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
|
||||
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&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<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let mut data = encrypted_data.to_vec();
|
||||
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
|
||||
|
||||
let decrypted = decryptor
|
||||
.decrypt_padded_mut::<NoPadding>(&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<Aes128>;
|
||||
// ── 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<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let mut buffer = ciphertext.to_vec();
|
||||
|
||||
@@ -36,4 +49,51 @@ pub fn decrypt_aes128_ecb(key: &[u8; 16], ciphertext: &[u8]) -> Result<Vec<u8>,
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ pub fn unlzop_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box<dyn std
|
||||
if segment_header.compressed_size > segment_header.uncompressed_size {
|
||||
println!("{:?}", segment_header);
|
||||
}
|
||||
//println!("{:?}", segment_header);
|
||||
if segment_header.uncompressed_size == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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)
|
||||
}
|
||||
+1
-1
@@ -44,7 +44,7 @@ pub fn unsparse_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box<dyn s
|
||||
|
||||
} else if chunk_header.chunk_type == 0xCAC2 { // "fill" type chunk (fill size with a value)
|
||||
if chunk_data.len() != 4 {
|
||||
return Err("Inavlid lenght of FILL chunk!".into());
|
||||
return Err("Invalid length of FILL chunk!".into());
|
||||
}
|
||||
let fill_size = (chunk_header.chunk_size * file_header.block_size) / 4;
|
||||
let fill_data = chunk_data.repeat(fill_size as usize);
|
||||
|
||||
Reference in New Issue
Block a user