From e06ba58725ea031788e1dbe05ed6a0d342f8f428 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:32:37 +0100 Subject: [PATCH 1/6] epk2: fix extraction --- src/formats/epk2.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/formats/epk2.rs b/src/formats/epk2.rs index 845d8e0..7c13bac 100644 --- a/src/formats/epk2.rs +++ b/src/formats/epk2.rs @@ -75,6 +75,8 @@ pub fn is_epk2_file(file: &File) -> bool { } pub fn extract_epk2(mut file: &File, output_folder: &str) -> Result<(), Box> { + file.seek(SeekFrom::Start(0))?; + let _header_signature = common::read_exact(&mut file, SIGNATURE_SIZE as usize)?; let stored_header = common::read_exact(&mut file, 1584)?; //max header size From 7b9b1211718b4b64dfad27ee71bc39e75970d1f5 Mon Sep 17 00:00:00 2001 From: WeebDataHoarder Date: Sun, 15 Feb 2026 00:24:05 +0100 Subject: [PATCH 2/6] pana_dvd: add keys for missing firmware sets, fix #2 --- src/keys.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/keys.rs b/src/keys.rs index af7ab8a..0dda242 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -61,6 +61,12 @@ pub static PFLUPG: &[(&str, &str)] = &[ //pana dvd keys (no AES) pub static PANA_DVD_KEYONLY: &[&str] = &[ ("08E03D859AF9F3EE"), //B3A25A0B9D864F08 # 87 firmwares, ..-~2011 + ("AF4D5B16C298C16D"), //720FCB305F2F6EDA # 3 firmwares, ~2011 + ("9407311B7561F97E"), //FBCC4CEA55466CA6 # 12 firmwares, ~2012 + ("38BB0C17D362701F"), //8AC6E0CFF65651F5 # 8 firmwares, ~2013 + ("EC238CD791456646"), //5FF6D2A1B8675D6F # 3 firmwares, ~2013 + ("8F8CA88482E43D9B"), //918C319720ACFEC1 # 5 firmwares, ~2014 + ("21FCA17361E66B4E"), //542FD3A55F200FD5 # 5 firmwares, ~2014 ]; //pana dvd keys (PANAEUSB pair - AES key, AES IV, cust) From 2519be0187e1e449c4a6522a00c0fd7649b464b9 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:11:47 +0100 Subject: [PATCH 3/6] pana_dvd: add support for DRIVE firmware decryption/decompression + some other tweaks --- src/formats/pana_dvd.rs | 130 +++++++++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 41 deletions(-) diff --git a/src/formats/pana_dvd.rs b/src/formats/pana_dvd.rs index 8985ba4..bad1ce3 100644 --- a/src/formats/pana_dvd.rs +++ b/src/formats/pana_dvd.rs @@ -84,8 +84,10 @@ struct MainListEntry { checksum: u32, //checksum of this MAIN entrys' data } +const COMPRESSED_FILE_MAGIC: &[u8; 8] = b"EXTRHEAD"; + #[derive(BinRead)] -struct MainEntryHeader { +struct CompressedFileHeader { #[br(count = 14)] _header_string: Vec, //EXTRHEADDRV \x01\x00 compression_type_byte: u16, decompressed_size: u32, @@ -98,12 +100,12 @@ struct MainEntryHeader { _checksum_flag: u8, #[br(count = 19)] _unused: Vec, } -impl MainEntryHeader { +impl CompressedFileHeader { fn compression_type(&self) -> &str { if self.compression_type_byte == 0 { return "Uncompressed" } else if self.compression_type_byte == 1 { - return "GZIP + LZSS" + return "GZIP" } else if self.compression_type_byte == 2 { return "LZSS" } else { @@ -198,7 +200,9 @@ pub fn extract_pana_dvd(mut file: &File, output_folder: &str, context: PanaDvdCo if file_entry.size == 0 && file_entry.offset == 0 { break } - file_entries.push(FileEntry { offset: file_entry.offset, base_offset: context.base_hdr_size }); + if !file_entries.iter().any(|f| f.offset == file_entry.offset ){ + file_entries.push(FileEntry { offset: file_entry.offset, base_offset: context.base_hdr_size }); + } } } else { @@ -231,14 +235,20 @@ fn extract_file(file_reader: &mut Cursor>, offset: u64, base_offset: u64 let mut modules: Vec = Vec::new(); for i in 0..100 { - let entry: ModuleEntry = hdr_reader.read_le()?; + let mut entry: ModuleEntry = hdr_reader.read_le()?; if !entry.is_valid() {break}; println!("Module {} - Name: {}, Version: {}, Platform: {}, ID: {}, Offset: {}, Size: {}", i + 1, entry.name(), entry.version(), entry.platform(), entry.id(), entry.offset, entry.size); - if modules.iter().any(|m| m.name() == entry.name()){ + if modules.iter().any(|m| m.offset == entry.offset ){ println!("- Duplicate module, skipping!"); continue } + + //prevent collision of modules with the same name + if modules.iter().any(|m| m.name() == entry.name() ){ + entry.name_bytes = format!("{}({})", entry.name(), i + 1).as_bytes().to_vec(); + } + modules.push(entry); } @@ -254,14 +264,19 @@ fn extract_file(file_reader: &mut Cursor>, offset: u64, base_offset: u64 file_reader.seek(SeekFrom::Start(rel_offset))?; if module.name() == "MAIN" { - println!("Extracting MAIN..."); + println!("- Extracting MAIN..."); extract_main(file_reader, key, output_path)?; continue } let data = common::read_exact(file_reader, module.size as usize)?; println!("- Decrypting..."); - let dec_data = decrypt_data(&data, &key); + let mut dec_data = decrypt_data(&data, &key); + + if module.name().starts_with("DRV") { + println!("- Extracting DRIVE firmware..."); + dec_data = extract_drv(dec_data, &key)?; + } fs::create_dir_all(&output_folder)?; let mut out_file = OpenOptions::new().write(true).create(true).open(output_path)?; @@ -275,9 +290,12 @@ fn extract_file(file_reader: &mut Cursor>, offset: u64, base_offset: u64 fn extract_main(file_reader: &mut Cursor>, key: [u8; 8], output_path: PathBuf) -> Result<(), Box> { let main_list_hdr: MainListHeader = file_reader.read_le()?; - println!("MAIN - Entry count: {}, Decompressed part size: {}", main_list_hdr.entry_count(), main_list_hdr.decompressed_part_size); - assert!(main_list_hdr.entry_count() < 200, "Unreasonable MAIN entry count!"); + if main_list_hdr.entry_count() > 200 { + println!("Unsupported MAIN data, skipping!"); + return Ok(()) + } + println!("MAIN - Entry count: {}, Decompressed part size: {}", main_list_hdr.entry_count(), main_list_hdr.decompressed_part_size); let mut main_entries: Vec = Vec::new(); for i in 0..main_list_hdr.entry_count() { let main_entry: MainListEntry = file_reader.read_le()?; @@ -304,37 +322,9 @@ fn extract_main(file_reader: &mut Cursor>, key: [u8; 8], output_path: Pa data.copy_from_slice(&decrypted); } - let mut data_reader = Cursor::new(data); - let header: MainEntryHeader = data_reader.read_le()?; - println!("\nMAIN ({}/{}) - Compressed size: {}, Decompressed size: {}, Compression type: {}({})", - maine_i, main_entries.len(), header.compressed_size, header.decompressed_size, header.compression_type_byte, header.compression_type()); - let compressed_data = common::read_exact(&mut data_reader, header.compressed_size as usize)?; - - let decompressed_data; - if header.compression_type_byte == 1 { //gzip + lzss - println!("- (1/2) Decompressing GZIP..."); - let decompressed_gzip = decompress_gzip(&compressed_data)?; - // the decompressed data has another header - let mut decompressed_gzip_reader = Cursor::new(decompressed_gzip); - let header: MainEntryHeader = decompressed_gzip_reader.read_le()?; - println!("- (2/2) Decompressing LZSS... (Compressed size: {}, Decompressed size: {})", header.compressed_size, header.decompressed_size); - let compressed_lzss = common::read_exact(&mut decompressed_gzip_reader, header.compressed_size as usize)?; - decompressed_data = decompress_lzss(&compressed_lzss); - assert!(decompressed_data.len() == header.decompressed_size as usize, "Decompressed size does not match size in header, decompression failed!"); - - } else if header.compression_type_byte == 2 { //only lzss - println!("- Decompressing LZSS..."); - decompressed_data = decompress_lzss(&compressed_data); - assert!(decompressed_data.len() == header.decompressed_size as usize, "Decompressed size does not match size in header, decompression failed!"); - - } else if header.compression_type_byte == 0 { //no compression. havent encountered one yet - decompressed_data = compressed_data; - - } else { - println!("- Unknown compression method!"); - decompressed_data = compressed_data; - } - + print!("\nMAIN ({}/{}) - ", maine_i, main_entries.len()); + let decompressed_data = decompress_data(&data)?; + let mut out_file = OpenOptions::new().append(true).create(true).open(&output_path)?; out_file.write_all(&decompressed_data)?; @@ -342,4 +332,62 @@ fn extract_main(file_reader: &mut Cursor>, key: [u8; 8], output_path: Pa } Ok(()) +} + +fn decompress_data(data: &[u8]) -> Result, Box> { + let mut data_reader = Cursor::new(data); + let header: CompressedFileHeader = data_reader.read_le()?; + println!("Compressed size: {}, Decompressed size: {}, Compression type: {}({})", + header.compressed_size, header.decompressed_size, header.compression_type_byte, header.compression_type()); + + let compressed_data = common::read_exact(&mut data_reader, header.compressed_size as usize)?; + let decompressed_data; + + if header.compression_type_byte == 1 { //gzip + optionally lzss + println!("- Decompressing GZIP..."); + let decompressed_gzip = decompress_gzip(&compressed_data)?; + + // the decompressed data can have another header + if decompressed_gzip.starts_with(COMPRESSED_FILE_MAGIC) { + decompressed_data = decompress_data(&decompressed_gzip)?; + } else { + decompressed_data = decompressed_gzip; + } + } else if header.compression_type_byte == 2 { //only lzss + println!("- Decompressing LZSS..."); + decompressed_data = decompress_lzss(&compressed_data); + if decompressed_data.len() != header.decompressed_size as usize { + return Err("Decompressed size does not match size in header, decompression failed!".into()); + } + } else if header.compression_type_byte == 0 { //no compression. havent encountered one yet + decompressed_data = compressed_data; + + } else { + println!("- Unknown compression method!"); + decompressed_data = compressed_data; + } + + Ok(decompressed_data) +} + +fn extract_drv(mut data: Vec, key: &[u8; 8]) -> Result, Box> { + let data_size = data.len(); + let decrypt_size: usize = 10240; + let header_size = 0x20; + + //decrypt first and last 10240b (execpt last 48b) + let first_decrypted = decrypt_data(&data[..decrypt_size], &key); + data[..decrypt_size].copy_from_slice(&first_decrypted); + + let last_decrypted = decrypt_data(&data[data_size as usize - decrypt_size - 48..data_size - 48], &key); + data[data_size as usize - decrypt_size - 48..data_size - 48].copy_from_slice(&last_decrypted); + + //can be compressed + if data[header_size..].starts_with(COMPRESSED_FILE_MAGIC) { + let decompressed = decompress_data(&data[header_size..])?; + data.truncate(header_size); + data.extend_from_slice(&decompressed); + } + + Ok(data) } \ No newline at end of file From 3da0212706b0683e1aee6227600aec5f7bfed951 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:23:05 +0100 Subject: [PATCH 4/6] edited Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1233822..03e7de4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ If an output folder is not provided, extracted files will be saved in folder `_< ## Panasonic Blu-Ray (PANA_DVD.FRM, PANA_ESD.FRM, PANAEUSB.FRM) **Used in:** Panasonic Blu-Ray Players and Recorders -**Notes:** **Depends on keys** - see keys.rs, and there is an issue with some ancient files not extracting correctly. +**Notes:** **Depends on keys** - see keys.rs (Included keys should work for 99% of players released in and before 2014), and there is an issue with some ancient files not extracting correctly. ## Philips UPG (Autorun.upg, 2SWU3TXV) **Used in:** Philips pre-TPVision TVs 200?-2013 From e03bcc9505f7c6f66efb16b66028e3d6bebf12a3 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:24:51 +0100 Subject: [PATCH 5/6] edited Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03e7de4..0fd06c2 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ If an output folder is not provided, extracted files will be saved in folder `_< ## Panasonic Blu-Ray (PANA_DVD.FRM, PANA_ESD.FRM, PANAEUSB.FRM) **Used in:** Panasonic Blu-Ray Players and Recorders -**Notes:** **Depends on keys** - see keys.rs (Included keys should work for 99% of players released in and before 2014), and there is an issue with some ancient files not extracting correctly. +**Notes:** **Depends on keys** - see keys.rs (Included keys should work for 99% of players released in and before 2014, and some released in 2018), Note that there is currently an issue with MAIN in some very ancient files not extracting correctly. ## Philips UPG (Autorun.upg, 2SWU3TXV) **Used in:** Philips pre-TPVision TVs 200?-2013 From dfbe810753fb1a233dbbe667ec231ab0ce36bf23 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:41:06 +0100 Subject: [PATCH 6/6] sddl_sec: update to match 6.0 version sddl_dec --- src/formats/sddl_sec.rs | 443 ++++++++++++++++++++++++++-------------- 1 file changed, 287 insertions(+), 156 deletions(-) diff --git a/src/formats/sddl_sec.rs b/src/formats/sddl_sec.rs index 4cf2fff..d08346a 100644 --- a/src/formats/sddl_sec.rs +++ b/src/formats/sddl_sec.rs @@ -1,4 +1,4 @@ -//sddl_dec 5.0 +//sddl_dec 6.0 https://github.com/theubusu/sddl_dec use std::path::{Path, PathBuf}; use std::fs::{self, File, OpenOptions}; @@ -9,107 +9,7 @@ use crate::utils::common; use crate::utils::aes::{decrypt_aes128_cbc_pcks7}; use crate::utils::compression::{decompress_zlib}; -#[derive(BinRead)] -struct SddlSecHeader { - #[br(count = 4)] _magic_bytes: Vec, //0x11, 0x22, 0x33, 0x44 - #[br(count = 4)] _unused: Vec, - #[br(count = 4)] info_entries_count_str_bytes: Vec, - #[br(count = 4)] module_entries_count_str_bytes: Vec, - #[br(count = 16)] _unk: Vec, -} -impl SddlSecHeader { - fn info_entry_count(&self) -> u32 { - let string = common::string_from_bytes(&self.info_entries_count_str_bytes); - string.parse().unwrap() - } - fn module_entries_count(&self) -> u32 { - let string = common::string_from_bytes(&self.module_entries_count_str_bytes); - string.parse().unwrap() - } -} - -#[derive(BinRead)] -struct EntryHeader { - #[br(count = 12)] name_str_bytes: Vec, - #[br(count = 12)] size_str_bytes: Vec, -} -impl EntryHeader { - fn name(&self) -> String { - common::string_from_bytes(&self.name_str_bytes) - } - fn size(&self) -> u64 { - let string = common::string_from_bytes(&self.size_str_bytes); - string.parse().unwrap() - } -} - -#[derive(BinRead)] -struct ModuleHeader { - #[br(count = 4)] _magic_bytes: Vec, //0x11, 0x22, 0x33, 0x44 - _unk1: u8, - _id: u8, - #[br(count = 10)] _unused: Vec, - #[br(count = 4)] _file_base_version: Vec, - #[br(count = 4)] _file_previous_version: Vec, - #[br(count = 4)] file_version: Vec, - #[br(count = 4)] _unused2: Vec, - _index: u16, - #[br(count = 2)] control_bytes: Vec, - compressed_data_size: u32, - _uncompressed_data_size: u32, - _checksum: u32, -} -impl ModuleHeader { - fn is_compressed(&self) -> bool { - self.control_bytes[0] == 0x3 - } -} - -#[derive(BinRead)] -struct ContentHeader { - _magic1: u8, - #[br(count = 4)] dest_offset_bytes: Vec, - #[br(count = 4)] source_offset_bytes: Vec, - size: u32, - _magic2: u8, -} -impl ContentHeader { - fn dest_offset(&self) -> u32 { - let first_byte; - if self.dest_offset_bytes[0] & 0xF0 == 0xD0 { - first_byte = self.dest_offset_bytes[0] & 0x0F; - } else { - first_byte = self.dest_offset_bytes[0]; - } - u32::from_be_bytes([first_byte, self.dest_offset_bytes[1], self.dest_offset_bytes[2], self.dest_offset_bytes[3]]) - } - fn source_offset(&self) -> u32 { - u32::from_be_bytes([0x00, self.source_offset_bytes[1], self.source_offset_bytes[2], self.source_offset_bytes[3]]) - } -} - -static DEC_KEY: [u8; 16] = [ - 0x26, 0xE0, 0x96, 0xD3, 0xEF, 0x8A, 0x8F, 0xBB, - 0xAA, 0x5E, 0x51, 0x6F, 0x77, 0x26, 0xC2, 0x2C, -]; - -static DEC_IV: [u8; 16] = [ - 0x3E, 0x4A, 0xE2, 0x3A, 0x69, 0xDB, 0x81, 0x54, - 0xCD, 0x88, 0x38, 0xC4, 0xB9, 0x0C, 0x76, 0x66, -]; - -pub fn is_sddl_sec_file(file: &File) -> bool { - let header = common::read_file(&file, 0, 32).expect("Failed to read from file."); - let deciph_header = decipher(&header); - if deciph_header.starts_with(b"\x11\x22\x33\x44") { - true - } else { - false - } -} - -//ported from original from https://nese.team/posts/justctf/ -fn decipher(s: &[u8]) -> Vec { +pub fn decipher(s: &[u8]) -> Vec { let len_ = s.len(); let mut v3: u32 = 904; let mut out = s.to_vec(); @@ -150,79 +50,310 @@ fn decipher(s: &[u8]) -> Vec { out } +// -- STRUCTURES -- +// -- SECFILE -- + +pub static DOWNLOAD_ID: [u8; 4] = [0x11, 0x22, 0x33, 0x44]; + +#[derive(Debug, BinRead)] +pub struct SecHeader { + pub _download_id: [u8; 4], //always 0x11, 0x22, 0x33, 0x44 - magic? + key_id_str_bytes: [u8; 4], //"key_id", purpose unknown + grp_num_str_bytes: [u8; 4], //"grp_num", the count of groups, also represents the count of info files because each group has a respective info file + prg_num_str_bytes: [u8; 4], //"prg_num", the count of module (.FXX) files + _unused_or_reserved: [u8; 16], //not used, is zeros +} +impl SecHeader { + pub fn key_id(&self) -> u32 { + let string = common::string_from_bytes(&self.key_id_str_bytes); + string.parse().unwrap() + } + pub fn grp_num(&self) -> u32 { + let string = common::string_from_bytes(&self.grp_num_str_bytes); + string.parse().unwrap() + } + pub fn prg_num(&self) -> u32 { + let string = common::string_from_bytes(&self.prg_num_str_bytes); + string.parse().unwrap() + } +} + +pub static INFO_FILE_EXTENSION: &str = ".TXT"; + +#[derive(Debug, BinRead)] +pub struct FileHeader { + name_str_bytes: [u8; 12], + size_str_bytes: [u8; 12], +} +impl FileHeader { + pub fn name(&self) -> String { + common::string_from_bytes(&self.name_str_bytes) + } + pub fn size(&self) -> u64 { + let string = common::string_from_bytes(&self.size_str_bytes); + string.parse().unwrap() + } +} + +// -- MODULE -- +#[derive(Debug, BinRead)] +pub struct ModuleComHeader { //"com_header" + pub download_id: [u8; 4], //always 0x11, 0x22, 0x33, 0x44 - magic? + _outer_maker_id: u8, + _outer_model_id: u8, + _inner_maker_id: u8, + _reserve1: u8, + _reserve2: u32, + _reserve3: u32, + _start_version: [u8; 4], //the first version that can upgrade to the new version + _end_version: [u8; 4], //the last version that can upgrade to the new version + _new_version: [u8; 4], //the new version, as in the version of the data in this module + _reserve4: u16, + _module_num: u16, //the logic seems to indicate that there can be multiple entries in one module, but i have never seen this go above 1. +} + +#[derive(Debug, BinRead)] +pub struct ModuleHeader { //"header", appears after com_header + _module_id: u16, + module_atr: u8, + _target_id: u8, + pub cmp_size: u32, + _org_size: u32, + _crc_value: u32, +} +impl ModuleHeader { + pub fn is_ciphered(&self) -> bool { + (self.module_atr & 0x02) != 0 + } + pub fn is_compressed(&self) -> bool { + (self.module_atr & 0x01) != 0 + } +} + +#[derive(Debug, BinRead)] +pub struct ContentHeader { + _magic1: u8, //always 0x01? + _dest_offset: u32, + _source_offset: u32, + pub size: u32, + _magic2: u8, //always 0x21? +} +impl ContentHeader { + //these hacks are needed because for some reason older files have the first nibble of the offset set to D/C + //no idea why, but masking them off makes it works properly + pub fn dest_offset(&self) -> u32 { + if ((self._dest_offset >> 28) & 0xF) == 0xD { + self._dest_offset & 0x0FFFFFFF + } else { + self._dest_offset + } + } + pub fn source_offset(&self) -> u32 { + if ((self._source_offset >> 28) & 0xF) == 0xC { + self._source_offset & 0x0FFFFFFF + } else { + self._source_offset + } + } + pub fn has_subfile(&self) -> bool { + self.source_offset() == 0x10E + } +} + +// -- TDI -- +// Called SDIT.FDI in the secfile + +pub static TDI_FILENAME: &str = "SDIT.FDI"; +pub static SUPPORTED_TDI_VERSION: u16 = 2; + +#[derive(Debug, BinRead)] +pub struct TdiHead { + pub download_id: [u8; 4], //always 0x11, 0x22, 0x33, 0x44 - magic? + pub num_of_group: u8, + _reserve1: u8, + pub format_version: u16, //checks for "2" here +} + +#[derive(Debug, BinRead)] +pub struct TdiGroupHead { + pub group_id: u8, + pub num_of_target: u8, //logic checks that this is not more than 5 + _reserved: u16, +} + +#[derive(Debug, BinRead)] +pub struct TdiTgtInf { + _outer_maker_id: u8, + _outer_model_id: u8, + _inner_maker_id: u8, + _reserve3: u8, + _inner_model_id: [u8; 4], + _ext_model_id: [u8; 4], + pub _start_version: [u8; 4], //the first version that can upgrade to the new version + pub _end_version: [u8; 4], //the last version that can upgrade to the new version + pub new_version: [u8; 4], //the new version, as in the version of the data in this module + pub target_id: u8, + _num_of_compatible_target: u8, + pub num_of_txx: u16, //"TXX" refers to the ".FXX" segment files of each module. I assume F is an encrypted version of T, the same happens with SDIT; "TDI" -> "FDI" + _unknown: [u8; 8], + module_name_bytes: [u8; 8], +} +impl TdiTgtInf { + pub fn module_name(&self) -> String { + common::string_from_bytes(&self.module_name_bytes) + } + pub fn version_string(&self) -> String { + format!("{}.{}{}{}", self.new_version[0], self.new_version[1], self.new_version[2], self.new_version[3]) + } +} + +// -- dec key -- +static DEC_KEY: [u8; 16] = [ + 0x26, 0xE0, 0x96, 0xD3, 0xEF, 0x8A, 0x8F, 0xBB, + 0xAA, 0x5E, 0x51, 0x6F, 0x77, 0x26, 0xC2, 0x2C, +]; + +static DEC_IV: [u8; 16] = [ + 0x3E, 0x4A, 0xE2, 0x3A, 0x69, 0xDB, 0x81, 0x54, + 0xCD, 0x88, 0x38, 0xC4, 0xB9, 0x0C, 0x76, 0x66, +]; + +pub fn is_sddl_sec_file(file: &File) -> bool { + let header = common::read_file(&file, 0, 32).expect("Failed to read from file."); + let deciph_header = decipher(&header); + if deciph_header.starts_with(b"\x11\x22\x33\x44") { + true + } else { + false + } +} + +fn get_sec_file(mut file: &File) -> Result<(FileHeader, Vec), Box> { + let mut hdr_reader = Cursor::new(decrypt_aes128_cbc_pcks7(&common::read_exact(&mut file, 32)?, &DEC_KEY, &DEC_IV)?); + let file_header: FileHeader = hdr_reader.read_be()?; + let file_data = decrypt_aes128_cbc_pcks7(&common::read_exact(&mut file, file_header.size() as usize)?, &DEC_KEY, &DEC_IV)?; + + Ok((file_header, file_data)) +} + +fn parse_tdi_to_modules(tdi_data: Vec) -> Result, Box> { + let mut tdi_reader = Cursor::new(tdi_data); + let tdi_header: TdiHead = tdi_reader.read_be()?; + if tdi_header.download_id != DOWNLOAD_ID { + return Err("Invalid TDI header!".into()); + } + if tdi_header.format_version != SUPPORTED_TDI_VERSION { + return Err(format!("Unsupported TDI format version {}! (The supported version is {})", tdi_header.format_version, SUPPORTED_TDI_VERSION).into()); + } + + println!("[TDI] Group count: {}", tdi_header.num_of_group); + let mut modules: Vec = Vec::new(); + + for _i in 0..tdi_header.num_of_group { + let group_head: TdiGroupHead = tdi_reader.read_be()?; + println!("[TDI] Group ID: {}, Target count: {}", group_head.group_id, group_head.num_of_target); + + for _i in 0..group_head.num_of_target { + let tgt_inf: TdiTgtInf = tdi_reader.read_be()?; + println!("[TDI] - {}, Target ID: {}, Segment count: {}, Version: {}", + tgt_inf.module_name(), tgt_inf.target_id, tgt_inf.num_of_txx, tgt_inf.version_string()); + + //push unique modules + if !modules.iter().any(|m| m.module_name() == tgt_inf.module_name()) { + modules.push(tgt_inf); + } + } + } + + Ok(modules) +} + pub fn extract_sddl_sec(mut file: &File, output_folder: &str) -> Result<(), Box> { - let mut hdr_reader = Cursor::new(decipher(&common::read_exact(&mut file, 32)?)); - let hdr: SddlSecHeader = hdr_reader.read_be()?; + file.seek(SeekFrom::Start(0))?; + let mut secfile_hdr_reader = Cursor::new(decipher(&common::read_exact(&mut file, 32)?)); + let secfile_header: SecHeader = secfile_hdr_reader.read_be()?; - //SDIT.FDI + info files + module files - let total_entry_count = 1 + hdr.info_entry_count() + hdr.module_entries_count(); - println!("File info:\nInfo entry count: {}\nModule entry count: {}\nTotal entry count: {}", - hdr.info_entry_count(), hdr.module_entries_count(), total_entry_count); + println!("File info -\nKey ID: {}\nGroup count: {}\nModule file count: {}\n", secfile_header.key_id(), secfile_header.grp_num(), secfile_header.prg_num()); + fs::create_dir_all(&output_folder)?; - for i in 0..total_entry_count { - let mut entry_header_reader = Cursor::new(decrypt_aes128_cbc_pcks7(&common::read_exact(&mut file, 32)?, &DEC_KEY, &DEC_IV)?); - let entry_header: EntryHeader = entry_header_reader.read_be()?; + let (tdi_file, tdi_data) = get_sec_file(&file)?; + println!("[TDI] Name: {}, Size: {}", tdi_file.name(), tdi_file.size()); + //if save_extra { //Save SDIT + // let mut out_file = OpenOptions::new().write(true).create(true).open(Path::new(&output_folder).join(tdi_file.name()))?; + // out_file.write_all(&tdi_data)?; + //} + if tdi_file.name() != TDI_FILENAME { + return Err(format!("Invalid TDI filename {}!, expected: {}", tdi_file.name(), TDI_FILENAME).into()); + } + //parse TDI + let modules = parse_tdi_to_modules(tdi_data)?; - println!("\n({}/{}) - {}, Size: {}", i + 1, total_entry_count, entry_header.name(), entry_header.size()); + //get info files, each info file belongs to its respecitve group in the TDI + for i in 0..secfile_header.grp_num() { + let (info_file, info_data) = get_sec_file(&file)?; + println!("\n[INFO] ID: {}, Name: {}, Size: {}", i, info_file.name(), info_file.size()); + if !info_file.name().ends_with(INFO_FILE_EXTENSION) { + return Err(format!("Info file {} does not have the expected extension {}!", info_file.name(), INFO_FILE_EXTENSION).into()); + } + //if save_extra { //Save info file + // let mut out_file = OpenOptions::new().write(true).create(true).open(Path::new(&output_folder).join(info_file.name()))?; + // out_file.write_all(&info_data)?; + //} + //print info file + println!("{}", String::from_utf8_lossy(&info_data)); + } - let data = common::read_exact(&mut file, entry_header.size() as usize)?; - let dec_data = decrypt_aes128_cbc_pcks7(&data, &DEC_KEY, &DEC_IV)?; + //parse module data + for (i, module) in modules.iter().enumerate(){ + println!("\nModule #{}/{} - {}, Target ID: {}, Segment count: {}, Version: {}", + i+1, &modules.len(), module.module_name(), module.target_id, module.num_of_txx, module.version_string()); - fs::create_dir_all(&output_folder)?; - //detect the file type based on the counts of each file - if i == 0 { //SDIT.FDI file - let output_path = Path::new(&output_folder).join(entry_header.name()); - let mut out_file = OpenOptions::new().write(true).create(true).open(output_path)?; - out_file.write_all(&dec_data)?; - println!("-- Saved file!"); + for i in 0..module.num_of_txx { + let (module_file, module_data) = get_sec_file(&file)?; + if !module_file.name().starts_with(&module.module_name()) { + return Err(format!("Module file {} does not start with the module's name: {}!", module_file.name(), module.module_name()).into()); + } + println!(" Segment #{}/{} - Name: {}, Size: {}", i+1, module.num_of_txx, module_file.name(), module_file.size()); - } else if i - 1 < hdr.info_entry_count() { //.TXT info file - println!("{}", String::from_utf8_lossy(&dec_data)); - continue - - } else { //Module file - let name = entry_header.name(); - let source_name = name.split(".").next().unwrap(); - - let mut module_reader = Cursor::new(dec_data); - let module_header: ModuleHeader = module_reader.read_be()?; - println!("- Version: {}.{}{}{}", module_header.file_version[0], module_header.file_version[1], module_header.file_version[2], module_header.file_version[3]); - - let module_data = common::read_exact(&mut module_reader, module_header.compressed_data_size as usize)?; - println!("- Deciphering..."); - let deciphered_data = decipher(&module_data); - - let content: Vec; - if module_header.is_compressed() { - println!("-- Decompressing..."); - content = decompress_zlib(&deciphered_data)?; - } else { - println!("-- Uncompressed..."); - content = deciphered_data; + let mut module_reader = Cursor::new(module_data); + let com_header: ModuleComHeader = module_reader.read_be()?; + if com_header.download_id != DOWNLOAD_ID { + return Err("Invalid module com_header!".into()); } - let mut content_reader = Cursor::new(content); + let module_header: ModuleHeader = module_reader.read_be()?; + let mut module_data = common::read_exact(&mut module_reader, module_header.cmp_size as usize)?; + if module_header.is_ciphered() { + println!(" - Deciphering..."); + module_data = decipher(&module_data); + } + if module_header.is_compressed() { + println!(" - Decompressing..."); + module_data = decompress_zlib(&module_data)?; + } + + let mut content_reader = Cursor::new(module_data); let content_header: ContentHeader = content_reader.read_be()?; + println!(" --> 0x{:X} @ 0x{:X}", content_header.size, content_header.dest_offset()); + + let output_path: PathBuf; + if content_header.has_subfile() { + let sub_filename_bytes = common::read_exact(&mut content_reader, 0x100)?; + let sub_filename = common::string_from_bytes(&sub_filename_bytes); + println!(" --> {}", sub_filename); - let output_path: PathBuf; - if content_header.source_offset() == 270 { - let file_name_bytes = common::read_exact(&mut content_reader, 256)?; - let file_name = common::string_from_bytes(&file_name_bytes); - println!("--- File name: {}", file_name); + let sub_folder_path = Path::new(&output_folder).join(module.module_name()); + fs::create_dir_all(&sub_folder_path)?; + output_path = Path::new(&sub_folder_path).join(sub_filename); - let out_folder_path = Path::new(&output_folder).join(source_name); - fs::create_dir_all(&out_folder_path)?; - output_path = Path::new(&out_folder_path).join(file_name); } else { - output_path = Path::new(&output_folder).join(format!("{}.bin", source_name)); + output_path = Path::new(&output_folder).join(format!("{}.bin", module.module_name())); } let data = common::read_exact(&mut content_reader, content_header.size as usize)?; - let mut out_file = OpenOptions::new().read(true).write(true).create(true).open(output_path)?; out_file.seek(SeekFrom::Start(content_header.dest_offset() as u64))?; out_file.write_all(&data)?; - println!("--- Saved!"); } }