Skip to content

Commit c91d3a5

Browse files
Alessio IncampoAlessio Incampo
authored andcommitted
added extract-icons argument
1 parent 1763c5e commit c91d3a5

File tree

3 files changed

+156
-43
lines changed

3 files changed

+156
-43
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ block-padding = "0.3.3"
1616
ctr = "0.9.2"
1717
byteorder = "1.5.0"
1818
log = "0.4.26"
19-
env_logger = "0.11.6"
19+
env_logger = "0.11.6"
20+
image = "0.25.9"

src/ctrutils.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,37 @@ impl CiaReader {
144144
let last_enc_block = BigEndian::read_u128(&data[(data.len() - 16)..]);
145145
cbc_decrypt(&self.key, &self.iv, data);
146146
let first_dec_block = BigEndian::read_u128(&data[0..16]);
147-
147+
148148
// XOR the last encrypted block with the first decrypted block
149149
BigEndian::write_u128(&mut data[0..16], first_dec_block ^ self.last_enc_block);
150150

151151
self.last_enc_block = last_enc_block;
152152
}
153153
}
154154
}
155+
156+
// Morton encoding
157+
fn part_1_by_1(mut x: u32) -> u32 {
158+
x &= 0x0000_FFFF;
159+
x = (x | (x << 8)) & 0x00FF_00FF;
160+
x = (x | (x << 4)) & 0x0F0F_0F0F;
161+
x = (x | (x << 2)) & 0x3333_3333;
162+
x = (x | (x << 1)) & 0x5555_5555;
163+
x
164+
}
165+
166+
pub fn morton_encode_2d(x: u32, y: u32) -> u32 {
167+
part_1_by_1(x) | (part_1_by_1(y) << 1)
168+
}
169+
170+
pub fn rgb565_to_rgb888(rgb565: u16) -> (u8, u8, u8) {
171+
let r = ((rgb565 >> 11) & 0x1F) as u8;
172+
let g = ((rgb565 >> 5) & 0x3F) as u8;
173+
let b = (rgb565 & 0x1F) as u8;
174+
175+
(
176+
(r << 3) | (r >> 2),
177+
(g << 2) | (g >> 4),
178+
(b << 3) | (b >> 2),
179+
)
180+
}

src/main.rs

Lines changed: 127 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{collections::HashMap, fs::canonicalize, fs::File, io::{Cursor, Read, S
88

99
use hex_literal::hex;
1010
use log::{debug, info, LevelFilter};
11+
use image::{ImageBuffer, Rgb};
1112

1213
const 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+
6392
fn 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]\nOptions:\n\t--ncch <partition-number>\n\t--no-verbose");
593+
println!("Usage: ctrdecrypt <ROMFILE> [OPTIONS]\nOptions:\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

Comments
 (0)