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",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.20"
|
version = "0.6.20"
|
||||||
@@ -26,7 +35,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
|
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"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-query",
|
||||||
"anstyle-wincon",
|
"anstyle-wincon",
|
||||||
"colorchoice",
|
"colorchoice",
|
||||||
@@ -36,9 +60,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.11"
|
version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-parse"
|
name = "anstyle-parse"
|
||||||
@@ -49,6 +73,15 @@ dependencies = [
|
|||||||
"utf8parse",
|
"utf8parse",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-query"
|
name = "anstyle-query"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -221,7 +254,7 @@ version = "4.5.47"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
|
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream 0.6.20",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
"strsim",
|
"strsim",
|
||||||
@@ -346,6 +379,29 @@ version = "1.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
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]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -454,6 +510,30 @@ dependencies = [
|
|||||||
"either",
|
"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]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
version = "0.1.34"
|
version = "0.1.34"
|
||||||
@@ -528,6 +608,12 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lz4"
|
name = "lz4"
|
||||||
version = "1.28.1"
|
version = "1.28.1"
|
||||||
@@ -563,6 +649,12 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minilzo-rs"
|
name = "minilzo-rs"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -676,6 +768,21 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
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]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -779,6 +886,35 @@ dependencies = [
|
|||||||
"bitflags",
|
"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]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
@@ -812,6 +948,26 @@ dependencies = [
|
|||||||
"windows-sys",
|
"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]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -901,6 +1057,26 @@ dependencies = [
|
|||||||
"xattr",
|
"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]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.0"
|
version = "1.20.0"
|
||||||
@@ -915,7 +1091,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unixtract"
|
name = "unixtract"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"binrw",
|
"binrw",
|
||||||
@@ -924,9 +1100,11 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"des",
|
"des",
|
||||||
"ecb",
|
"ecb",
|
||||||
|
"env_logger",
|
||||||
"flate2",
|
"flate2",
|
||||||
"hex",
|
"hex",
|
||||||
"liblzma",
|
"liblzma",
|
||||||
|
"log",
|
||||||
"lz4",
|
"lz4",
|
||||||
"lzma-rs",
|
"lzma-rs",
|
||||||
"md5",
|
"md5",
|
||||||
@@ -937,6 +1115,7 @@ dependencies = [
|
|||||||
"sha1",
|
"sha1",
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
"tar",
|
"tar",
|
||||||
|
"thiserror",
|
||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -1,7 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "unixtract"
|
name = "unixtract"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
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]
|
[dependencies]
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
@@ -24,4 +27,7 @@ prost-types = "0.14.1"
|
|||||||
bzip2 = "0.6.1"
|
bzip2 = "0.6.1"
|
||||||
liblzma = "0.4.5"
|
liblzma = "0.4.5"
|
||||||
des = "0.8.1"
|
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 std::any::Any;
|
||||||
use crate::AppContext;
|
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 struct Format {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub detector_func: fn(&AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>>,
|
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 onkyo;
|
||||||
pub mod philips_bdp;
|
pub mod philips_bdp;
|
||||||
pub mod tsb_bin;
|
pub mod tsb_bin;
|
||||||
|
pub mod pup;
|
||||||
|
pub mod vestel;
|
||||||
|
|
||||||
pub mod pup;
|
pub mod msd;
|
||||||
|
|
||||||
pub mod msd;
|
|
||||||
pub mod msd10;
|
pub mod msd10;
|
||||||
pub mod msd11;
|
pub mod msd11;
|
||||||
|
|
||||||
@@ -53,7 +70,11 @@ pub mod mtk_pkg_old;
|
|||||||
pub mod mtk_pkg_new;
|
pub mod mtk_pkg_new;
|
||||||
pub mod mtk_bdp;
|
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> {
|
pub fn get_registry() -> Vec<Format> {
|
||||||
return vec![
|
return vec![
|
||||||
Format {
|
Format {
|
||||||
@@ -256,5 +277,9 @@ pub fn get_registry() -> Vec<Format> {
|
|||||||
detector_func: crate::formats::tsb_bin::is_tsb_bin_file,
|
detector_func: crate::formats::tsb_bin::is_tsb_bin_file,
|
||||||
extractor_func: crate::formats::tsb_bin::extract_tsb_bin,
|
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 {
|
for _i in 0..header.pkg_count {
|
||||||
let pkg_entry: PkgListEntry = file.read_le()?;
|
let pkg_entry: PkgListEntry = file.read_le()?;
|
||||||
//println!("Package {} - Offset: {}, Size: {}", i + 1, pkg_entry.offset, pkg_entry.size);
|
|
||||||
pkgs.push(pkg_entry);
|
pkgs.push(pkg_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-38
@@ -3,6 +3,7 @@ use std::io::Seek;
|
|||||||
use crate::AppContext;
|
use crate::AppContext;
|
||||||
|
|
||||||
use crate::utils::common;
|
use crate::utils::common;
|
||||||
|
use crate::utils::aes::decrypt_aes_ecb_auto;
|
||||||
use crate::formats;
|
use crate::formats;
|
||||||
|
|
||||||
pub struct EpkContext {
|
pub struct EpkContext {
|
||||||
@@ -29,13 +30,13 @@ fn check_epk_version(versions: &[u8]) -> Option<u8> {
|
|||||||
|
|
||||||
if match_with_pattern(&versions, epk2_pattern) {
|
if match_with_pattern(&versions, epk2_pattern) {
|
||||||
Some(2)
|
Some(2)
|
||||||
} else if match_with_pattern(&versions, epk3_pattern) {
|
} else if match_with_pattern(&versions, epk3_pattern) {
|
||||||
Some(3)
|
Some(3)
|
||||||
} else if match_with_pattern(&versions, epk3_another_pattern) {
|
} else if match_with_pattern(&versions, epk3_another_pattern) {
|
||||||
Some(3)
|
Some(3)
|
||||||
} else if match_with_pattern(&versions, epk3_new_pattern) {
|
} else if match_with_pattern(&versions, epk3_new_pattern) {
|
||||||
Some(3)
|
Some(3)
|
||||||
}else {
|
} else {
|
||||||
None
|
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 platform_version = common::string_from_bytes(&versions[4..20]);
|
||||||
let sdk_version = common::string_from_bytes(&versions[20..36]);
|
let sdk_version = common::string_from_bytes(&versions[20..36]);
|
||||||
println!("Platform version: {}\nSDK version: {}", platform_version, sdk_version);
|
println!("Platform version: {}\nSDK version: {}", platform_version, sdk_version);
|
||||||
|
|
||||||
file.seek(std::io::SeekFrom::Start(0))?;
|
file.seek(std::io::SeekFrom::Start(0))?;
|
||||||
|
|
||||||
if ctx.epk_version == 2 {
|
if ctx.epk_version == 2 {
|
||||||
@@ -75,7 +76,11 @@ pub fn extract_epk(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), Box<dy
|
|||||||
Ok(())
|
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>> {
|
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 {
|
for (key_hex, name) in key_array {
|
||||||
let key_bytes = hex::decode(key_hex)?;
|
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,
|
Ok(d) => d,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if decrypted.starts_with(expected_magic) {
|
if decrypted.starts_with(expected_magic) {
|
||||||
return Ok(Some((name, key_bytes)));
|
return Ok(Some((name, key_bytes)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
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::common;
|
||||||
use crate::utils::global::opt_dump_dec_hdr;
|
use crate::utils::global::opt_dump_dec_hdr;
|
||||||
use crate::keys;
|
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::*;
|
use include::*;
|
||||||
|
|
||||||
pub fn is_epk2_file(app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
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::common;
|
||||||
use crate::utils::global::opt_dump_dec_hdr;
|
use crate::utils::global::opt_dump_dec_hdr;
|
||||||
use crate::keys;
|
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::*;
|
use include::*;
|
||||||
|
|
||||||
pub fn is_epk3_file(_app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
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?) )
|
//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" {
|
if name == "SoC" {
|
||||||
let r_out_file = File::open(&output_path)?;
|
let r_out_file = File::open(&output_path)?;
|
||||||
let in_ctx: AppContext = AppContext {
|
let in_ctx: AppContext = AppContext {
|
||||||
input: InputTarget::File(r_out_file),
|
input: InputTarget::File(r_out_file),
|
||||||
output_dir: app_ctx.output_dir.join("SoC"),
|
output_dir: app_ctx.output_dir.join("SoC"),
|
||||||
options: app_ctx.options.clone()
|
options: app_ctx.options.clone(),
|
||||||
|
dry_run: app_ctx.dry_run,
|
||||||
|
quiet: app_ctx.quiet,
|
||||||
};
|
};
|
||||||
|
|
||||||
//do check and extarct
|
//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_old;
|
||||||
pub mod msd_ouith_parser_tizen_1_8;
|
pub mod msd_ouith_parser_tizen_1_8;
|
||||||
pub mod msd_ouith_parser_tizen_1_9;
|
pub mod msd_ouith_parser_tizen_1_9;
|
||||||
|
|
||||||
// COMMON MSD FUNCTIONS
|
// COMMON MSD FUNCTIONS
|
||||||
use aes::Aes128;
|
use crate::utils::aes::decrypt_aes128_cbc_pcks7;
|
||||||
use cbc::{Decryptor, cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}};
|
|
||||||
|
|
||||||
type Aes128CbcDec = Decryptor<Aes128>;
|
|
||||||
|
|
||||||
|
/// 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>> {
|
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 encrypted_data.len() < 16 || encrypted_data[0..8].to_vec() != b"Salted__" {
|
||||||
|
|
||||||
if data[0..8].to_vec() != b"Salted__" {
|
|
||||||
return Err("Invalid encrypted data!".into());
|
return Err("Invalid encrypted data!".into());
|
||||||
}
|
}
|
||||||
let salt = &data[8..16];
|
let salt = &encrypted_data[8..16];
|
||||||
|
|
||||||
//key = md5 of (passphrase + salt)
|
// key = MD5(passphrase + salt)
|
||||||
let mut key = Vec::new();
|
let mut key_input = Vec::new();
|
||||||
key.extend_from_slice(&passphrase_bytes);
|
key_input.extend_from_slice(&passphrase_bytes);
|
||||||
key.extend_from_slice(&salt);
|
key_input.extend_from_slice(&salt);
|
||||||
let key_md5 = md5::compute(&key);
|
let key_md5 = md5::compute(&key_input);
|
||||||
|
|
||||||
//iv = md5 of (md5 of key + passphrase + salt)
|
// iv = MD5(key + passphrase + salt)
|
||||||
let mut iv = Vec::new();
|
let mut iv_input = Vec::new();
|
||||||
iv.extend_from_slice(&key_md5.0);
|
iv_input.extend_from_slice(&key_md5.0);
|
||||||
iv.extend_from_slice(&passphrase_bytes);
|
iv_input.extend_from_slice(&passphrase_bytes);
|
||||||
iv.extend_from_slice(&salt);
|
iv_input.extend_from_slice(&salt);
|
||||||
let iv_md5 = md5::compute(&iv);
|
let iv_md5 = md5::compute(&iv_input);
|
||||||
|
|
||||||
let decryptor = Aes128CbcDec::new((&key_md5.0).into(), (&iv_md5.0).into());
|
let key: [u8; 16] = key_md5.0;
|
||||||
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data[16..])
|
let iv: [u8; 16] = iv_md5.0;
|
||||||
.map_err(|e| format!("Decryption error!!: {:?}", e))?;
|
decrypt_aes128_cbc_pcks7(&encrypted_data[16..], &key, &iv)
|
||||||
|
|
||||||
Ok(decrypted.to_vec())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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>> {
|
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 encrypted_data.len() < 16 || encrypted_data[0..8].to_vec() != b"Salted__" {
|
||||||
|
|
||||||
if data[0..8].to_vec() != b"Salted__" {
|
|
||||||
return Err("Invalid encrypted data!".into());
|
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 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());
|
decrypt_aes128_cbc_pcks7(&encrypted_data[16..], &key, &iv)
|
||||||
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data[16..])
|
|
||||||
.map_err(|e| format!("Decryption error!!: {:?}", e))?;
|
|
||||||
|
|
||||||
Ok(decrypted.to_vec())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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>> {
|
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(salt)
|
||||||
|
|
||||||
//iv = md5 of salt
|
|
||||||
let iv = md5::compute(&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());
|
decrypt_aes128_cbc_pcks7(&encrypted_data, &key, &iv)
|
||||||
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data)
|
}
|
||||||
.map_err(|e| format!("Decryption error!!: {:?}", e))?;
|
|
||||||
|
|
||||||
Ok(decrypted.to_vec())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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); };
|
if print_tree { println!(" Type descriptor(0x{:02x}) - Size: {}", type_descriptor.tag, type_descriptor.size); };
|
||||||
let destination_info: CommonDestinationInfo = reader.read_be()?;
|
let destination_info: CommonDestinationInfo = reader.read_be()?;
|
||||||
if print_tree {
|
if print_tree {
|
||||||
println!(" Name lenght: {}", destination_info.name_len);
|
println!(" Name length: {}", destination_info.name_len);
|
||||||
println!(" Name: {}", destination_info.name());
|
println!(" Name: {}", destination_info.name());
|
||||||
println!(" Version: {}", destination_info.version);
|
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); };
|
if print_tree { println!(" OUSWImageVersionExDesc(0x12) - Size: {}", version_descriptor.size); };
|
||||||
let sw_image_version_ex_desc: OUSWImageVersionExDesc = reader.read_be()?;
|
let sw_image_version_ex_desc: OUSWImageVersionExDesc = reader.read_be()?;
|
||||||
if print_tree {
|
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!(" Name: {}", sw_image_version_ex_desc.name());
|
||||||
println!(" Major version: {}", sw_image_version_ex_desc.major_ver);
|
println!(" Major version: {}", sw_image_version_ex_desc.major_ver);
|
||||||
println!(" Minor version: {}", sw_image_version_ex_desc.minor_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); };
|
if print_tree { println!(" OUPartitionVersionDesc(0x0A) - Size: {}", sub_descriptor.size); };
|
||||||
let partition_version_desc: OUPartitionVersionDesc = reader.read_be()?;
|
let partition_version_desc: OUPartitionVersionDesc = reader.read_be()?;
|
||||||
if print_tree {
|
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!(" Name: {}", partition_version_desc.name());
|
||||||
println!(" Version: {}", partition_version_desc.version);
|
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); };
|
if print_tree { println!(" OUSWImageVersionDesc(0x19) - Size: {}", sub_descriptor.size); };
|
||||||
let sw_image_version_desc: OUSWImageVersionDesc = reader.read_be()?;
|
let sw_image_version_desc: OUSWImageVersionDesc = reader.read_be()?;
|
||||||
if print_tree {
|
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!(" Name: {}", sw_image_version_desc.name());
|
||||||
println!(" Major ver: {}", sw_image_version_desc.major_ver);
|
println!(" Major ver: {}", sw_image_version_desc.major_ver);
|
||||||
println!(" Minor ver: {}", sw_image_version_desc.minor_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); };
|
if print_tree { println!(" OUPartitionVersionDesc(0x0A) - Size: {}", sub_descriptor.size); };
|
||||||
let partition_version_desc: OUPartitionVersionDesc = reader.read_le()?;
|
let partition_version_desc: OUPartitionVersionDesc = reader.read_le()?;
|
||||||
if print_tree {
|
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!(" 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); };
|
if print_tree { println!(" OUSWImageVersionDesc(0x19) - Size: {}", sub_descriptor.size); };
|
||||||
let sw_image_version_desc: OUSWImageVersionDesc = reader.read_le()?;
|
let sw_image_version_desc: OUSWImageVersionDesc = reader.read_le()?;
|
||||||
if print_tree {
|
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!(" Name: {}", sw_image_version_desc.name());
|
||||||
println!(" Major ver: {}", sw_image_version_desc.major_ver);
|
println!(" Major ver: {}", sw_image_version_desc.major_ver);
|
||||||
println!(" Minor ver: {}", sw_image_version_desc.minor_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);
|
let mut script_string = String::from_utf8_lossy(&script);
|
||||||
//println!("{}", script_string);
|
|
||||||
if script_string == "" {
|
if script_string == "" {
|
||||||
//try for hisense
|
//try for hisense
|
||||||
println!("Failed to get script at 0x0, trying 0x1000...");
|
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);
|
println!("- Decompressing lz4, expected size: {}", lz4_expect_size);
|
||||||
out_data = decompress_lz4(&data, lz4_expect_size.try_into().unwrap())?;
|
out_data = decompress_lz4(&data, lz4_expect_size.try_into().unwrap())?;
|
||||||
} else if compression == CompressionType::Lzo {
|
} else if compression == CompressionType::Lzo {
|
||||||
println!("- Decompessing LZO..");
|
println!("- Decompressing LZO..");
|
||||||
unlzop_to_file(&data, output_path)?;
|
unlzop_to_file(&data, output_path)?;
|
||||||
println!("-- Saved file!");
|
println!("-- Saved file!");
|
||||||
i += 1;
|
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
|
//run standard mstar ext into same directory
|
||||||
let r_out_file = File::open(&output_path)?;
|
let r_out_file = File::open(&output_path)?;
|
||||||
let in_ctx: AppContext = AppContext {
|
let in_ctx: AppContext = AppContext {
|
||||||
input: InputTarget::File(r_out_file),
|
input: InputTarget::File(r_out_file),
|
||||||
output_dir: app_ctx.output_dir.clone(),
|
output_dir: app_ctx.output_dir.clone(),
|
||||||
options: app_ctx.options.clone()
|
options: app_ctx.options.clone(),
|
||||||
|
dry_run: app_ctx.dry_run,
|
||||||
|
quiet: app_ctx.quiet,
|
||||||
};
|
};
|
||||||
|
|
||||||
//do check just in case and extract
|
//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)]
|
#[derive(BinRead)]
|
||||||
pub struct PITITPITEntry {
|
pub struct PITITPITEntry {
|
||||||
pub nand_size: u32,
|
pub nand_size: u32,
|
||||||
pub pit_offset: u32,
|
pub pit_offset: u32,
|
||||||
pub pit_size: u32,
|
pub pit_size: u32,
|
||||||
_table_id: u32,
|
_table_id: u32,
|
||||||
@@ -18,7 +18,7 @@ pub struct PITITPITEntry {
|
|||||||
|
|
||||||
#[derive(BinRead)]
|
#[derive(BinRead)]
|
||||||
pub struct PITITBITEntry {
|
pub struct PITITBITEntry {
|
||||||
pub bit_offset: u32,
|
pub bit_offset: u32,
|
||||||
pub bit_size: u32,
|
pub bit_size: u32,
|
||||||
_private_data_1: u32,
|
_private_data_1: u32,
|
||||||
_private_data_2: u32,
|
_private_data_2: u32,
|
||||||
@@ -29,7 +29,7 @@ pub struct PITHeader {
|
|||||||
pub pit_magic: [u8; 8],
|
pub pit_magic: [u8; 8],
|
||||||
_version: u32,
|
_version: u32,
|
||||||
pub first_entry_offset: u32, //"header len"
|
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"
|
pub entry_count: u32, //"item num"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-13
@@ -213,12 +213,11 @@ fn unhuff(data: &[u8]) -> Vec<u8> {
|
|||||||
|
|
||||||
let found_j: i32;
|
let found_j: i32;
|
||||||
loop {
|
loop {
|
||||||
if !get_byte(&mut ctx, &mut in_cur) {
|
if !get_byte(&mut ctx, &mut in_cur) {
|
||||||
//println!("retA");
|
// flush
|
||||||
//flush
|
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
|
||||||
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
|
return out;
|
||||||
return out;
|
}
|
||||||
}
|
|
||||||
if ctx.len < 2 { continue; }
|
if ctx.len < 2 { continue; }
|
||||||
let keyp = (((ctx.len & 0x7) as usize) << 6) | ((ctx.code & 0x3F) as usize);
|
let keyp = (((ctx.len & 0x7) as usize) << 6) | ((ctx.code & 0x3F) as usize);
|
||||||
let mut jdx = lookup_charpos[keyp];
|
let mut jdx = lookup_charpos[keyp];
|
||||||
@@ -235,16 +234,15 @@ fn unhuff(data: &[u8]) -> Vec<u8> {
|
|||||||
found_j = jdx as i32;
|
found_j = jdx as i32;
|
||||||
let b = ((jdx as i32) >> 1) as u8;
|
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; }
|
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;
|
ctx.code = 0;
|
||||||
for _ in 0..7 { if !get_byte(&mut ctx, &mut in_cur) {
|
for _ in 0..7 { if !get_byte(&mut ctx, &mut in_cur) {
|
||||||
//println!("retB");
|
// flush
|
||||||
//flush
|
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
|
||||||
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
|
return out;
|
||||||
return out;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let combined = (ctx.code | ((found_j as u32) << 7)) as u32;
|
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; }
|
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;
|
ctx.code = 0; ctx.len = 0;
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
use crate::utils::common;
|
use crate::utils::common;
|
||||||
|
use crate::utils::aes::decrypt_aes256_ecb;
|
||||||
use binrw::BinRead;
|
use binrw::BinRead;
|
||||||
use aes::Aes256;
|
|
||||||
use ecb::{Decryptor, cipher::{BlockDecryptMut, KeyInit, generic_array::GenericArray}};
|
|
||||||
use crate::keys;
|
use crate::keys;
|
||||||
use rsa::{RsaPublicKey, BigUint};
|
use rsa::{RsaPublicKey, BigUint};
|
||||||
|
|
||||||
pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result<Option<(String, [u8; 32])>, Box<dyn std::error::Error>> {
|
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;
|
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 n = BigUint::from_bytes_be(&hex::decode(n_hex)?);
|
||||||
let e = BigUint::from_bytes_be(&hex::decode("010001")?);
|
let e = BigUint::from_bytes_be(&hex::decode("010001")?);
|
||||||
let pubkey = RsaPublicKey::new(n, e)?;
|
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_int = rsa::hazmat::rsa_encrypt(&pubkey, &sig_int)?;
|
||||||
let dec_sig = dec_int.to_bytes_le();
|
let dec_sig = dec_int.to_bytes_le();
|
||||||
|
|
||||||
let aes_key = &dec_sig[20..52];
|
let aes_key: [u8; 32] = dec_sig[20..52].try_into().unwrap();
|
||||||
let dec_ciphertext = decrypt_aes256_ecb(aes_key.try_into().unwrap(), &ciphertext)?;
|
let dec_ciphertext = decrypt_aes256_ecb(&aes_key, &ciphertext)?;
|
||||||
|
|
||||||
//needs to start with null-termninated filename string
|
// Needs to start with null-terminated filename string
|
||||||
let end = match dec_ciphertext.iter().position(|&b| b == 0) {
|
let end = match dec_ciphertext.iter().position(|&b| b == 0) {
|
||||||
Some(pos) => pos,
|
Some(pos) => pos,
|
||||||
None => continue, //there is no 0, continue
|
None => continue, // There is no 0, continue
|
||||||
};
|
};
|
||||||
let fname = &dec_ciphertext[..end];
|
let fname = &dec_ciphertext[..end];
|
||||||
if fname.len() > 1 && fname.is_ascii() { //is ascii filename
|
if fname.len() > 1 && fname.is_ascii() { // Is ASCII filename
|
||||||
result = Some((name.to_string(), aes_key.try_into().unwrap()));
|
result = Some((name.to_string(), aes_key));
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,31 +34,16 @@ pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result<Option<(String, [u8
|
|||||||
Ok(result)
|
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)]
|
#[derive(BinRead)]
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
_magic_bytes: [u8; 8],
|
_magic_bytes: [u8; 8],
|
||||||
pub header_size: u32, //data start
|
pub header_size: u32, //data start
|
||||||
pub data_size: u32,
|
pub data_size: u32,
|
||||||
_crc32: u32,
|
_crc32: u32,
|
||||||
pub mask: u32,
|
pub mask: u32,
|
||||||
_data_size_decompressed: u32,
|
_data_size_decompressed: u32,
|
||||||
_padding2: u32,
|
_padding2: u32,
|
||||||
description_bytes: [u8; 512],
|
description_bytes: [u8; 512],
|
||||||
}
|
}
|
||||||
impl Header {
|
impl Header {
|
||||||
pub fn description(&self) -> String {
|
pub fn description(&self) -> String {
|
||||||
@@ -74,8 +58,8 @@ impl Header {
|
|||||||
pub struct FileHeader {
|
pub struct FileHeader {
|
||||||
file_name_bytes: [u8; 60],
|
file_name_bytes: [u8; 60],
|
||||||
pub real_size: u32,
|
pub real_size: u32,
|
||||||
pub stored_size: u32,
|
pub stored_size: u32,
|
||||||
pub header_size: u32,
|
pub header_size: u32,
|
||||||
pub attributes: [u8; 4],
|
pub attributes: [u8; 4],
|
||||||
}
|
}
|
||||||
impl FileHeader {
|
impl FileHeader {
|
||||||
@@ -91,4 +75,4 @@ impl FileHeader {
|
|||||||
pub fn is_package(&self) -> bool {
|
pub fn is_package(&self) -> bool {
|
||||||
(self.attributes[3] & (1 << 2)) != 0
|
(self.attributes[3] & (1 << 2)) != 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::fs::{self, File, OpenOptions};
|
|||||||
use binrw::BinReaderExt;
|
use binrw::BinReaderExt;
|
||||||
|
|
||||||
use crate::utils::common;
|
use crate::utils::common;
|
||||||
|
use crate::utils::aes::decrypt_aes256_ecb;
|
||||||
use include::*;
|
use include::*;
|
||||||
|
|
||||||
pub fn is_pfl_upg_file(app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
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)?;
|
let encrypted_data = common::read_exact(&mut file, (header.data_size as usize + 0xf) & !0xf)?;
|
||||||
|
|
||||||
println!("Decrypting data...");
|
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
|
data.truncate(header.data_size as usize); //discard padding
|
||||||
|
|
||||||
} else {
|
} 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
|
//REOPEN temp file and make ctx
|
||||||
let r_temp_file = File::open(&temp_path)?;
|
let r_temp_file = File::open(&temp_path)?;
|
||||||
let in_ctx: AppContext = AppContext {
|
let in_ctx: AppContext = AppContext {
|
||||||
input: InputTarget::File(r_temp_file),
|
input: InputTarget::File(r_temp_file),
|
||||||
output_dir: output_path,
|
output_dir: output_path,
|
||||||
options: app_ctx.options.clone()
|
options: app_ctx.options.clone(),
|
||||||
|
dry_run: app_ctx.dry_run,
|
||||||
|
quiet: app_ctx.quiet,
|
||||||
};
|
};
|
||||||
|
|
||||||
//do check just in case and extract
|
//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...");
|
println!("Checking if it's also MTK BDP...");
|
||||||
|
|
||||||
let new_file = File::open(&output_path)?;
|
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(), dry_run: app_ctx.dry_run, quiet: app_ctx.quiet };
|
||||||
let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone() };
|
|
||||||
|
|
||||||
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? {
|
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? {
|
||||||
println!("- MTK BDP file detected!\n");
|
println!("- MTK BDP file detected!\n");
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ pub struct AImageHeader {
|
|||||||
_release_id: u32, // toplevel build / release version
|
_release_id: u32, // toplevel build / release version
|
||||||
_platform_id: u32, // target platform information
|
_platform_id: u32, // target platform information
|
||||||
pub image_type: u32, // image type
|
pub image_type: u32, // image type
|
||||||
pub lenght: u32, // length of entire image (header + data + trailing padding)
|
pub length: 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)
|
_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
|
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_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
|
_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 {
|
while image_reader.stream_position()? < size as u64 {
|
||||||
let image: AImageHeader = image_reader.read_le()?;
|
let image: AImageHeader = image_reader.read_le()?;
|
||||||
println!(" #{} - Type: {}(0x{:x}), Lenght: {}, encmode: {}",
|
println!(" #{} - Type: {}(0x{:x}), Length: {}, encmode: {}",
|
||||||
i, image.image_type_str(), image.image_type, image.lenght, image.encmode_str());
|
i, image.image_type_str(), image.image_type, image.length, image.encmode_str());
|
||||||
//println!("{:?}", image);
|
|
||||||
|
let data =
|
||||||
let data =
|
|
||||||
if image.data_start_offset == 0 { // "0 if header is part of data"
|
if image.data_start_offset == 0 { // "0 if header is part of data"
|
||||||
image_reader.seek(SeekFrom::Current(-256))?; //rewind header
|
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 {
|
} else {
|
||||||
let _skip_data = common::read_exact(&mut image_reader, image.data_start_offset as usize - 256)?;
|
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);
|
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_md5.0);
|
||||||
iv.extend_from_slice(&key);
|
iv.extend_from_slice(&key);
|
||||||
iv_md5 = md5::compute(&iv);
|
iv_md5 = md5::compute(&iv);
|
||||||
}
|
}
|
||||||
|
|
||||||
//println!("Key: {:02x?}", key_md5);
|
|
||||||
//println!("IV: {:02x?}", iv_md5);
|
|
||||||
let end = file_size - 260;
|
let end = file_size - 260;
|
||||||
println!("- Decrypting file...");
|
println!("- Decrypting file...");
|
||||||
let decrypted_data = decrypt_aes128_cbc_pcks7(&data[16..end.try_into().unwrap()], &key_md5, &iv_md5)?;
|
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 entry_header: SdbootEntryHeader = entry_header_reader.read_be()?;
|
||||||
|
|
||||||
let offset = file.stream_position()?;
|
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_list.push( FileEntry { name: entry_header.name(), size: entry_header.file_size(), offset });
|
||||||
file.seek(SeekFrom::Current(entry_header.file_size() as i64))?;
|
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 last_file = File::open(last_file_path.unwrap())?;
|
||||||
let mtk_extraction_path = app_ctx.output_dir.join(format!("{}", i));
|
let mtk_extraction_path = app_ctx.output_dir.join(format!("{}", i));
|
||||||
|
|
||||||
let ctx: AppContext = AppContext {
|
let ctx: AppContext = AppContext {
|
||||||
input: InputTarget::File(last_file),
|
input: InputTarget::File(last_file),
|
||||||
output_dir: mtk_extraction_path,
|
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)? {
|
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&ctx)? {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use binrw::BinRead;
|
|||||||
pub struct Header {
|
pub struct Header {
|
||||||
_signature: [u8; 128],
|
_signature: [u8; 128],
|
||||||
_control_id: u32, //must be zero
|
_control_id: u32, //must be zero
|
||||||
pub lenght: u32, //file size
|
pub length: u32, //file size
|
||||||
pub entry_count: u32,
|
pub entry_count: u32,
|
||||||
_pad1: [u8; 4],
|
_pad1: [u8; 4],
|
||||||
build_no_bytes: [u8; 16],
|
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()?;
|
let hdr: Header = hdr_rdr.read_be()?;
|
||||||
|
|
||||||
println!("File info -\nSize: {}\nEntry count: {}\nBuild no.: {}\nEntry address: 0x{:02x}"
|
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() {
|
for (i, entry) in hdr.entries.iter().enumerate() {
|
||||||
println!("\n({}/{}) - {}, Size: {}, Offset: {}, Load address: 0x{:02x}",
|
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)"),
|
("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025)"),
|
||||||
("1FB2C3B789D5EA48ED16E79A0343986C691DACEC872BB07787D0F722AF5D1E2C", "lm21ann - MStar webOS 8 (2023)"),
|
("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 formats;
|
||||||
mod keys;
|
mod keys;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::path::{PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::io::{self, Seek, SeekFrom};
|
use std::io::{self, Seek, SeekFrom};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use crate::formats::{Format, get_registry};
|
use crate::formats::{Format, get_registry};
|
||||||
|
use crate::error::UnixtractError;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[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 {
|
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>,
|
output_directory: Option<String>,
|
||||||
|
|
||||||
///format specific options
|
/// Format-specific or global options (can be used multiple times)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
options: Vec<String>,
|
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 {
|
pub enum InputTarget {
|
||||||
@@ -27,7 +57,10 @@ pub struct AppContext {
|
|||||||
pub input: InputTarget,
|
pub input: InputTarget,
|
||||||
pub output_dir: PathBuf,
|
pub output_dir: PathBuf,
|
||||||
pub options: Vec<String>,
|
pub options: Vec<String>,
|
||||||
|
pub dry_run: bool,
|
||||||
|
pub quiet: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppContext {
|
impl AppContext {
|
||||||
pub fn file(&self) -> Option<&File> {
|
pub fn file(&self) -> Option<&File> {
|
||||||
match &self.input {
|
match &self.input {
|
||||||
@@ -49,32 +82,63 @@ impl AppContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("unixtract Firmware extractor");
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let target_path_str = args.input_target;
|
// Initialize logger based on verbosity level
|
||||||
println!("Input target: {}", target_path_str);
|
let log_level = if args.quiet {
|
||||||
let target_path = PathBuf::from(&target_path_str);
|
"error"
|
||||||
|
|
||||||
let output_path_str = if args.output_directory.is_some() {
|
|
||||||
args.output_directory.unwrap()
|
|
||||||
} else {
|
} 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);
|
let output_directory_path = PathBuf::from(&output_path_str);
|
||||||
|
|
||||||
if output_directory_path.exists() {
|
if output_directory_path.exists() {
|
||||||
if output_directory_path.is_dir() {
|
if output_directory_path.is_dir() {
|
||||||
let is_empty = fs::read_dir(&output_directory_path)?.next().is_none();
|
let is_empty = fs::read_dir(&output_directory_path)?.next().is_none();
|
||||||
if !is_empty {
|
if !is_empty && !args.quiet {
|
||||||
println!("\nWarning: Output folder already exists and is NOT empty! Files may be overwritten!");
|
log::warn!("Output folder already exists and is NOT empty! Files may be overwritten.");
|
||||||
println!("Press Enter if you want to continue...");
|
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())?;
|
io::stdin().read_line(&mut String::new())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === DEFAULT MODE (original behavior) ===
|
||||||
let app_ctx;
|
let app_ctx;
|
||||||
|
|
||||||
if target_path.is_file() {
|
if target_path.is_file() {
|
||||||
@@ -83,37 +147,55 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
input: InputTarget::File(file),
|
input: InputTarget::File(file),
|
||||||
output_dir: output_directory_path,
|
output_dir: output_directory_path,
|
||||||
options: args.options,
|
options: args.options,
|
||||||
|
dry_run: args.dry_run,
|
||||||
|
quiet: args.quiet,
|
||||||
};
|
};
|
||||||
} else if target_path.is_dir() {
|
} else if target_path.is_dir() {
|
||||||
app_ctx = AppContext {
|
app_ctx = AppContext {
|
||||||
input: InputTarget::Directory(target_path),
|
input: InputTarget::Directory(target_path),
|
||||||
output_dir: output_directory_path,
|
output_dir: output_directory_path,
|
||||||
options: args.options,
|
options: args.options,
|
||||||
|
dry_run: args.dry_run,
|
||||||
|
quiet: args.quiet,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return Err("Invalid input path!".into());
|
return Err("Invalid input path!".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let formats: Vec<Format> = get_registry();
|
let formats: Vec<Format> = get_registry();
|
||||||
println!("Loaded {} formats!", formats.len());
|
log::info!("Loaded {} formats", formats.len());
|
||||||
|
|
||||||
for format in formats {
|
for format in formats {
|
||||||
if let Some(ctx) = (format.detector_func)(&app_ctx)? {
|
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() {
|
if let Some(mut file) = app_ctx.file() {
|
||||||
file.seek(SeekFrom::Start(0))?;
|
file.seek(SeekFrom::Start(0))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
(format.extractor_func)(&app_ctx, ctx)?;
|
(format.extractor_func)(&app_ctx, ctx)?;
|
||||||
|
|
||||||
//extractor returned with no error
|
// Extractor returned with no error
|
||||||
println!("\nExtraction finished! Saved extracted files to {}", output_path_str);
|
if !app_ctx.quiet {
|
||||||
|
println!("\nExtraction finished! Saved extracted files to {}", output_path_str);
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\nInput format not recognized!");
|
if !app_ctx.quiet {
|
||||||
Ok(())
|
println!("\nInput format not recognized!");
|
||||||
|
}
|
||||||
|
Err("Unrecognized input format".into())
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -3,4 +3,5 @@ pub mod aes;
|
|||||||
pub mod lzop;
|
pub mod lzop;
|
||||||
pub mod sparse;
|
pub mod sparse;
|
||||||
pub mod compression;
|
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}};
|
use aes::{Aes128, Aes256};
|
||||||
type Aes128CbcDec = Decryptor<Aes128>;
|
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>> {
|
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 mut data = encrypted_data.to_vec();
|
||||||
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
|
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
|
||||||
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data)
|
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())
|
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>> {
|
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 mut data = encrypted_data.to_vec();
|
||||||
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
|
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
|
||||||
|
|
||||||
let decrypted = decryptor
|
let decrypted = decryptor
|
||||||
.decrypt_padded_mut::<NoPadding>(&mut data)
|
.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())
|
Ok(decrypted.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
use ecb::{Decryptor as EcbDecryptor, cipher::{KeyInit, generic_array::GenericArray}};
|
// ── AES-128 ECB ─────────────────────────────────────────────────────────────
|
||||||
type Aes128EcbDec = EcbDecryptor<Aes128>;
|
|
||||||
|
|
||||||
|
/// 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>> {
|
pub fn decrypt_aes128_ecb(key: &[u8; 16], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
let mut buffer = ciphertext.to_vec();
|
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)
|
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 {
|
if segment_header.compressed_size > segment_header.uncompressed_size {
|
||||||
println!("{:?}", segment_header);
|
println!("{:?}", segment_header);
|
||||||
}
|
}
|
||||||
//println!("{:?}", segment_header);
|
|
||||||
if segment_header.uncompressed_size == 0 {
|
if segment_header.uncompressed_size == 0 {
|
||||||
break
|
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)
|
} else if chunk_header.chunk_type == 0xCAC2 { // "fill" type chunk (fill size with a value)
|
||||||
if chunk_data.len() != 4 {
|
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_size = (chunk_header.chunk_size * file_header.block_size) / 4;
|
||||||
let fill_data = chunk_data.repeat(fill_size as usize);
|
let fill_data = chunk_data.repeat(fill_size as usize);
|
||||||
|
|||||||
Reference in New Issue
Block a user