2026-02-16 23:42:23 +01:00
//sddl_dec 6.0
2026-02-05 15:18:26 +01:00
use std ::any ::Any ;
2026-02-06 16:27:13 +01:00
use crate ::{ AppContext , formats ::Format };
2026-02-05 15:18:26 +01:00
pub fn format () -> Format {
2026-02-05 18:53:35 +01:00
Format { name : "sddl_sec" , detector_func : is_sddl_sec_file , extractor_func : extract_sddl_sec }
2026-02-05 15:18:26 +01:00
}
2025-12-04 19:33:23 +01:00
2025-10-07 16:00:32 +02:00
use std ::path ::{ Path , PathBuf };
use std ::fs ::{ self , File , OpenOptions };
2025-12-04 19:33:23 +01:00
use std ::io ::{ Cursor , Seek , SeekFrom , Write };
use binrw ::{ BinRead , BinReaderExt };
2025-10-07 16:00:32 +02:00
2025-11-26 17:29:25 +01:00
use crate ::utils ::common ;
2025-10-30 22:04:59 +01:00
use crate ::utils ::aes ::{ decrypt_aes128_cbc_pcks7 };
2025-11-26 17:29:25 +01:00
use crate ::utils ::compression ::{ decompress_zlib };
2025-10-07 16:00:32 +02:00
2026-02-16 22:41:06 +01:00
pub fn decipher ( s : & [ u8 ]) -> Vec < u8 > {
let len_ = s . len ();
let mut v3 : u32 = 904 ;
let mut out = s . to_vec ();
let mut cnt = 0 ;
if len_ > 0 {
let true_len = if len_ >= 0x80 {
128
} else {
len_
};
if true_len > 0 {
let mut i = 0 ;
let mut j : u8 = 0 ;
while i < s . len () {
let iter_ = s [ i ];
i += 1 ;
j = j . wrapping_add ( 1 );
let v11 = ( iter_ as u32 ). wrapping_add ( 38400 ) & 0xffffffff ;
let v12 = iter_ ^ (( v3 & 0xff00 ) >> 8 ) as u8 ;
v3 = v3 . wrapping_add ( v11 ). wrapping_add ( 163 ) & 0xffffffff ;
if j == 0 {
v3 = 904 ;
}
if cnt < out . len () {
out [ cnt ] = v12 ;
cnt += 1 ;
}
}
}
}
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 );
2025-12-04 19:33:23 +01:00
string . parse (). unwrap ()
}
2026-02-16 22:41:06 +01:00
pub fn grp_num ( & self ) -> u32 {
let string = common ::string_from_bytes ( & self . grp_num_str_bytes );
2025-12-04 19:33:23 +01:00
string . parse (). unwrap ()
}
2026-02-16 22:41:06 +01:00
pub fn prg_num ( & self ) -> u32 {
let string = common ::string_from_bytes ( & self . prg_num_str_bytes );
2025-12-04 19:33:23 +01:00
string . parse (). unwrap ()
}
}
2026-02-16 22:41:06 +01:00
pub static INFO_FILE_EXTENSION : & str = ".TXT" ;
#[derive(Debug, BinRead)]
pub struct FileHeader {
name_str_bytes : [ u8 ; 12 ],
size_str_bytes : [ u8 ; 12 ],
2025-12-04 19:33:23 +01:00
}
2026-02-16 22:41:06 +01:00
impl FileHeader {
pub fn name ( & self ) -> String {
2025-12-04 19:33:23 +01:00
common ::string_from_bytes ( & self . name_str_bytes )
}
2026-02-16 22:41:06 +01:00
pub fn size ( & self ) -> u64 {
2025-12-04 19:33:23 +01:00
let string = common ::string_from_bytes ( & self . size_str_bytes );
string . parse (). unwrap ()
}
}
2026-02-16 22:41:06 +01:00
// -- 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 ,
2025-12-04 19:33:23 +01:00
}
impl ModuleHeader {
2026-02-16 22:41:06 +01:00
pub fn is_ciphered ( & self ) -> bool {
( self . module_atr & 0x02 ) != 0
}
pub fn is_compressed ( & self ) -> bool {
( self . module_atr & 0x01 ) != 0
2025-12-04 19:33:23 +01:00
}
}
2026-02-16 22:41:06 +01:00
#[derive(Debug, BinRead)]
pub struct ContentHeader {
_magic1 : u8 , //always 0x01?
_dest_offset : u32 ,
_source_offset : u32 ,
pub size : u32 ,
_magic2 : u8 , //always 0x21?
2025-12-04 19:33:23 +01:00
}
impl ContentHeader {
2026-02-16 22:41:06 +01:00
//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
2025-12-04 19:33:23 +01:00
} else {
2026-02-16 22:41:06 +01:00
self . _dest_offset
2025-12-04 19:33:23 +01:00
}
}
2026-02-16 22:41:06 +01:00
pub fn source_offset ( & self ) -> u32 {
if (( self . _source_offset >> 28 ) & 0xF ) == 0xC {
self . _source_offset & 0x0FFFFFFF
2025-12-04 19:33:23 +01:00
} else {
2026-02-16 22:41:06 +01:00
self . _source_offset
2025-12-04 19:33:23 +01:00
}
}
2026-02-16 22:41:06 +01:00
pub fn has_subfile ( & self ) -> bool {
self . source_offset () == 0x10E
2025-12-04 19:33:23 +01:00
}
}
2026-02-16 22:41:06 +01:00
// -- 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 --
2025-12-04 19:33:23 +01:00
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 ,
];
2026-02-05 18:53:35 +01:00
pub fn is_sddl_sec_file ( app_ctx : & AppContext ) -> Result < Option < Box < dyn Any >> , Box < dyn std ::error ::Error >> {
2026-02-06 16:27:13 +01:00
let file = match app_ctx . file () { Some ( f ) => f , None => return Ok ( None )};
2025-12-04 19:33:23 +01:00
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 " ) {
2026-02-05 15:18:26 +01:00
Ok ( Some ( Box ::new (())))
2025-10-07 16:00:32 +02:00
} else {
2026-02-05 15:18:26 +01:00
Ok ( None )
2025-10-07 16:00:32 +02:00
}
}
2026-02-16 22:41:06 +01:00
fn get_sec_file ( mut file : & File ) -> Result < ( FileHeader , Vec < u8 > ), Box < dyn std ::error ::Error >> {
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 < u8 > ) -> Result < Vec < TdiTgtInf > , Box < dyn std ::error ::Error >> {
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 < TdiTgtInf > = 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 );
2025-10-07 16:00:32 +02:00
}
}
}
2026-02-16 22:41:06 +01:00
Ok ( modules )
2025-10-07 16:00:32 +02:00
}
2026-02-06 16:27:13 +01:00
pub fn extract_sddl_sec ( app_ctx : & AppContext , _ctx : Box < dyn Any > ) -> Result < (), Box < dyn std ::error ::Error >> {
let mut file = app_ctx . file (). ok_or ( "Extractor expected file" ) ? ;
2026-02-16 22:41:06 +01:00
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 () ? ;
2026-02-06 15:08:38 +01:00
2026-02-16 22:41:06 +01:00
println! ( "File info - \n Key ID: {} \n Group count: {} \n Module file count: {} \n " , secfile_header . key_id (), secfile_header . grp_num (), secfile_header . prg_num ());
2026-02-16 23:42:23 +01:00
fs ::create_dir_all ( & app_ctx . output_dir ) ? ;
2025-10-07 16:00:32 +02:00
2026-02-16 22:41:06 +01:00
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 ) ? ;
2025-10-07 16:00:32 +02:00
2026-02-16 22:41:06 +01:00
//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 ));
}
2025-10-07 16:00:32 +02:00
2026-02-16 22:41:06 +01:00
//parse module data
for ( i , module ) in modules . iter (). enumerate (){
println! ( " \n Module # {} / {} - {} , Target ID: {} , Segment count: {} , Version: {} " ,
i + 1 , & modules . len (), module . module_name (), module . target_id , module . num_of_txx , module . version_string ());
2025-10-07 16:00:32 +02:00
2026-02-16 22:41:06 +01:00
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 ());
2025-10-07 16:00:32 +02:00
2026-02-16 22:41:06 +01:00
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 ());
}
2025-10-07 16:00:32 +02:00
2025-12-04 19:33:23 +01:00
let module_header : ModuleHeader = module_reader . read_be () ? ;
2026-02-16 22:41:06 +01:00
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 );
}
2025-12-04 19:33:23 +01:00
if module_header . is_compressed () {
2026-02-16 22:41:06 +01:00
println! ( " - Decompressing..." );
module_data = decompress_zlib ( & module_data ) ? ;
2025-10-07 16:00:32 +02:00
}
2026-02-16 22:41:06 +01:00
let mut content_reader = Cursor ::new ( module_data );
2025-12-04 19:33:23 +01:00
let content_header : ContentHeader = content_reader . read_be () ? ;
2026-02-16 22:41:06 +01:00
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 );
2025-10-07 16:00:32 +02:00
2026-02-16 23:42:23 +01:00
let sub_folder_path = Path ::new ( & app_ctx . output_dir ). join ( module . module_name ());
2026-02-16 22:41:06 +01:00
fs ::create_dir_all ( & sub_folder_path ) ? ;
output_path = Path ::new ( & sub_folder_path ). join ( sub_filename );
2025-10-07 16:00:32 +02:00
} else {
2026-02-16 23:42:23 +01:00
output_path = Path ::new ( & app_ctx . output_dir ). join ( format! ( " {} .bin" , module . module_name ()));
2025-10-07 16:00:32 +02:00
}
2025-12-04 19:33:23 +01:00
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 ) ? ;
2025-10-07 16:00:32 +02:00
2025-12-04 19:33:23 +01:00
}
2025-10-07 16:00:32 +02:00
}
println! ( " \n Extraction finished!" );
Ok (())
}