@@ -8,6 +8,7 @@ use std::{collections::HashMap, fs::canonicalize, fs::File, io::{Cursor, Read, S
88
99use hex_literal:: hex;
1010use log:: { debug, info, LevelFilter } ;
11+ use image:: { ImageBuffer , Rgb } ;
1112
1213const CMNKEYS : [ [ u8 ; 16 ] ; 6 ] = [
1314 hex ! ( "64c5fd55dd3ad988325baaec5243db98" ) ,
@@ -60,14 +61,42 @@ fn flag_to_bool(flag: u8) -> bool {
6061 }
6162}
6263
64+ fn decode_tiled_icon ( data : & [ u8 ] , width : u32 , height : u32 ) -> ImageBuffer < Rgb < u8 > , Vec < u8 > > {
65+ let mut img = ImageBuffer :: new ( width, height) ;
66+
67+ let tiles_per_row = width / 8 ;
68+
69+ for tile_y in ( 0 ..height) . step_by ( 8 ) {
70+ for tile_x in ( 0 ..width) . step_by ( 8 ) {
71+ let tile_idx = ( tile_y / 8 ) * tiles_per_row + ( tile_x / 8 ) ;
72+ let tile_offset = ( tile_idx * 128 ) as usize ; // 64 pixels * 2 bytes
73+
74+ for py in 0 ..8 {
75+ for px in 0 ..8 {
76+ let morton_idx = ctrutils:: morton_encode_2d ( px, py) as usize ;
77+ let byte_offset = tile_offset + ( morton_idx << 1 ) ; // morton_idx * 2
78+
79+ let pixel_rgb565 = LittleEndian :: read_u16 ( & data[ byte_offset..] ) ;
80+ let ( r, g, b) = ctrutils:: rgb565_to_rgb888 ( pixel_rgb565) ;
81+
82+ img. put_pixel ( tile_x + px, tile_y + py, Rgb ( [ r, g, b] ) ) ;
83+ }
84+ }
85+ }
86+ }
87+
88+ img
89+ }
90+
91+
6392fn get_ncch_aes_counter ( hdr : & NcchHdr , section : NcchSection ) -> [ u8 ; 16 ] {
6493 let mut counter: [ u8 ; 16 ] = [ 0 ; 16 ] ;
6594 if hdr. formatversion == 2 || hdr. formatversion == 0 {
6695 let mut titleid: [ u8 ; 8 ] = hdr. titleid ;
6796 titleid. reverse ( ) ;
6897 counter[ 0 ..8 ] . copy_from_slice ( & titleid) ;
6998 counter[ 8 ] = section as u8 ;
70-
99+
71100 } else if hdr. formatversion == 1 {
72101 let x = match section {
73102 NcchSection :: ExHeader => 512 ,
@@ -97,7 +126,7 @@ fn scramblekey(key_x: u128, key_y: u128) -> u128 {
97126 rol ( value, 87 )
98127}
99128
100- fn dump_section ( ncch : & mut File , cia : & mut CiaReader , offset : u64 , size : u32 , sec_type : NcchSection , sec_idx : usize , ctr : [ u8 ; 16 ] , uses_extra_crypto : u8 , fixed_crypto : u8 , use_seed_crypto : bool , encrypted : bool , keyys : [ u128 ; 2 ] ) {
129+ fn dump_section ( ncch : & mut File , cia : & mut CiaReader , offset : u64 , size : u32 , sec_type : NcchSection , sec_idx : usize , ctr : [ u8 ; 16 ] , uses_extra_crypto : u8 , fixed_crypto : u8 , use_seed_crypto : bool , encrypted : bool , keyys : [ u128 ; 2 ] , titleid : [ u8 ; 8 ] , icons : bool ) {
101130 let sections = [ "ExHeader" , "ExeFS" , "RomFS" ] ;
102131 const CHUNK : u32 = 4194304 ; // 4 MiB
103132 debug ! ( " {} offset: {:08X}" , sections[ sec_idx] , offset) ;
@@ -126,7 +155,7 @@ fn dump_section(ncch: &mut File, cia: &mut CiaReader, offset: u64, size: u32, se
126155 ncch. write_all ( & buf) . unwrap ( ) ;
127156 sizeleft -= CHUNK ;
128157 }
129-
158+
130159 if sizeleft > 0 {
131160 buf = vec ! [ 0u8 ; sizeleft as usize ] ;
132161 cia. read ( & mut buf) ;
@@ -159,41 +188,96 @@ fn dump_section(ncch: &mut File, cia: &mut CiaReader, offset: u64, size: u32, se
159188 let mut exedata = vec ! [ 0u8 ; size as usize ] ;
160189 cia. read ( & mut exedata) ;
161190 let mut exetmp = exedata. clone ( ) ;
191+
162192 Aes128Ctr :: new_from_slices ( & key, & ctr)
163193 . unwrap ( )
164194 . apply_keystream ( & mut exetmp) ;
165195
196+ // ASCII for 'icon'
197+ let icon: [ u8 ; 4 ] = hex ! ( "69636f6e" ) ;
198+ // ASCII for 'banner'
199+ let banner: [ u8 ; 6 ] = hex ! ( "62616e6e6572" ) ;
200+
201+ let path = Path :: new ( & cia. name ) ;
202+ let parent_dir = path. canonicalize ( )
203+ . unwrap ( )
204+ . parent ( )
205+ . unwrap ( )
206+ . to_path_buf ( ) ;
207+
208+ #[ repr( C ) ]
209+ struct ExeInfo {
210+ fname : [ u8 ; 8 ] ,
211+ off : [ u8 ; 4 ] ,
212+ size : [ u8 ; 4 ] ,
213+ }
214+
215+ if icons {
216+ for i in 0usize ..10 {
217+ let exebytes = & exetmp[ i * 16 ..( i + 1 ) * 16 ] ;
218+ let exeinfo: ExeInfo = unsafe { std:: mem:: transmute ( LittleEndian :: read_u128 ( exebytes) ) } ;
219+
220+ let mut off = LittleEndian :: read_u32 ( & exeinfo. off ) as usize ;
221+ let size = LittleEndian :: read_u32 ( & exeinfo. size ) as usize ;
222+ off += 512 ;
223+
224+ match exeinfo. fname . iter ( ) . rposition ( |& x| x != 0 ) {
225+ Some ( zero_idx) => {
226+ if exeinfo. fname [ ..=zero_idx] == icon {
227+ if size >= 0x36C0 {
228+ let icon_data = & exetmp[ off..off + size] ;
229+
230+ // SMDH magic check
231+ if & icon_data[ 0 ..4 ] == b"SMDH" {
232+
233+ let icon_24x24_offset = 0x2040 ;
234+ let icon_24x24_size = 0x480 ;
235+
236+ let icon_48x48_offset = 0x24C0 ;
237+ let icon_48x48_size = 0x1200 ;
238+
239+ if icon_data. len ( ) >= icon_24x24_offset + icon_24x24_size {
240+ let icon_24x24_rgb565 = & icon_data[ icon_24x24_offset..icon_24x24_offset + icon_24x24_size] ;
241+ // TODO: decode_tiled_icon
242+ let icon_24x24_png = decode_tiled_icon ( icon_24x24_rgb565, 24 , 24 ) ;
243+
244+ icon_24x24_png. save ( format ! ( "{}/{}_icon_24x24.png" , parent_dir. display( ) , hex:: encode( titleid) . to_uppercase( ) ) ) . unwrap ( ) ;
245+ }
246+
247+ if icon_data. len ( ) >= icon_48x48_offset + icon_48x48_size {
248+ let icon_48x48_rgb565 = & icon_data[ icon_48x48_offset..icon_48x48_offset + icon_48x48_size] ;
249+ let icon_48x48_img = decode_tiled_icon ( icon_48x48_rgb565, 48 , 48 ) ;
250+
251+ icon_48x48_img. save ( format ! ( "{}/{}_icon_48x48.png" , parent_dir. display( ) , hex:: encode( titleid) . to_uppercase( ) ) ) . unwrap ( ) ;
252+ }
253+ }
254+ }
255+ }
256+ }
257+ None => ( ) ,
258+ }
259+ }
260+ }
261+
166262 if flag_to_bool ( uses_extra_crypto) || use_seed_crypto {
167263 let mut exetmp2 = exedata;
168264 key = u128:: to_be_bytes ( scramblekey ( KEYS_0 [ get_crypto_key ( & uses_extra_crypto) ] , keyys[ 1 ] ) ) ;
169-
265+
170266 Aes128Ctr :: new_from_slices ( & key, & ctr)
171267 . unwrap ( )
172268 . apply_keystream ( & mut exetmp2) ;
173269
174- #[ repr( C ) ]
175- struct ExeInfo {
176- fname : [ u8 ; 8 ] ,
177- off : [ u8 ; 4 ] ,
178- size : [ u8 ; 4 ] ,
179- }
180-
181270 for i in 0usize ..10 {
182271 let exebytes = & exetmp[ i * 16 ..( i + 1 ) * 16 ] ;
183272 let exeinfo: ExeInfo = unsafe { std:: mem:: transmute ( LittleEndian :: read_u128 ( exebytes) ) } ;
184-
273+
185274 let mut off = LittleEndian :: read_u32 ( & exeinfo. off ) as usize ;
186275 let size = LittleEndian :: read_u32 ( & exeinfo. size ) as usize ;
187276 off += 512 ;
188277
189278 match exeinfo. fname . iter ( ) . rposition ( |& x| x != 0 ) {
190279 Some ( zero_idx) => if exeinfo. fname [ ..=zero_idx] . is_ascii ( )
191280 {
192- // ASCII for 'icon'
193- let icon: [ u8 ; 4 ] = hex ! ( "69636f6e" ) ;
194- // ASCII for 'banner'
195- let banner: [ u8 ; 6 ] = hex ! ( "62616e6e6572" ) ;
196-
197281 if !( exeinfo. fname [ ..=zero_idx] == icon || exeinfo. fname [ ..=zero_idx] == banner) {
198282 exetmp. splice ( off..( off + size) , exetmp2[ off..off + size] . iter ( ) . cloned ( ) ) ;
199283 }
@@ -202,6 +286,7 @@ fn dump_section(ncch: &mut File, cia: &mut CiaReader, offset: u64, size: u32, se
202286 }
203287 }
204288 }
289+
205290 ncch. write_all ( & exetmp) . unwrap ( ) ;
206291 }
207292 NcchSection :: RomFS => {
@@ -247,7 +332,7 @@ fn get_new_key(key_y: u128, header: &NcchHdr, titleid: String) -> u128 {
247332 seeddb. read_exact ( & mut cbuffer) . unwrap ( ) ;
248333 let seed_count = LittleEndian :: read_u32 ( & cbuffer) ;
249334 seeddb. seek ( SeekFrom :: Current ( 12 ) ) . unwrap ( ) ;
250-
335+
251336 for _ in 0 ..seed_count {
252337 seeddb. read_exact ( & mut kbuffer) . unwrap ( ) ;
253338 kbuffer. reverse ( ) ;
@@ -271,7 +356,7 @@ fn get_new_key(key_y: u128, header: &NcchHdr, titleid: String) -> u128 {
271356 let bytes = req. bytes ( ) . unwrap ( ) ;
272357
273358 match bytes. try_into ( ) {
274- Ok ( bytes) => {
359+ Ok ( bytes) => {
275360 seeds. insert ( titleid. clone ( ) , bytes) ;
276361 debug ! ( "A seed has been found online in the region {}" , country) ;
277362 break ;
@@ -297,7 +382,7 @@ fn get_new_key(key_y: u128, header: &NcchHdr, titleid: String) -> u128 {
297382 new_key
298383}
299384
300- fn parse_ncsd ( cia : & mut CiaReader ) {
385+ fn parse_ncsd ( cia : & mut CiaReader , icons : bool ) {
301386 debug ! ( "Parsing NCSD in file: {}" , cia. name) ;
302387 cia. seek ( 0 ) ;
303388 let mut tmp: [ u8 ; 512 ] = [ 0u8 ; 512 ] ;
@@ -309,12 +394,12 @@ fn parse_ncsd(cia: &mut CiaReader) {
309394 cia. content_id = idx as u32 ;
310395 let mut tid: [ u8 ; 8 ] = header. titleid ;
311396 tid. reverse ( ) ;
312- parse_ncch ( cia, ( header. offset_sizetable [ idx] . offset * MEDIA_UNIT_SIZE ) . clone ( ) . into ( ) , tid) ;
397+ parse_ncch ( cia, ( header. offset_sizetable [ idx] . offset * MEDIA_UNIT_SIZE ) . clone ( ) . into ( ) , tid, icons ) ;
313398 }
314399 }
315400}
316401
317- fn parse_ncch ( cia : & mut CiaReader , offs : u64 , mut titleid : [ u8 ; 8 ] ) {
402+ fn parse_ncch ( cia : & mut CiaReader , offs : u64 , mut titleid : [ u8 ; 8 ] , icons : bool ) {
318403 if cia. from_ncsd {
319404 debug ! ( " Parsing {} NCCH" , NCSD_PARTITIONS [ cia. cidx as usize ] ) ;
320405 } else if cia. single_ncch {
@@ -379,13 +464,12 @@ fn parse_ncch(cia: &mut CiaReader, offs: u64, mut titleid: [u8; 8]) {
379464 base = file_name. strip_suffix ( ".cia" ) . unwrap ( ) . to_string ( ) ;
380465 }
381466
382- let absolute_path = canonicalize ( & path) . unwrap ( ) ;
383- let final_path = if cfg ! ( windows) && absolute_path. to_string_lossy ( ) . starts_with ( r"\\?\" ) {
384- Path :: new ( & absolute_path. to_string_lossy ( ) [ 4 ..] . replace ( "\\ " , "/" ) ) . to_path_buf ( )
385- } else {
386- absolute_path
387- } ;
388- let parent_dir = final_path. parent ( ) . unwrap ( ) ;
467+ let path = Path :: new ( & cia. name ) ;
468+ let parent_dir = path. canonicalize ( )
469+ . unwrap ( )
470+ . parent ( )
471+ . unwrap ( )
472+ . to_path_buf ( ) ;
389473
390474 base = format ! ( "{}/{}.{}.{:08X}.ncch" ,
391475 parent_dir. display( ) ,
@@ -401,23 +485,23 @@ fn parse_ncch(cia: &mut CiaReader, offs: u64, mut titleid: [u8; 8]) {
401485 let mut counter: [ u8 ; 16 ] ;
402486 if header. exhdrsize != 0 {
403487 counter = get_ncch_aes_counter ( & header, NcchSection :: ExHeader ) ;
404- dump_section ( & mut ncch, cia, 512 , header. exhdrsize * 2 , NcchSection :: ExHeader , 0 , counter, uses_extra_crypto, fixed_crypto, use_seed_crypto, encrypted, [ ncch_key_y, key_y] ) ;
488+ dump_section ( & mut ncch, cia, 512 , header. exhdrsize * 2 , NcchSection :: ExHeader , 0 , counter, uses_extra_crypto, fixed_crypto, use_seed_crypto, encrypted, [ ncch_key_y, key_y] , tid , icons ) ;
405489 }
406490
407491 if header. exefssize != 0 {
408492 counter = get_ncch_aes_counter ( & header, NcchSection :: ExeFS ) ;
409- dump_section ( & mut ncch, cia, ( header. exefsoffset * MEDIA_UNIT_SIZE ) as u64 , header. exefssize * MEDIA_UNIT_SIZE , NcchSection :: ExeFS , 1 , counter, uses_extra_crypto, fixed_crypto, use_seed_crypto, encrypted, [ ncch_key_y, key_y] ) ;
493+ dump_section ( & mut ncch, cia, ( header. exefsoffset * MEDIA_UNIT_SIZE ) as u64 , header. exefssize * MEDIA_UNIT_SIZE , NcchSection :: ExeFS , 1 , counter, uses_extra_crypto, fixed_crypto, use_seed_crypto, encrypted, [ ncch_key_y, key_y] , tid , icons ) ;
410494 }
411495
412496 if header. romfssize != 0 {
413497 counter = get_ncch_aes_counter ( & header, NcchSection :: RomFS ) ;
414- dump_section ( & mut ncch, cia, ( header. romfsoffset * MEDIA_UNIT_SIZE ) as u64 , header. romfssize * MEDIA_UNIT_SIZE , NcchSection :: RomFS , 2 , counter, uses_extra_crypto, fixed_crypto, use_seed_crypto, encrypted, [ ncch_key_y, key_y] ) ;
498+ dump_section ( & mut ncch, cia, ( header. romfsoffset * MEDIA_UNIT_SIZE ) as u64 , header. romfssize * MEDIA_UNIT_SIZE , NcchSection :: RomFS , 2 , counter, uses_extra_crypto, fixed_crypto, use_seed_crypto, encrypted, [ ncch_key_y, key_y] , tid , icons ) ;
415499 }
416-
500+
417501 info ! ( "{}" , base) ;
418502}
419503
420- fn parse_cia ( mut romfile : File , filename : String , partition : Option < u8 > ) {
504+ fn parse_cia ( mut romfile : File , filename : String , partition : Option < u8 > , icons : Option < bool > ) {
421505 romfile. seek ( SeekFrom :: Start ( 0 ) ) . unwrap ( ) ;
422506 let mut tmp: [ u8 ; 32 ] = [ 0 ; 32 ] ;
423507 romfile. read_exact ( & mut tmp) . unwrap ( ) ;
@@ -468,12 +552,12 @@ fn parse_cia(mut romfile: File, filename: String, partition: Option<u8>) {
468552 let cenc: bool = ( content. ctype & 1 ) != 0 ;
469553
470554 romfile. seek ( SeekFrom :: Start ( ( contentoffs + next_content_offs) as u64 ) ) . unwrap ( ) ;
471- let mut test: [ u8 ; 512 ] = [ 0 ; 512 ] ;
555+ let mut test: [ u8 ; 512 ] = [ 0 ; 512 ] ;
472556 romfile. read_exact ( & mut test) . unwrap ( ) ;
473557 let mut search: [ u8 ; 4 ] = test[ 256 ..260 ] . try_into ( ) . unwrap ( ) ;
474558
475559 let iv: [ u8 ; 16 ] = gen_iv ( content. cidx ) ;
476-
560+
477561 if cenc {
478562 cbc_decrypt ( & titkey, & iv, & mut test) ;
479563 search = test[ 256 ..260 ] . try_into ( ) . unwrap ( ) ;
@@ -490,7 +574,7 @@ fn parse_cia(mut romfile: File, filename: String, partition: Option<u8>) {
490574 Some ( number) => if ( i as u8 ) != number { continue ; } ,
491575 None => ( ) ,
492576 }
493- parse_ncch ( & mut cia_handle, 0 , tid[ 0 ..8 ] . try_into ( ) . unwrap ( ) ) ;
577+ parse_ncch ( & mut cia_handle, 0 , tid[ 0 ..8 ] . try_into ( ) . unwrap ( ) , icons . unwrap_or ( false ) ) ;
494578
495579 } else { debug ! ( "CIA content can't be parsed, skipping partition" ) }
496580 Err ( _) => debug ! ( "CIA content can't be parsed, skipping partition" )
@@ -502,10 +586,11 @@ fn main() {
502586 let args: Vec < String > = std:: env:: args ( ) . collect ( ) ;
503587
504588 let mut partition: Option < u8 > = None ;
589+ let mut icons: Option < bool > = None ;
505590 let mut verbose = true ;
506591
507592 if args. len ( ) < 2 {
508- println ! ( "Usage: ctrdecrypt <ROMFILE> [OPTIONS]\n Options:\n \t --ncch <partition-number>\n \t --no-verbose" ) ;
593+ println ! ( "Usage: ctrdecrypt <ROMFILE> [OPTIONS]\n Options:\n \t --ncch <partition-number>\n \t --no-verbose\n \t --extract-icons " ) ;
509594 return ;
510595 }
511596
@@ -531,6 +616,7 @@ fn main() {
531616 i += 1 ; // Partition number already checked
532617 }
533618 "--no-verbose" => verbose = false ,
619+ "--extract-icons" => icons = Some ( true ) ,
534620 _ => {
535621 println ! ( "Invalid argument: {}" , args[ i] ) ;
536622 return ;
@@ -553,11 +639,11 @@ fn main() {
553639 Ok ( ptype) => {
554640 if ptype == "NCSD" {
555641 let mut reader = CiaReader :: new ( rom. try_clone ( ) . unwrap ( ) , false , args[ 1 ] . to_string ( ) , [ 0u8 ; 16 ] , 0 , 0 , 0 , false , true ) ;
556- parse_ncsd ( & mut reader) ;
642+ parse_ncsd ( & mut reader, icons . unwrap_or ( false ) ) ;
557643 return ;
558644 } else if ptype == "NCCH" {
559645 let mut reader = CiaReader :: new ( rom. try_clone ( ) . unwrap ( ) , false , args[ 1 ] . to_string ( ) , [ 0u8 ; 16 ] , 0 , 0 , 0 , true , false ) ;
560- parse_ncch ( & mut reader, 0 , [ 0u8 ; 8 ] ) ;
646+ parse_ncch ( & mut reader, 0 , [ 0u8 ; 8 ] , icons . unwrap_or ( false ) ) ;
561647 return ;
562648 }
563649 }
@@ -568,6 +654,6 @@ fn main() {
568654 let mut check: [ u8 ; 4 ] = [ 0 ; 4 ] ;
569655 rom. read_exact ( & mut check) . unwrap ( ) ;
570656
571- if check[ 2 ..4 ] == [ 0 , 0 ] { parse_cia ( rom, args[ 1 ] . to_string ( ) , partition) }
657+ if check[ 2 ..4 ] == [ 0 , 0 ] { parse_cia ( rom, args[ 1 ] . to_string ( ) , partition, icons ) }
572658 }
573659}
0 commit comments