Skip to content

Commit 0bc7b1c

Browse files
committed
support for .3ds files and additional optimizations
1 parent 9a5ca5b commit 0bc7b1c

File tree

3 files changed

+108
-27
lines changed

3 files changed

+108
-27
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# ctrdecrypt
2-
Decrypt module for cia-unix
2+
Decrypt module for [cia-unix](https://github.com/shijimasoft/cia-unix)
33

4-
> **Warning**
5-
> This module is under development, please do not use it yet.
4+
### Usage
5+
| System | Command |
6+
|-----------------|----------------------------|
7+
| Linux and macOS | `./ctrdecrypt <ROMFILE>` |
8+
| Windows | `ctrdecrypt.exe <ROMFILE>` |
9+
10+
where `<ROMFILE>` is the full name of encrypted ROM (CIA or 3DS) file
11+
12+
> [!IMPORTANT]
13+
> It's recommended to download the latest available artifacts from the last commit to have the most recent corrections

src/ctrutils.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ pub struct NcchHdr {
5454
romfshash: [u8; 32]
5555
}
5656

57+
#[repr(C)]
58+
pub struct NcchOffsetSize {
59+
pub offset: u32,
60+
size: u32,
61+
}
62+
63+
#[repr(C)]
64+
pub struct NcsdHdr {
65+
signature: [u8; 256],
66+
magic: [u8; 4],
67+
mediasize: u32,
68+
pub titleid: [u8; 8],
69+
padding0: [u8; 16],
70+
pub offset_sizetable: [NcchOffsetSize; 8],
71+
padding1: [u8; 40],
72+
flags: [u8; 8],
73+
ncchidtable: [u8; 64],
74+
padding2: [u8; 48]
75+
}
76+
5777
#[repr(C)]
5878
pub struct CiaFile {
5979
pub headersize: u32,
@@ -82,11 +102,13 @@ pub struct CiaReader {
82102
pub cidx: u16,
83103
iv: [u8; 16],
84104
contentoff: u64,
105+
pub single_ncch: bool,
106+
pub from_ncsd: bool,
85107
last_enc_block: u128,
86108
}
87109

88110
impl CiaReader {
89-
pub fn new(fhandle: File, encrypted: bool, name: String, key: [u8; 16], cidx: u16, contentoff: u64) -> CiaReader {
111+
pub fn new(fhandle: File, encrypted: bool, name: String, key: [u8; 16], cidx: u16, contentoff: u64, single_ncch: bool, from_ncsd: bool) -> CiaReader {
90112
CiaReader {
91113
fhandle,
92114
encrypted,
@@ -95,12 +117,16 @@ impl CiaReader {
95117
cidx,
96118
iv: gen_iv(cidx),
97119
contentoff,
120+
single_ncch,
121+
from_ncsd,
98122
last_enc_block: 0
99123
}
100124
}
101125

102126
pub fn seek(&mut self, offs: u64) {
103-
if offs == 0 {
127+
if self.single_ncch || self.from_ncsd {
128+
self.fhandle.seek(SeekFrom::Start(offs)).unwrap();
129+
} else if offs == 0 {
104130
self.fhandle.seek(SeekFrom::Start(self.contentoff)).unwrap();
105131
self.iv = gen_iv(self.cidx);
106132
} else {
@@ -111,8 +137,8 @@ impl CiaReader {
111137

112138
pub fn read(&mut self, data: &mut [u8]) {
113139
self.fhandle.read_exact(data).unwrap();
140+
114141
if self.encrypted {
115-
116142
let last_enc_block = BigEndian::read_u128(&data[(data.len() - 16)..]);
117143
cbc_decrypt(&self.key, &self.iv, data);
118144
let first_dec_block = BigEndian::read_u128(&data[0..16]);

src/main.rs

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
mod ctrutils;
2-
use ctrutils::{cbc_decrypt, gen_iv, CiaContent, CiaFile, CiaReader, NcchHdr};
2+
use ctrutils::{cbc_decrypt, gen_iv, CiaContent, CiaFile, CiaReader, NcchHdr, NcsdHdr};
33

44
use byteorder::{ByteOrder, BigEndian, LittleEndian, ReadBytesExt};
55
use aes::{cipher::{KeyIvInit, StreamCipher}, Aes128};
66

7-
use std::{collections::HashMap, env, fs::File, io::{Read, Seek, SeekFrom, Write, Cursor}, path::Path, usize, vec};
7+
use std::{collections::HashMap, env, fs::File, io::{Cursor, Read, Seek, SeekFrom, Write}, path::Path, usize, vec};
88

99
use hex_literal::hex;
1010

@@ -211,10 +211,9 @@ fn dump_section(ncch: &mut File, cia: &mut CiaReader, offset: u64, size: u32, se
211211
let mut sizeleft = size;
212212
let mut buf = vec![0u8; CHUNK as usize];
213213
let mut ctr_cipher = Aes128Ctr::new_from_slices(&key, &ctr).unwrap();
214-
215214
while sizeleft > CHUNK {
216215
cia.read(&mut buf);
217-
if cia.cidx != 0 { buf[1] = buf[1] ^ cia.cidx as u8 }
216+
if cia.cidx > 0 && !(cia.single_ncch || cia.from_ncsd) { buf[1] = buf[1] ^ cia.cidx as u8 }
218217
ctr_cipher.apply_keystream(&mut buf);
219218
ncch.write_all(&buf).unwrap();
220219
sizeleft -= CHUNK;
@@ -223,7 +222,7 @@ fn dump_section(ncch: &mut File, cia: &mut CiaReader, offset: u64, size: u32, se
223222
if sizeleft > 0 {
224223
buf = vec![0u8; sizeleft as usize];
225224
cia.read(&mut buf);
226-
if cia.cidx != 0 { buf[1] = buf[1] ^ cia.cidx as u8 }
225+
if cia.cidx > 0 && !(cia.single_ncch || cia.from_ncsd) { buf[1] = buf[1] ^ cia.cidx as u8 }
227226
ctr_cipher.apply_keystream(&mut buf);
228227
ncch.write_all(&buf).unwrap();
229228
}
@@ -262,9 +261,6 @@ fn get_new_key(key_y: u128, header: &NcchHdr, titleid: String) -> u128 {
262261

263262
// Check into Nintendo's servers
264263
if !seeds.contains_key(&titleid) {
265-
println!("\t********************************");
266-
println!("\tCouldn't find seed in seeddb, checking online...");
267-
println!("\t********************************");
268264
for country in ["JP", "US", "GB", "KR", "TW", "AU", "NZ"] {
269265
let req = attohttpc::get(format!("https://kagiya-ctr.cdn.nintendo.net/title/0x{}/ext_key?country={}", titleid, country))
270266
.danger_accept_invalid_certs(true)
@@ -273,9 +269,13 @@ fn get_new_key(key_y: u128, header: &NcchHdr, titleid: String) -> u128 {
273269
if req.is_success() {
274270
let bytes = req.bytes().unwrap();
275271

276-
if bytes.len() == 16 {
277-
seeds.insert(titleid.clone(), bytes.try_into().unwrap());
278-
break;
272+
match bytes.try_into() {
273+
Ok(bytes) => {
274+
seeds.insert(titleid.clone(), bytes);
275+
println!("A seed has been found online in the region {}", country);
276+
break;
277+
}
278+
Err(_) => ()
279279
}
280280
}
281281
}
@@ -296,9 +296,31 @@ fn get_new_key(key_y: u128, header: &NcchHdr, titleid: String) -> u128 {
296296
new_key
297297
}
298298

299-
fn parse_ncch(mut cia: CiaReader, mut titleid: [u8; 8], from_ncsd: bool) {
300-
println!("Parsing NCCH: {}", cia.cidx);
299+
fn parse_ncsd(cia: &mut CiaReader) {
300+
println!("Parsing NCSD in file: {}", cia.name);
301301
cia.seek(0);
302+
let mut tmp: [u8; 512] = [0u8; 512];
303+
cia.read(&mut tmp);
304+
let mut header: NcsdHdr = unsafe { std::mem::transmute(tmp) };
305+
for idx in 0..header.offset_sizetable.len() {
306+
if header.offset_sizetable[idx].offset != 0 {
307+
cia.cidx = idx as u16;
308+
header.titleid.reverse();
309+
parse_ncch(cia, (header.offset_sizetable[idx].offset * MEDIA_UNIT_SIZE).clone().into(), header.titleid);
310+
}
311+
}
312+
}
313+
314+
fn parse_ncch(cia: &mut CiaReader, offs: u64, mut titleid: [u8; 8]) {
315+
if cia.from_ncsd {
316+
println!(" Parsing {} NCCH", NCSD_PARTITIONS[cia.cidx as usize]);
317+
} else if cia.single_ncch {
318+
println!(" Parsing NCCH in file: {}", cia.name);
319+
} else {
320+
println!("Parsing NCCH: {}", cia.cidx)
321+
}
322+
323+
cia.seek(offs);
302324
let mut tmp = [0u8; 512];
303325
cia.read(&mut tmp);
304326
let mut header: NcchHdr = unsafe { std::mem::transmute(tmp) };
@@ -342,11 +364,18 @@ fn parse_ncch(mut cia: CiaReader, mut titleid: [u8; 8], from_ncsd: bool) {
342364
key_y = get_new_key(ncch_key_y, &header, hex::encode(titleid));
343365
println!("Uses 9.6 NCCH Seed crypto with KeyY: {:032X}", key_y);
344366
}
345-
let mut base: String = cia.name.strip_suffix(".cia").unwrap().to_string();
367+
368+
let mut base: String;
369+
if cia.single_ncch || cia.from_ncsd {
370+
base = cia.name.strip_suffix(".3ds").unwrap().to_string();
371+
} else {
372+
base = cia.name.strip_suffix(".cia").unwrap().to_string();
373+
}
374+
346375
base = format!("{}/{}.{}.ncch",
347376
env::current_dir().unwrap().to_str().unwrap(),
348377
base,
349-
if from_ncsd { NCSD_PARTITIONS[cia.cidx as usize].to_string() } else { cia.cidx.to_string() }
378+
if cia.from_ncsd { NCSD_PARTITIONS[cia.cidx as usize].to_string() } else { cia.cidx.to_string() }
350379
);
351380

352381
let mut ncch: File = File::create(base).unwrap();
@@ -356,17 +385,17 @@ fn parse_ncch(mut cia: CiaReader, mut titleid: [u8; 8], from_ncsd: bool) {
356385
let mut counter: [u8; 16];
357386
if header.exhdrsize != 0 {
358387
counter = get_ncch_aes_counter(&header, NcchSection::ExHeader);
359-
dump_section(&mut ncch, &mut cia, 512, header.exhdrsize * 2, NcchSection::ExHeader, 0, counter, uses_extra_crypto, fixed_crypto, encrypted, [ncch_key_y, key_y]);
388+
dump_section(&mut ncch, cia, 512, header.exhdrsize * 2, NcchSection::ExHeader, 0, counter, uses_extra_crypto, fixed_crypto, encrypted, [ncch_key_y, key_y]);
360389
}
361390

362391
if header.exefssize != 0 {
363392
counter = get_ncch_aes_counter(&header, NcchSection::ExeFS);
364-
dump_section(&mut ncch, &mut cia, (header.exefsoffset * MEDIA_UNIT_SIZE) as u64, header.exefssize * MEDIA_UNIT_SIZE, NcchSection::ExeFS, 1, counter, uses_extra_crypto, fixed_crypto, encrypted, [ncch_key_y, key_y]);
393+
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, encrypted, [ncch_key_y, key_y]);
365394
}
366395

367396
if header.romfssize != 0 {
368397
counter = get_ncch_aes_counter(&header, NcchSection::RomFS);
369-
dump_section(&mut ncch, &mut cia, (header.romfsoffset * MEDIA_UNIT_SIZE) as u64, header.romfssize * MEDIA_UNIT_SIZE, NcchSection::RomFS, 2, counter, uses_extra_crypto, fixed_crypto, encrypted, [ncch_key_y, key_y]);
398+
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, encrypted, [ncch_key_y, key_y]);
370399
}
371400
}
372401

@@ -436,9 +465,9 @@ fn parse_cia(mut romfile: File, filename: String) {
436465
Ok(utf8) => if utf8 == "NCCH"
437466
{
438467
romfile.seek(SeekFrom::Start(contentoffs + next_content_offs)).unwrap();
439-
let cia_handle = CiaReader::new(romfile.try_clone().unwrap(), cenc, filename.clone(), titkey, content.cidx, contentoffs + next_content_offs);
468+
let mut cia_handle = CiaReader::new(romfile.try_clone().unwrap(), cenc, filename.clone(), titkey, content.cidx, contentoffs + next_content_offs, false, false);
440469
next_content_offs += align(content.csize, 64);
441-
parse_ncch(cia_handle, tid[0..8].try_into().unwrap(), false);
470+
parse_ncch(&mut cia_handle, 0, tid[0..8].try_into().unwrap());
442471
} else { println!("CIA content can't be parsed, skipping partition") }
443472
Err(_) => println!("CIA content can't be parsed, skipping partition")
444473
}
@@ -456,7 +485,25 @@ fn main() {
456485
}
457486

458487
let mut rom = File::open(&args[1]).unwrap();
459-
488+
rom.seek(SeekFrom::Start(256)).unwrap();
489+
let mut magic: [u8; 4] = [0u8; 4];
490+
rom.read_exact(&mut magic).unwrap();
491+
492+
match std::str::from_utf8(&magic) {
493+
Ok(ptype) => {
494+
if ptype == "NCSD" {
495+
let mut reader = CiaReader::new(rom.try_clone().unwrap(), false, args[1].to_string(), [0u8; 16], 0, 0, false, true);
496+
parse_ncsd(&mut reader);
497+
return;
498+
} else if ptype == "NCCH" {
499+
let mut reader = CiaReader::new(rom.try_clone().unwrap(), false, args[1].to_string(), [0u8; 16], 0, 0, true, false);
500+
parse_ncch(&mut reader, 0, [0u8; 8]);
501+
return;
502+
}
503+
}
504+
Err(_) => ()
505+
}
506+
460507
if args[1].ends_with(".cia") {
461508
let mut check: [u8; 4] = [0; 4];
462509
rom.read_exact(&mut check).unwrap();

0 commit comments

Comments
 (0)