mirror of
https://github.com/BillyOutlast/oboromi.git
synced 2026-07-01 19:54:43 -04:00
gsd snapshot: uncommitted changes after 106m inactivity
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
//! **Constraints:** No real firmware dump — tests use inline fixtures.
|
||||
//! Key material is never logged as hex values.
|
||||
|
||||
use super::aes::{Aes128Key, aes_encrypt_block, aes_decrypt_block};
|
||||
use super::aes::{Aes128Key, aes_encrypt_block, aes_decrypt_block, aes_xts_decrypt};
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
|
||||
@@ -446,6 +446,49 @@ pub fn decrypt_nca_key_area(header: &NcaHeader, device_key: &[u8; 16], key_index
|
||||
aes_decrypt_block(&dk, &header.key_area[key_index])
|
||||
}
|
||||
|
||||
// ── NCA header XTS decryption ─────────────────────────────────────
|
||||
|
||||
/// Sector size for XTS header decryption (0x200 bytes per sector).
|
||||
const XTS_SECTOR_SIZE: usize = 0x200;
|
||||
|
||||
/// Number of sectors in a full NCA header (0xC00 / 0x200 = 6).
|
||||
const XTS_SECTOR_COUNT: usize = NCA_FULL_HEADER_SIZE / XTS_SECTOR_SIZE;
|
||||
|
||||
/// Decrypt an XTS-encrypted NCA header and parse it.
|
||||
///
|
||||
/// NCA headers (crypto_type = 1) are encrypted with AES-XTS-128 using
|
||||
/// a non-standard endianness-reversed tweak. The 0xC00-byte encrypted
|
||||
/// header is divided into six 0x200-byte sectors, each decrypted independently
|
||||
/// with the sector number (0–5) as the XTS tweak/IV.
|
||||
///
|
||||
/// `header_key` is a 32-byte slice: first 16 bytes = XTS key1 (tweak key),
|
||||
/// second 16 bytes = XTS key2 (ciphertext key).
|
||||
///
|
||||
/// Returns the parsed `NcaHeader`, or `NcaError` on decrypt/parse failure.
|
||||
pub fn decrypt_nca_header(
|
||||
encrypted_header: &[u8; NCA_FULL_HEADER_SIZE],
|
||||
header_key: &[u8; 32],
|
||||
) -> Result<NcaHeader, NcaError> {
|
||||
let key1 = Aes128Key::from_bytes(&header_key[0..16].try_into().unwrap());
|
||||
let key2 = Aes128Key::from_bytes(&header_key[16..32].try_into().unwrap());
|
||||
|
||||
let mut decrypted = Vec::with_capacity(NCA_FULL_HEADER_SIZE);
|
||||
|
||||
for sector in 0..XTS_SECTOR_COUNT {
|
||||
let offset = sector * XTS_SECTOR_SIZE;
|
||||
let sector_data = &encrypted_header[offset..offset + XTS_SECTOR_SIZE];
|
||||
|
||||
// Build IV: lower 8 bytes = sector number (LE), upper 8 bytes = 0
|
||||
let mut iv = [0u8; 16];
|
||||
iv[0..8].copy_from_slice(&(sector as u64).to_le_bytes());
|
||||
|
||||
let pt = aes_xts_decrypt(&key1, &key2, &iv, sector_data);
|
||||
decrypted.extend_from_slice(&pt);
|
||||
}
|
||||
|
||||
parse_nca_header(&decrypted)
|
||||
}
|
||||
|
||||
// ── AES-CTR section decryption ────────────────────────────────────
|
||||
|
||||
/// Decrypt an NCA section using AES-128-CTR mode.
|
||||
@@ -505,7 +548,7 @@ pub fn decrypt_nca_section(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::aes::{aes_encrypt_block, aes_decrypt_block, Aes128Key};
|
||||
use super::super::aes::{aes_encrypt_block, aes_decrypt_block, aes_xts_encrypt, Aes128Key};
|
||||
|
||||
const KNOWN_TITLE_KEY: [u8; 16] = [
|
||||
0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18,
|
||||
@@ -922,4 +965,192 @@ mod tests {
|
||||
assert!(debug.contains("100"));
|
||||
assert!(debug.contains("50"));
|
||||
}
|
||||
|
||||
// ── XTS header decryption tests ────────────────────────────────
|
||||
//
|
||||
// Build a known NCA header, XTS-encrypt it sector-by-sector,
|
||||
// then call decrypt_nca_header() to decrypt-and-parse in one step.
|
||||
|
||||
/// 32-byte test header key: first 16 = key1 (tweak), second 16 = key2 (cipher).
|
||||
const XTS_HEADER_KEY: [u8; 32] = [
|
||||
0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
|
||||
0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
|
||||
0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
|
||||
0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
|
||||
];
|
||||
|
||||
/// Build a known-plaintext NCA header (0xC00 bytes) with recognizable field values.
|
||||
fn build_plaintext_header() -> Vec<u8> {
|
||||
let mut hdr = vec![0u8; NCA_FULL_HEADER_SIZE];
|
||||
|
||||
// 0x000–0x0FF: RSA-2048 signature (all zeros in test)
|
||||
// 0x100–0x1FF: NCA header block
|
||||
hdr[0x100] = b'N';
|
||||
hdr[0x101] = b'C';
|
||||
hdr[0x102] = b'A';
|
||||
hdr[0x103] = b'3';
|
||||
hdr[0x104] = 0x00; // distribution_type = System
|
||||
hdr[0x105] = 0x01; // content_type = Meta
|
||||
hdr[0x106] = 0x03; // key_generation = 4.0.0+
|
||||
hdr[0x107] = 0x01; // key_area_encryption_key_index = 1 (ocean)
|
||||
// content_size = 0x100000 bytes (u64 LE)
|
||||
hdr[0x108..0x110].copy_from_slice(&0x0010_0000u64.to_le_bytes());
|
||||
// title_id = 0x0100ABCD0000EF00 (u64 LE)
|
||||
hdr[0x110..0x118].copy_from_slice(&0x0100_ABCD_0000_EF00u64.to_le_bytes());
|
||||
// sdk_version = 0x000B0000 (u32 LE)
|
||||
hdr[0x118..0x11C].copy_from_slice(&0x000B_0000u32.to_le_bytes());
|
||||
hdr[0x11C] = 0x01; // crypto_type = 1 (XTS header encryption)
|
||||
hdr[0x11D] = 0x03; // format_version = 3 (NCA3)
|
||||
|
||||
// Key area (0x200–0x3FF): leave zeroed (not relevant for header decrypt test)
|
||||
// FsEntry table at 0x240: section 0 has data
|
||||
let media_sectors: u32 = 0x20; // 0x4000 bytes
|
||||
hdr[0x240..0x244].copy_from_slice(&0u32.to_le_bytes()); // start
|
||||
hdr[0x244..0x248].copy_from_slice(&media_sectors.to_le_bytes()); // end
|
||||
|
||||
// FsHeader at 0x400 (section 0)
|
||||
hdr[0x400] = 0x02; // version = 2
|
||||
hdr[0x401] = 0x01; // fs_type = PartitionFS
|
||||
hdr[0x402] = 0x02; // hash_type = PFS0
|
||||
hdr[0x403] = 0x02; // encryption_type = CTR
|
||||
hdr[0x408..0x410].copy_from_slice(&0x10000u64.to_le_bytes()); // sb offset
|
||||
hdr[0x410..0x418].copy_from_slice(&0x8000u64.to_le_bytes()); // sb size
|
||||
|
||||
hdr
|
||||
}
|
||||
|
||||
/// XTS-encrypt a 0xC00-byte header sector-by-sector.
|
||||
fn xts_encrypt_header(plaintext: &[u8; NCA_FULL_HEADER_SIZE], key: &[u8; 32]) -> Vec<u8> {
|
||||
let key1 = Aes128Key::from_bytes(&key[0..16].try_into().unwrap());
|
||||
let key2 = Aes128Key::from_bytes(&key[16..32].try_into().unwrap());
|
||||
let mut ct = Vec::with_capacity(NCA_FULL_HEADER_SIZE);
|
||||
for sector in 0..6 {
|
||||
let offset = sector * XTS_SECTOR_SIZE;
|
||||
let sector_data = &plaintext[offset..offset + XTS_SECTOR_SIZE];
|
||||
let mut iv = [0u8; 16];
|
||||
iv[0..8].copy_from_slice(&(sector as u64).to_le_bytes());
|
||||
let enc = aes_xts_encrypt(&key1, &key2, &iv, sector_data);
|
||||
ct.extend_from_slice(&enc);
|
||||
}
|
||||
ct
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_nca_header_roundtrip() {
|
||||
let pt = build_plaintext_header();
|
||||
let pt_array: [u8; NCA_FULL_HEADER_SIZE] = pt.as_slice().try_into().unwrap();
|
||||
let encrypted_array: [u8; NCA_FULL_HEADER_SIZE] =
|
||||
xts_encrypt_header(&pt_array, &XTS_HEADER_KEY).as_slice().try_into().unwrap();
|
||||
|
||||
let hdr = decrypt_nca_header(&encrypted_array, &XTS_HEADER_KEY).unwrap();
|
||||
|
||||
assert_eq!(hdr.magic, NCA3_MAGIC);
|
||||
assert_eq!(hdr.distribution_type, 0x00);
|
||||
assert_eq!(hdr.content_type, 0x01); // Meta
|
||||
assert_eq!(hdr.key_generation, 0x03);
|
||||
assert_eq!(hdr.key_area_encryption_key_index, 0x01);
|
||||
assert_eq!(hdr.content_size, 0x0010_0000);
|
||||
assert_eq!(hdr.title_id, 0x0100_ABCD_0000_EF00);
|
||||
assert_eq!(hdr.sdk_version, 0x000B_0000);
|
||||
assert_eq!(hdr.crypto_type, 0x01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_nca_header_preserves_fs_headers() {
|
||||
let pt = build_plaintext_header();
|
||||
let pt_array: [u8; NCA_FULL_HEADER_SIZE] = pt.as_slice().try_into().unwrap();
|
||||
let encrypted_array: [u8; NCA_FULL_HEADER_SIZE] =
|
||||
xts_encrypt_header(&pt_array, &XTS_HEADER_KEY).as_slice().try_into().unwrap();
|
||||
|
||||
let hdr = decrypt_nca_header(&encrypted_array, &XTS_HEADER_KEY).unwrap();
|
||||
|
||||
// FsHeader[0] values from fixture
|
||||
assert_eq!(hdr.fs_headers[0].version, 2);
|
||||
assert_eq!(hdr.fs_headers[0].fs_type, 0x01); // PartitionFS
|
||||
assert_eq!(hdr.fs_headers[0].hash_type, 0x02); // PFS0
|
||||
assert_eq!(hdr.fs_headers[0].encryption_type, 0x02); // CTR
|
||||
assert!(hdr.fs_headers[0].exists);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_nca_header_wrong_key_fails_magic_check() {
|
||||
let pt = build_plaintext_header();
|
||||
let pt_array: [u8; NCA_FULL_HEADER_SIZE] = pt.as_slice().try_into().unwrap();
|
||||
let encrypted_array: [u8; NCA_FULL_HEADER_SIZE] =
|
||||
xts_encrypt_header(&pt_array, &XTS_HEADER_KEY).as_slice().try_into().unwrap();
|
||||
|
||||
// Use a different header key
|
||||
let wrong_key: [u8; 32] = [0xFFu8; 32];
|
||||
let result = decrypt_nca_header(&encrypted_array, &wrong_key);
|
||||
|
||||
// With wrong key, decryption produces garbage → magic check fails
|
||||
match result {
|
||||
Err(NcaError::BadMagic { .. }) => { /* expected */ }
|
||||
Err(NcaError::UnsupportedVersion { .. }) => { /* also valid — garbage byte at 0x11D */ }
|
||||
Ok(hdr) => {
|
||||
// If it somehow passes magic/version, the field values should be garbage
|
||||
// (not matching our known values)
|
||||
assert!(
|
||||
hdr.title_id != 0x0100_ABCD_0000_EF00
|
||||
|| hdr.content_size != 0x0010_0000
|
||||
|| hdr.content_type != 0x01
|
||||
|| hdr.key_area_encryption_key_index != 0x01,
|
||||
"wrong-key decrypt should not recover original fields"
|
||||
);
|
||||
}
|
||||
_ => { /* also acceptable */ }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_nca_header_wrong_key_produces_different_result() {
|
||||
let pt = build_plaintext_header();
|
||||
let pt_array: [u8; NCA_FULL_HEADER_SIZE] = pt.as_slice().try_into().unwrap();
|
||||
let encrypted_array: [u8; NCA_FULL_HEADER_SIZE] =
|
||||
xts_encrypt_header(&pt_array, &XTS_HEADER_KEY).as_slice().try_into().unwrap();
|
||||
|
||||
let hdr_correct = decrypt_nca_header(&encrypted_array, &XTS_HEADER_KEY).unwrap();
|
||||
|
||||
let wrong_key: [u8; 32] = [0xBBu8; 32];
|
||||
let hdr_wrong = decrypt_nca_header(&encrypted_array, &wrong_key);
|
||||
|
||||
// If wrong-key result is Ok, it must differ from correct
|
||||
if let Ok(hdr) = hdr_wrong {
|
||||
assert_ne!(hdr.title_id, hdr_correct.title_id,
|
||||
"wrong key must produce different parsed fields");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_nca_header_deterministic() {
|
||||
let pt = build_plaintext_header();
|
||||
let pt_array: [u8; NCA_FULL_HEADER_SIZE] = pt.as_slice().try_into().unwrap();
|
||||
let encrypted_array: [u8; NCA_FULL_HEADER_SIZE] =
|
||||
xts_encrypt_header(&pt_array, &XTS_HEADER_KEY).as_slice().try_into().unwrap();
|
||||
|
||||
let hdr1 = decrypt_nca_header(&encrypted_array, &XTS_HEADER_KEY).unwrap();
|
||||
let hdr2 = decrypt_nca_header(&encrypted_array, &XTS_HEADER_KEY).unwrap();
|
||||
|
||||
assert_eq!(hdr1.title_id, hdr2.title_id);
|
||||
assert_eq!(hdr1.content_size, hdr2.content_size);
|
||||
assert_eq!(hdr1.key_area, hdr2.key_area);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_nca_header_handles_all_zero_header() {
|
||||
// An all-zero encrypted header decrypts to some plaintext (not all-zero,
|
||||
// because XTS produces well-distributed output). The parser should either
|
||||
// reject bad magic or parse whatever bytes emerge.
|
||||
let encrypted_zero = [0u8; NCA_FULL_HEADER_SIZE];
|
||||
let result = decrypt_nca_header(&encrypted_zero, &XTS_HEADER_KEY);
|
||||
// Should either fail with BadMagic/UnsupportedVersion, or parse successfully
|
||||
// but with whatever the decrypted bytes contain. Neither case panics.
|
||||
match result {
|
||||
Ok(_) | Err(NcaError::BadMagic { .. }) | Err(NcaError::UnsupportedVersion { .. }) => {}
|
||||
Err(e) => {
|
||||
// Any other error (e.g., TruncatedFile) also acceptable — no panic.
|
||||
let _ = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
//! with derived keys, then decrypt and verify.
|
||||
|
||||
use crate::security::efuse::EfuseArray;
|
||||
use crate::security::key_derivation::KeyDerivation;
|
||||
use crate::security::key_derivation::{KeyDerivation, KeySet};
|
||||
use crate::security::nca_decrypt::{
|
||||
decrypt_nca_key_area, decrypt_nca_section, parse_nca_header,
|
||||
NCA_FULL_HEADER_SIZE, NCA_SECTION_COUNT,
|
||||
decrypt_nca_header, decrypt_nca_key_area, decrypt_nca_section, parse_nca_header,
|
||||
NcaError, NCA3_MAGIC, NCA_FULL_HEADER_SIZE, NCA_SECTION_COUNT,
|
||||
};
|
||||
use crate::security::aes::{aes_encrypt_block, aes_decrypt_block, Aes128Key};
|
||||
use crate::security::aes::{aes_encrypt_block, aes_xts_encrypt, Aes128Key};
|
||||
|
||||
/// Known reference title key (same as used in key_derivation tests).
|
||||
const KNOWN_TITLE_KEY: [u8; 16] = [
|
||||
@@ -204,3 +204,279 @@ fn test_nca_header_parsing_preserves_metadata() {
|
||||
assert_eq!(hdr.key_area_encryption_key_index, 0x00);
|
||||
assert!(hdr.fs_headers[0].exists, "section 0 must exist in the fixture");
|
||||
}
|
||||
|
||||
// ── Integration test: full NCA3 with all 4 sections ───────────────
|
||||
|
||||
/// Build an NCA3 fixture where all 4 sections are populated with distinct
|
||||
/// known payloads. Uses a KeySet's device key for encryption so that
|
||||
/// `KeySet::derive_title_key()` can recover the title key.
|
||||
///
|
||||
/// Returns (header_bytes, sections_ciphertext, section_ctrs, keyset).
|
||||
fn build_four_section_fixture() -> (Vec<u8>, [Vec<u8>; 4], [u64; 4], KeySet) {
|
||||
let efuse = EfuseArray::new_t210();
|
||||
let ks = KeySet::from_efuse(&efuse);
|
||||
let device_key = ks.device_key();
|
||||
let dk = Aes128Key::from_bytes(&device_key);
|
||||
let tk_key = Aes128Key::from_bytes(&KNOWN_TITLE_KEY);
|
||||
|
||||
let sections_pt: [[u8; 32]; 4] = [
|
||||
*b"Section0: RomFS header payload!\0",
|
||||
*b"Section1: PartitionFS entries!!\0",
|
||||
*b"Section2: Meta data goes here!!\0",
|
||||
*b"Section3: Legal info goes here!\0",
|
||||
];
|
||||
let section_ctrs: [u64; 4] = [0x100, 0x200, 0x300, 0x400];
|
||||
|
||||
// Encrypt each section with AES-CTR using the title key
|
||||
let sections_ct: [Vec<u8>; 4] = [
|
||||
encrypt_ctr(&tk_key, §ions_pt[0], section_ctrs[0]),
|
||||
encrypt_ctr(&tk_key, §ions_pt[1], section_ctrs[1]),
|
||||
encrypt_ctr(&tk_key, §ions_pt[2], section_ctrs[2]),
|
||||
encrypt_ctr(&tk_key, §ions_pt[3], section_ctrs[3]),
|
||||
];
|
||||
|
||||
// Encrypt title key with device key → key area entry 0
|
||||
let encrypted_tk = aes_encrypt_block(&dk, &KNOWN_TITLE_KEY);
|
||||
let mut header = vec![0u8; NCA_FULL_HEADER_SIZE];
|
||||
|
||||
// NCA3 header block at 0x100
|
||||
NCA3_MAGIC.iter().enumerate().for_each(|(i, &b)| header[0x100 + i] = b);
|
||||
header[0x104] = 0x00; // distribution_type
|
||||
header[0x105] = 0x00; // content_type = Program
|
||||
header[0x106] = 0x02; // key_generation
|
||||
header[0x107] = 0x00; // key_area_encryption_key_index
|
||||
header[0x11C] = 0x00; // crypto_type
|
||||
header[0x11D] = 0x03; // format_version = 3
|
||||
|
||||
// Key area: encrypted title key at entry 0, block 0
|
||||
header[0x200..0x210].copy_from_slice(&encrypted_tk);
|
||||
|
||||
// FsEntry table: 4 entries at 0x240
|
||||
for i in 0..4 {
|
||||
let base = 0x240 + i * 0x20;
|
||||
let start: u32 = i as u32;
|
||||
let end: u32 = (i + 1) as u32;
|
||||
header[base..base + 4].copy_from_slice(&start.to_le_bytes());
|
||||
header[base + 4..base + 8].copy_from_slice(&end.to_le_bytes());
|
||||
}
|
||||
|
||||
// FsHeader blocks at 0x400, 0x600, 0x800, 0xA00
|
||||
for i in 0..4 {
|
||||
let base = 0x400 + i * 0x200;
|
||||
header[base] = 0x02; // version = 2
|
||||
header[base + 1] = i as u8; // fs_type varies per section
|
||||
header[base + 2] = 0x03; // hash_type
|
||||
header[base + 3] = 0x02; // encryption_type = CTR
|
||||
header[base + 8..base + 16].copy_from_slice(&(0x1000u64 + (i as u64 * 0x1000)).to_le_bytes());
|
||||
header[base + 16..base + 24].copy_from_slice(&32u64.to_le_bytes());
|
||||
}
|
||||
|
||||
(header, sections_ct, section_ctrs, ks)
|
||||
}
|
||||
|
||||
fn encrypt_ctr(key: &Aes128Key, plaintext: &[u8], ctr: u64) -> Vec<u8> {
|
||||
let mut ct = Vec::with_capacity(plaintext.len());
|
||||
for bi in 0..((plaintext.len() + 15) / 16) {
|
||||
let mut ctr_block = [0u8; 16];
|
||||
ctr_block[0..8].copy_from_slice(&ctr.to_be_bytes());
|
||||
ctr_block[8..16].copy_from_slice(&(bi as u64).to_be_bytes());
|
||||
let ks = aes_encrypt_block(key, &ctr_block);
|
||||
let off = bi * 16;
|
||||
let take = (plaintext.len() - off).min(16);
|
||||
for i in 0..take {
|
||||
ct.push(plaintext[off + i] ^ ks[i]);
|
||||
}
|
||||
}
|
||||
ct
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nca3_four_sections_parse_all_fs_entries() {
|
||||
let (header, _, _, _) = build_four_section_fixture();
|
||||
let hdr = parse_nca_header(&header).unwrap();
|
||||
assert_eq!(hdr.magic, NCA3_MAGIC);
|
||||
for i in 0..4 {
|
||||
assert!(!hdr.fs_headers[i].exists
|
||||
|| hdr.fs_entries[i].start_offset < hdr.fs_entries[i].end_offset,
|
||||
"section {} FsEntry must be valid", i);
|
||||
}
|
||||
assert_eq!(hdr.fs_headers.len(), 4);
|
||||
assert_eq!(hdr.fs_entries.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nca3_four_sections_fs_header_fields() {
|
||||
let (header, _, _, _) = build_four_section_fixture();
|
||||
let hdr = parse_nca_header(&header).unwrap();
|
||||
for i in 0..4 {
|
||||
assert_eq!(hdr.fs_headers[i].version, 2, "FsHeader[{}].version must be 2", i);
|
||||
assert!(hdr.fs_headers[i].exists, "FsHeader[{}] must exist", i);
|
||||
assert_eq!(hdr.fs_headers[i].encryption_type, 0x02, "FsHeader[{}] must use CTR", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn test_nca3_four_sections_decrypt_all_with_KeySet() {
|
||||
let (header, sections_ct, section_ctrs, ks) = build_four_section_fixture();
|
||||
let hdr = parse_nca_header(&header).unwrap();
|
||||
|
||||
// Derive title key via KeySet — the fixture was built with this KeySet's device key
|
||||
let tk = ks.derive_title_key(&hdr.key_area[0]);
|
||||
assert_eq!(tk, KNOWN_TITLE_KEY, "KeySet must recover known title key");
|
||||
|
||||
let expected: [&[u8; 32]; 4] = [
|
||||
b"Section0: RomFS header payload!\0",
|
||||
b"Section1: PartitionFS entries!!\0",
|
||||
b"Section2: Meta data goes here!!\0",
|
||||
b"Section3: Legal info goes here!\0",
|
||||
];
|
||||
for i in 0..4 {
|
||||
let pt = decrypt_nca_section(&tk, §ions_ct[i], section_ctrs[i]);
|
||||
assert_eq!(&pt[..], &expected[i][..], "section {} plaintext mismatch", i);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Integration test: XTS header decrypt chain ─────────────────────
|
||||
|
||||
const XTS_INTEGRATION_KEY: [u8; 32] = [
|
||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
|
||||
0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
|
||||
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
|
||||
0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_xts_header_decrypt_and_parse_integration() {
|
||||
// Build plaintext header, XTS-encrypt it, then decrypt+parse
|
||||
let (header_pt, sections_ct, section_ctrs, ks) = build_four_section_fixture();
|
||||
let pt_array: [u8; NCA_FULL_HEADER_SIZE] = header_pt.as_slice().try_into().unwrap();
|
||||
|
||||
let key1 = Aes128Key::from_bytes(&XTS_INTEGRATION_KEY[0..16].try_into().unwrap());
|
||||
let key2 = Aes128Key::from_bytes(&XTS_INTEGRATION_KEY[16..32].try_into().unwrap());
|
||||
|
||||
// XTS-encrypt sector-by-sector (non-standard endianness-reversed tweak)
|
||||
let mut encrypted = vec![0u8; NCA_FULL_HEADER_SIZE];
|
||||
for sector in 0..6 {
|
||||
let off = sector * 0x200;
|
||||
let mut iv = [0u8; 16];
|
||||
iv[0..8].copy_from_slice(&(sector as u64).to_le_bytes());
|
||||
let enc = aes_xts_encrypt(&key1, &key2, &iv, &pt_array[off..off + 0x200]);
|
||||
encrypted[off..off + 0x200].copy_from_slice(&enc);
|
||||
}
|
||||
let enc_array: [u8; NCA_FULL_HEADER_SIZE] = encrypted.as_slice().try_into().unwrap();
|
||||
|
||||
// Decrypt and parse via the production path
|
||||
let hdr = decrypt_nca_header(&enc_array, &XTS_INTEGRATION_KEY).unwrap();
|
||||
assert_eq!(hdr.magic, NCA3_MAGIC);
|
||||
assert_eq!(hdr.distribution_type, 0x00);
|
||||
assert_eq!(hdr.content_type, 0x00);
|
||||
|
||||
// Verify key area survived the XTS roundtrip
|
||||
for i in 0..8 {
|
||||
assert_eq!(hdr.key_area[i], pt_array[0x200 + i * 16..0x200 + (i + 1) * 16],
|
||||
"key_area block {} mismatch after XTS roundtrip", i);
|
||||
}
|
||||
|
||||
// Verify sections decrypt correctly post-XTS
|
||||
let tk = ks.derive_title_key(&hdr.key_area[0]);
|
||||
let pt0 = decrypt_nca_section(&tk, §ions_ct[0], section_ctrs[0]);
|
||||
assert_eq!(&pt0[..], b"Section0: RomFS header payload!\0" as &[u8]);
|
||||
}
|
||||
|
||||
// ── Integration test: NcaError rejection paths ────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_bad_magic_in_integration_context() {
|
||||
let mut buf = vec![0u8; NCA_FULL_HEADER_SIZE];
|
||||
buf[0x100..0x104].copy_from_slice(b"NCA2");
|
||||
buf[0x11D] = 0x03; // valid version, so magic is the sole rejection
|
||||
let err = parse_nca_header(&buf).unwrap_err();
|
||||
assert!(matches!(err, NcaError::BadMagic { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncated_file_in_integration_context() {
|
||||
// Build a full fixture then truncate
|
||||
let (full, ..) = build_four_section_fixture();
|
||||
let truncated = &full[..0x200];
|
||||
let err = parse_nca_header(truncated).unwrap_err();
|
||||
assert!(matches!(err, NcaError::TruncatedFile { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_version_in_integration_context() {
|
||||
let (full, ..) = build_four_section_fixture();
|
||||
let mut modded = full.clone();
|
||||
modded[0x11D] = 7; // nonsense version
|
||||
let err = parse_nca_header(&modded).unwrap_err();
|
||||
match err {
|
||||
NcaError::UnsupportedVersion { version } => assert_eq!(version, 7),
|
||||
_ => panic!("expected UnsupportedVersion"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nca_error_implements_error_trait_in_integration() {
|
||||
// Prove NcaError can be used in a Result chain at the integration level
|
||||
fn returns_result() -> Result<(), NcaError> {
|
||||
Err(NcaError::InvalidKeyIndex { index: 8 })
|
||||
}
|
||||
let e = returns_result().unwrap_err();
|
||||
assert_eq!(e.to_string(), "invalid NCA key area index: 8 (valid range: 0-7)");
|
||||
}
|
||||
|
||||
// ── Regression: KeySet full chain mirrors KeyDerivation ───────────
|
||||
|
||||
#[test]
|
||||
fn test_keyset_full_chain_matches_key_derivation() {
|
||||
let efuse = EfuseArray::new_t210();
|
||||
let kd = KeyDerivation::from_efuse(&efuse);
|
||||
let ks = KeySet::from_efuse(&efuse);
|
||||
|
||||
let ssk = kd.derive_ssk();
|
||||
let dk_kd = kd.derive_device_key(&ssk);
|
||||
let dk_ks = ks.device_key();
|
||||
|
||||
assert_eq!(dk_ks, dk_kd,
|
||||
"KeySet device_key must match KeyDerivation for regression safety");
|
||||
|
||||
// Build fixture with the real derived key
|
||||
let (header_bytes, section_ct, section_ctr) = build_nca_fixture(&dk_kd);
|
||||
let hdr = parse_nca_header(&header_bytes).unwrap();
|
||||
|
||||
// Decrypt title key via both paths
|
||||
let tk_kd = decrypt_nca_key_area(&hdr, &dk_kd, 0);
|
||||
let tk_ks = ks.derive_title_key(&hdr.key_area[0]);
|
||||
assert_eq!(tk_ks, tk_kd,
|
||||
"KeySet title key must match KeyDerivation title key");
|
||||
|
||||
// Decrypt section via KeySet-derived title key
|
||||
let pt = decrypt_nca_section(&tk_ks, §ion_ct, section_ctr);
|
||||
assert_eq!(&pt[..], KNOWN_SECTION_PLAINTEXT);
|
||||
}
|
||||
|
||||
// ── Regression: M001 boot chain compatibility ────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_boot_chain_compatible_with_nca_parser() {
|
||||
// M001 boot chain: EfuseArray → KeyDerivation → device key.
|
||||
// After NCA parser changes, this must still work.
|
||||
let efuse = EfuseArray::new_t210();
|
||||
let kd = KeyDerivation::from_efuse(&efuse);
|
||||
let ssk = kd.derive_ssk();
|
||||
let device_key = kd.derive_device_key(&ssk);
|
||||
|
||||
// Verify the chain produces non-trivial keys (M001 regression check)
|
||||
assert_ne!(ssk, [0u8; 16]);
|
||||
assert_ne!(device_key, [0u8; 16]);
|
||||
assert_ne!(device_key, ssk);
|
||||
|
||||
// Build an NCA fixture using the derived device key and verify E2E
|
||||
let (header_bytes, section_ct, section_ctr) = build_nca_fixture(&device_key);
|
||||
let hdr = parse_nca_header(&header_bytes).unwrap();
|
||||
let tk = decrypt_nca_key_area(&hdr, &device_key, 0);
|
||||
let pt = decrypt_nca_section(&tk, §ion_ct, section_ctr);
|
||||
assert_eq!(&pt[..], KNOWN_SECTION_PLAINTEXT,
|
||||
"M001 boot chain must produce a valid NCA decryption pipeline");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user