gsd snapshot: uncommitted changes after 106m inactivity

This commit is contained in:
Oboromi GSD
2026-05-24 10:43:09 -04:00
parent 88eb65bb1a
commit 523970f28d
2 changed files with 513 additions and 6 deletions
+233 -2
View File
@@ -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 (05) 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];
// 0x0000x0FF: RSA-2048 signature (all zeros in test)
// 0x1000x1FF: 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 (0x2000x3FF): 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;
}
}
}
}
+280 -4
View File
@@ -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, &sections_pt[0], section_ctrs[0]),
encrypt_ctr(&tk_key, &sections_pt[1], section_ctrs[1]),
encrypt_ctr(&tk_key, &sections_pt[2], section_ctrs[2]),
encrypt_ctr(&tk_key, &sections_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, &sections_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, &sections_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, &section_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, &section_ct, section_ctr);
assert_eq!(&pt[..], KNOWN_SECTION_PLAINTEXT,
"M001 boot chain must produce a valid NCA decryption pipeline");
}