mstar: add support for old secure format, add option to keep unknown dest

This commit is contained in:
theubusu
2026-05-11 16:21:45 +02:00
parent c8a691d776
commit 30fafbd3fc
6 changed files with 156 additions and 35 deletions
+5
View File
@@ -101,8 +101,13 @@ Tip: if you have split ROM (.ROM-00 and .ROM-01), extract both into the same fol
**Used in:** Many MStar-based TVs (Hisense, Toshiba...)
**Notes:** All files should be supported, includes lzop, lz4, lzma, sparse_write support
**Options:**
`mstar:keep_unknown` - Save data with unknown destination.
※ Support `dump_dec_hdrs` option (will save the script)
## MStar upgrade bin (Secure, old)
**Used in:** Older MStar-based TVs with Secure upgrade mode (encrypted+signed)
**Notes:** Only default upgrade key is supported. This use the extractor above after decrypting.
## MediaTek BDP
**Used in:** Many MediaTek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...)
**Notes:** Some older files may fail to extract
+6
View File
@@ -8,6 +8,7 @@ pub struct Format {
}
pub mod mstar;
pub mod mstar_secure_old;
pub mod samsung_old;
pub mod nvt_timg;
pub mod pfl_upg;
@@ -58,6 +59,11 @@ pub fn get_registry() -> Vec<Format> {
detector_func: crate::formats::mstar::is_mstar_file,
extractor_func: crate::formats::mstar::extract_mstar,
},
Format {
name: "mstar_secure_old",
detector_func: crate::formats::mstar_secure_old::is_mstar_secure_old_file,
extractor_func: crate::formats::mstar_secure_old::extract_mstar_secure_old,
},
Format {
name: "samsung_old",
detector_func: crate::formats::samsung_old::is_samsung_old_dir,
+40 -34
View File
@@ -126,45 +126,51 @@ pub fn extract_mstar(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), Box
println!("\nPart - Offset: {}, Size: {} --> {}", offset, size, partname);
if partname == "unknown" {
println!("- Unknown destination, skipping!");
} else {
let data = common::read_file(&file, offset, size.try_into().unwrap())?;
let out_data;
let output_path = Path::new(&app_ctx.output_dir).join(format!("{}.bin", partname));
if compression == CompressionType::Lzma {
println!("- Decompressing LZMA...");
out_data = decompress_lzma(&data)?;
} else if compression == CompressionType::DoubleLzma {
println!("- Decompressing LZMA (Pass 1)...");
let pass_1 = decompress_lzma(&data)?;
println!("- Decompressing LZMA (Pass 2)...");
out_data = decompress_lzma(&pass_1)?;
} else if compression == CompressionType::Lz4 {
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..");
unlzop_to_file(&data, output_path)?;
println!("-- Saved file!");
i += 1;
continue
} else if compression == CompressionType::Sparse {
println!("- Unsparsing...");
unsparse_to_file(&data, output_path)?;
println!("-- Saved file!");
i += 1;
continue
let data = common::read_file(&file, offset, size.try_into().unwrap())?;
let out_data;
let output_path = if partname == "unknown" {
if app_ctx.has_option("mstar:keep_unknown") {
println!("- Warning, unknown destination - saving to _unknown_{}.bin", offset);
Path::new(&app_ctx.output_dir).join(format!("_unknown_{}.bin", offset))
} else {
out_data = data;
println!("- Warning, unknown destination - skipping...");
continue;
}
} else {
Path::new(&app_ctx.output_dir).join(format!("{}.bin", partname))
};
fs::create_dir_all(&app_ctx.output_dir)?;
let mut out_file = OpenOptions::new().append(true).create(true).open(output_path)?;
out_file.write_all(&out_data)?;
if compression == CompressionType::Lzma {
println!("- Decompressing LZMA...");
out_data = decompress_lzma(&data)?;
} else if compression == CompressionType::DoubleLzma {
println!("- Decompressing LZMA (Pass 1)...");
let pass_1 = decompress_lzma(&data)?;
println!("- Decompressing LZMA (Pass 2)...");
out_data = decompress_lzma(&pass_1)?;
} else if compression == CompressionType::Lz4 {
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..");
unlzop_to_file(&data, output_path)?;
println!("-- Saved file!");
i += 1;
continue
} else if compression == CompressionType::Sparse {
println!("- Unsparsing...");
unsparse_to_file(&data, output_path)?;
println!("-- Saved file!");
i += 1;
continue
} else {
out_data = data;
}
fs::create_dir_all(&app_ctx.output_dir)?;
let mut out_file = OpenOptions::new().append(true).create(true).open(output_path)?;
out_file.write_all(&out_data)?;
println!("-- Saved file!");
}
i += 1;
+19
View File
@@ -0,0 +1,19 @@
use binrw::BinRead;
pub static MSTAR_DEFAULT_UPGRADE_KEY: [u8; 16] = [0x00, 0x07, 0xff, 0x41, 0x54, 0x53, 0x4d, 0x92, 0xfc, 0x55, 0xaa, 0x0f, 0xff, 0x01, 0x10, 0xe0];
pub static CHUNK_ID: &[u8; 8] = b"MSTAR...";
pub static CHUNK_END: &[u8; 8] = b"...mstar";
#[derive(BinRead)]
pub struct ChunkFileFooter {
_chunk_id: [u8; 8],
pub segment_size: u32,
pub file_data_offset: u32,
pub file_data_len: u32,
_file_hash_offset: u32,
_file_hash_len: u32,
_file_signature_offset: u32,
_file_signature_len: u32,
_reserved: [u8; 84],
_chunk_end: [u8; 8],
}
+70
View File
@@ -0,0 +1,70 @@
mod include;
use std::any::Any;
use crate::{AppContext, InputTarget};
use std::path::Path;
use std::fs::{self, File, OpenOptions};
use std::io::{Cursor, Write};
use binrw::BinReaderExt;
use crate::utils::common;
use crate::utils::aes::decrypt_aes128_ecb;
use crate::formats::mstar::{extract_mstar, is_mstar_file};
use include::*;
pub struct MstarSecureCtx {
dec_footer: Vec<u8>,
}
pub fn is_mstar_secure_old_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();
if file_size < 128 {
return Ok(None);
}
let enc_footer = common::read_file(&file, file_size - 128, 128)?;
let dec_footer = decrypt_aes128_ecb(&MSTAR_DEFAULT_UPGRADE_KEY, &enc_footer)?;
if &dec_footer[0..8] == CHUNK_ID && &dec_footer[120..128] == CHUNK_END {
Ok(Some(Box::new(MstarSecureCtx {dec_footer})))
} else {
Ok(None)
}
}
pub fn extract_mstar_secure_old(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::<MstarSecureCtx>().expect("Missing context");
let hdr: ChunkFileFooter = Cursor::new(ctx.dec_footer).read_le()?;
println!("Info -\nSegment size: {}\nFile data offset: {}\nFile data len: {}", hdr.segment_size, hdr.file_data_offset, hdr.file_data_len);
let enc_data = common::read_file(&mut file, hdr.file_data_offset as u64, hdr.file_data_len as usize)?;
println!("Decrypting...");
let dec_data = decrypt_aes128_ecb(&MSTAR_DEFAULT_UPGRADE_KEY, &enc_data)?;
let output_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).open(&output_path)?;
out_file.write_all(&dec_data)?;
println!("- Saved decrypted file!");
//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()
};
//do check just in case and extract
if let Some(result) = is_mstar_file(&in_ctx)? {
extract_mstar(&in_ctx, result)?;
} else {
return Err("detection failed on decrypted data".into());
}
Ok(())
}
+16 -1
View File
@@ -1,6 +1,6 @@
use aes::Aes128;
use cbc::{Decryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}};
use cbc::{Decryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}};
type Aes128CbcDec = Decryptor<Aes128>;
pub fn decrypt_aes128_cbc_pcks7(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
@@ -21,4 +21,19 @@ pub fn decrypt_aes128_cbc_nopad(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8;
.map_err(|e| format!("UnpadError: {:?}", e))?;
Ok(decrypted.to_vec())
}
use ecb::{Decryptor as EcbDecryptor, cipher::{KeyInit, generic_array::GenericArray}};
type Aes128EcbDec = EcbDecryptor<Aes128>;
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 decryptor = Aes128EcbDec::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)
}