use std::io::{self, Read, Seek, SeekFrom};
use crate::crypto::{ctr, xtsn};
use crate::error::{Error, Result};
use crate::format::ncz::Section;
use crate::format::read_vec;
use crate::keys::Keys;
pub const MEDIA_SIZE: u64 = 0x200;
pub const HEADER_ENCRYPTED_SIZE: usize = 0xC00;
pub const CONTENT_PROGRAM: u8 = 0;
pub const CONTENT_META: u8 = 1;
pub const CONTENT_PUBLIC_DATA: u8 = 5;
pub fn is_ctr(crypto_type: i64) -> bool {
crypto_type == 3 || crypto_type == 4
}
pub fn fs_header_offset(i: usize) -> usize {
0x400 + i * 0x200
}
pub fn section_counter(decrypted_header: &[u8], i: usize) -> [u8; 16] {
let fs_hdr = fs_header_offset(i);
let mut counter = [0u8; 16];
for (c, b) in counter[..8].iter_mut().zip(
decrypted_header[fs_hdr + 0x140..fs_hdr + 0x148]
.iter()
.rev(),
) {
*c = *b;
}
counter
}
#[derive(Debug, Clone, Copy)]
pub struct SectionTableEntry {
pub media_offset: u32,
pub media_end_offset: u32,
pub offset: u64,
pub end_offset: u64,
}
#[derive(Debug, Clone)]
pub struct NcaHeader {
pub magic: [u8; 4],
pub is_game_card: i8,
pub content_type: u8,
pub crypto_type: i8,
pub key_index: i8,
pub size: u64,
pub title_id: [u8; 8],
pub content_index: u32,
pub sdk_version: u32,
pub crypto_type2: i8,
pub rights_id: [u8; 16],
pub section_tables: [SectionTableEntry; 4],
pub master_key: i32,
pub key_block: [u8; 64],
pub title_key_dec: [u8; 16],
}
pub fn decrypt_header_bytes(header_key: &[u8; 32], buf: &mut [u8]) {
assert!(buf.len() >= HEADER_ENCRYPTED_SIZE);
xtsn::crypt_with_32byte_key(
header_key,
0x200,
0,
&mut buf[..HEADER_ENCRYPTED_SIZE],
true,
);
}
impl NcaHeader {
pub fn parse(
decrypted: &[u8],
keys: &Keys,
rights_title_key: Option<&[u8; 16]>,
) -> Result<NcaHeader> {
if decrypted.len() < HEADER_ENCRYPTED_SIZE {
return Err(Error::Corrupt("NCA header too short".into()));
}
let magic: [u8; 4] = decrypted[0x200..0x204].try_into().unwrap();
if magic != *b"NCA3" && magic != *b"NCA2" {
return Err(Error::BadMagic {
expected: "NCA3/NCA2".into(),
found: hex::encode(magic),
});
}
let is_game_card = decrypted[0x204] as i8;
let content_type = decrypted[0x205];
let crypto_type = decrypted[0x206] as i8;
let key_index = decrypted[0x207] as i8;
let size = u64::from_le_bytes(decrypted[0x208..0x210].try_into().unwrap());
let mut title_id = [0u8; 8];
title_id.copy_from_slice(&decrypted[0x210..0x218]);
let content_index = u32::from_le_bytes(decrypted[0x218..0x21C].try_into().unwrap());
let sdk_version = u32::from_le_bytes(decrypted[0x21C..0x220].try_into().unwrap());
let crypto_type2 = decrypted[0x220] as i8;
let mut rights_id = [0u8; 16];
rights_id.copy_from_slice(&decrypted[0x230..0x240]);
let mut section_tables = [SectionTableEntry {
media_offset: 0,
media_end_offset: 0,
offset: 0,
end_offset: 0,
}; 4];
for (i, st) in section_tables.iter_mut().enumerate() {
let base = 0x240 + i * 0x10;
let media_offset = u32::from_le_bytes(decrypted[base..base + 4].try_into().unwrap());
let media_end_offset =
u32::from_le_bytes(decrypted[base + 4..base + 8].try_into().unwrap());
*st = SectionTableEntry {
media_offset,
media_end_offset,
offset: media_offset as u64 * MEDIA_SIZE,
end_offset: media_end_offset as u64 * MEDIA_SIZE,
};
}
let mut master_key = (crypto_type.max(crypto_type2) as i32) - 1;
if master_key < 0 {
master_key = 0;
}
let mut enc_key_block = [0u8; 64];
enc_key_block.copy_from_slice(&decrypted[0x300..0x340]);
let mut key_block = [0u8; 64];
for i in 0..4 {
let off = i * 0x10;
let mut w = [0u8; 16];
w.copy_from_slice(&enc_key_block[off..off + 16]);
let k = keys.unwrap_title_key(&w, master_key as usize)?;
key_block[off..off + 16].copy_from_slice(&k);
}
let title_key_dec = if has_title_rights(&rights_id) {
match rights_title_key {
Some(wrapped) => keys.decrypt_title_key(wrapped, master_key as usize)?,
None => {
return Err(Error::MissingKey(format!(
"title key for rights-managed NCA rightsId={}",
hex::encode(rights_id).to_uppercase()
)));
}
}
} else {
let mut k = [0u8; 16];
k.copy_from_slice(&key_block[32..48]);
k
};
Ok(NcaHeader {
magic,
is_game_card,
content_type,
crypto_type,
key_index,
size,
title_id,
content_index,
sdk_version,
crypto_type2,
rights_id,
section_tables,
master_key,
key_block,
title_key_dec,
})
}
pub fn has_title_rights(&self) -> bool {
has_title_rights(&self.rights_id)
}
pub fn encryption_sections<R: Read + Seek>(
&self,
decrypted_header: &[u8],
nca: &mut R,
) -> Result<Vec<Section>> {
let mut sections = Vec::new();
for (i, st) in self.section_tables.iter().enumerate() {
let fs_hdr = fs_header_offset(i);
if st.end_offset <= st.offset || decrypted_header[fs_hdr + 0x3] == 0 {
continue;
}
let section = Section {
offset: st.offset as i64,
size: (st.end_offset - st.offset) as i64,
crypto_type: decrypted_header[fs_hdr + 0x4] as i64,
crypto_key: self.title_key_dec,
crypto_counter: section_counter(decrypted_header, i),
};
match bktr_subsections(§ion, &decrypted_header[fs_hdr..fs_hdr + 0x200], nca)? {
Some(subs) => sections.extend(subs),
None => sections.push(section),
}
}
Ok(sections)
}
}
fn bktr_subsections<R: Read + Seek>(
section: &Section,
fs_hdr: &[u8],
nca: &mut R,
) -> Result<Option<Vec<Section>>> {
const NODE: usize = 0x4000;
let table_offset = u64::from_le_bytes(fs_hdr[0x120..0x128].try_into().unwrap());
let table_size = u64::from_le_bytes(fs_hdr[0x128..0x130].try_into().unwrap());
let section_size = section.size as u64;
if table_size == 0 || !is_ctr(section.crypto_type) || table_offset >= section_size {
return Ok(None);
}
let table_len = table_size
.min(section_size - table_offset)
.min(NODE as u64 * 0x7FF);
let abs = section.offset as u64 + table_offset;
nca.seek(SeekFrom::Start(abs))?;
let mut table = match read_vec(nca, table_len) {
Ok(t) => t,
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e.into()),
};
ctr::keystream_xor(
§ion.crypto_key,
§ion.crypto_counter,
abs,
&mut table,
);
if table.len() < NODE {
return Ok(None);
}
let bucket_count = u32::from_le_bytes(table[4..8].try_into().unwrap()) as usize;
let mut subs: Vec<Section> = Vec::new();
let mut expected = 0u64; for b in 0..bucket_count {
let Some(bucket) = table.get(NODE * (b + 1)..NODE * (b + 2)) else {
return Ok(None);
};
let entry_count = u32::from_le_bytes(bucket[4..8].try_into().unwrap()) as usize;
let end_offset = u64::from_le_bytes(bucket[8..16].try_into().unwrap());
if entry_count > (NODE - 0x10) / 0x10 {
return Ok(None);
}
for e in 0..entry_count {
let entry = &bucket[0x10 + e * 0x10..0x20 + e * 0x10];
let virtual_offset = u64::from_le_bytes(entry[0..8].try_into().unwrap());
let next = if e + 1 < entry_count {
u64::from_le_bytes(bucket[0x20 + e * 0x10..0x28 + e * 0x10].try_into().unwrap())
} else {
end_offset
};
if virtual_offset != expected || next <= virtual_offset || next > section_size {
return Ok(None);
}
let ctr_val = u32::from_le_bytes(entry[12..16].try_into().unwrap());
let mut counter = section.crypto_counter;
counter[4..8].copy_from_slice(&ctr_val.to_be_bytes());
subs.push(Section {
offset: section.offset + virtual_offset as i64,
size: (next - virtual_offset) as i64,
crypto_counter: counter,
..section.clone()
});
expected = next;
}
}
if subs.is_empty() {
return Ok(None);
}
if expected < section_size {
subs.push(Section {
offset: section.offset + expected as i64,
size: (section_size - expected) as i64,
..section.clone()
});
}
Ok(Some(subs))
}
fn has_title_rights(rights_id: &[u8; 16]) -> bool {
rights_id.iter().any(|&b| b != 0)
}