From 2ef5638cef5759ae5a25bf5f9f17e97b837edf71 Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Sun, 8 Jan 2023 19:21:04 +0300 Subject: [PATCH 1/6] Start porting over some NCA decryption code, make the NCA header parse --- Cargo.lock | 80 ++++++++++ Cargo.toml | 6 + src/bin/linkle_clap.rs | 75 +++++++++- src/error.rs | 10 ++ src/format/mod.rs | 1 + src/format/nca/mod.rs | 275 +++++++++++++++++++++++++++++++++++ src/format/nca/structures.rs | 164 +++++++++++++++++++++ src/pki.rs | 182 ++++++++++++++++++----- src/utils.rs | 83 +++++++++++ 9 files changed, 832 insertions(+), 44 deletions(-) create mode 100644 src/format/nca/mod.rs create mode 100644 src/format/nca/structures.rs diff --git a/Cargo.lock b/Cargo.lock index ec4dcf1..0c69367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "atty" version = "0.2.14" @@ -80,6 +86,30 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "binrw" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f846d8732b2a55b569b885852ecc925a2b1f24568f4707f8b1ccd5dc6805ea9b" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2aa66a5e35daf7f91ed44c945886597ef4c327f34f68b6bbf22951a250ceeb" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bit_field" version = "0.10.1" @@ -110,6 +140,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + [[package]] name = "byteorder" version = "1.4.3" @@ -330,6 +366,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "elf" version = "0.0.12" @@ -378,6 +420,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "gimli" version = "0.27.0" @@ -491,6 +545,7 @@ name = "linkle" version = "0.2.10" dependencies = [ "aes", + "binrw", "bit_field", "blz-nx", "byteorder", @@ -504,9 +559,11 @@ dependencies = [ "digest", "dirs-next", "elf", + "getset", "goblin", "lz4", "num-traits", + "pretty-hex", "rust-ini", "scroll", "semver", @@ -516,6 +573,7 @@ dependencies = [ "sha2", "snafu", "structopt", + "xts-mode", ] [[package]] @@ -608,12 +666,24 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "plain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1044,3 +1114,13 @@ name = "windows_x86_64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "xts-mode" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cbddb7545ca0b9ffa7bdc653e8743303e1712687a6918ced25f2cdbed42520" +dependencies = [ + "byteorder", + "cipher", +] diff --git a/Cargo.toml b/Cargo.toml index 0c4b02a..5d6daca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,10 @@ derive_more = "0.99" blz-nx = "1.0" bit_field = "0.10" cargo-toml2 = { version = "1.3.2", optional = true } +binrw = "0.10.0" +getset = "0.1.2" +pretty-hex = "0.3.0" + cipher = "0.4.3" digest = "0.10.6" @@ -48,5 +52,7 @@ ctr = "0.9.2" aes = "0.8.2" cmac = "0.7.1" +xts-mode = "0.5.1" + [features] binaries = ["structopt", "cargo_metadata", "semver", "scroll", "goblin", "clap", "cargo-toml2"] diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 98927cd..87aa55a 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -103,6 +103,41 @@ enum Opt { #[structopt(long = "console-unique")] show_console_unique: bool, }, + /// Extract NCA + #[structopt(name = "nca_extract")] + NcaExtract { + /// Sets the input file to use. + #[structopt(parse(from_os_str))] + input_file: PathBuf, + + /// Sets the output file to extract the header to. + #[structopt(parse(from_os_str), long = "header-json")] + header_file: Option, + + /// Sets the output file to extract the section0 to. + #[structopt(parse(from_os_str), long = "section0")] + section0_file: Option, + + /// Sets the output file to extract the section1 to. + #[structopt(parse(from_os_str), long = "section1")] + section1_file: Option, + + /// Sets the output file to extract the section2 to. + #[structopt(parse(from_os_str), long = "section2")] + section2_file: Option, + + /// Sets the output file to extract the section3 to. + #[structopt(parse(from_os_str), long = "section3")] + section3_file: Option, + + /// Use development keys instead of retail + #[structopt(short = "d", long = "dev")] + dev: bool, + + /// Keyfile + #[structopt(parse(from_os_str), short = "k", long = "keyset")] + keyfile: Option, + }, } fn create_nxo( @@ -241,17 +276,28 @@ fn print_keys( console_unique: bool, minimal: bool, ) -> Result<(), linkle::error::Error> { - let keys = if is_dev { - linkle::pki::Keys::new_dev(key_path).unwrap() - } else { - linkle::pki::Keys::new_retail(key_path).unwrap() - }; + let keys = linkle::pki::Keys::new(key_path, is_dev).unwrap(); keys.write(&mut std::io::stdout(), console_unique, minimal) .unwrap(); Ok(()) } +fn extract_nca( + input_file: &Path, + is_dev: bool, + key_path: Option<&Path>, + output_header_json: Option<&Path>, + output_section0: Option<&Path>, + output_section1: Option<&Path>, + output_section2: Option<&Path>, + output_section3: Option<&Path>, +) -> Result<(), linkle::error::Error> { + let keys = linkle::pki::Keys::new(key_path, is_dev).unwrap(); + let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?).unwrap(); + todo!() +} + fn to_opt_ref>(s: &Option) -> Option<&U> { s.as_ref().map(AsRef::as_ref) } @@ -303,6 +349,25 @@ fn process_args(app: &Opt) { show_console_unique, minimal, } => print_keys(*dev, to_opt_ref(keyfile), *show_console_unique, *minimal), + Opt::NcaExtract { + ref input_file, + ref header_file, + ref section0_file, + ref section1_file, + ref section2_file, + ref section3_file, + dev, + ref keyfile, + } => extract_nca( + input_file, + *dev, + to_opt_ref(keyfile), + to_opt_ref(header_file), + to_opt_ref(section0_file), + to_opt_ref(section1_file), + to_opt_ref(section2_file), + to_opt_ref(section3_file), + ), }; if let Err(e) = res { diff --git a/src/error.rs b/src/error.rs index 2284c44..8e0a457 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,6 +57,16 @@ pub enum Error { error: PathBuf, backtrace: Backtrace, }, + #[snafu(display("Missing key {}. Make sure your keyfile is complete", key_name))] + MissingKey { + key_name: &'static str, + backtrace: Backtrace, + }, + #[snafu(display("Failed to parse NCA. Make sure your {} key is correct.", key_name))] + NcaParse { + key_name: &'static str, + backtrace: Backtrace, + }, } impl Error { diff --git a/src/format/mod.rs b/src/format/mod.rs index 97d597e..d67d734 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -1,4 +1,5 @@ pub mod nacp; +pub mod nca; mod npdm; pub mod nxo; pub mod pfs0; diff --git a/src/format/nca/mod.rs b/src/format/nca/mod.rs new file mode 100644 index 0000000..9778e9f --- /dev/null +++ b/src/format/nca/mod.rs @@ -0,0 +1,275 @@ +//! NCA Parsing +//! +//! Nintendo Container Archives (NCAs) are signed and encrypted archives that +//! contain software and other content Nintendo provides. Almost every file on +//! the Horizon/NX OS are stored in this container, as it guarantees its +//! authenticity, preventing tampering. +//! +//! For more information about the NCA file format, see the [switchbrew page]. +//! +//! In order to parse an NCA, you may use the `from_file` method: +//! +//! ``` +//! # fn get_nca_file() -> std::io::Result { +//! # std::fs::File::open("tests/fixtures/test.nca") +//! # } +//! let f = get_nca_file()?; +//! let nca = Nca::from_file(nca)?; +//! let section = nca.section(0); +//! ``` +//! +//! [switchbrew page]: https://switchbrew.org/w/index.php?title=NCA_Format + +use crate::error::Error; +use crate::format::nca::structures::{ContentType, CryptoType, KeyType, RawNca, RawSuperblock}; +use crate::impl_debug_deserialize_serialize_hexstring; +use crate::pki::{Aes128Key, AesXtsKey, Keys}; +use binrw::BinRead; +use serde_derive::{Deserialize, Serialize}; +use snafu::{Backtrace, GenerateImplicitData}; +use std::cmp::max; +use std::io::Read; + +mod structures; + +#[repr(transparent)] +#[derive(Clone, Copy)] +struct Hash([u8; 0x20]); +impl_debug_deserialize_serialize_hexstring!(Hash); + +#[derive(Debug, Serialize, Deserialize, Clone)] +enum FsType { + Pfs0 { + master_hash: Hash, + block_size: u32, + hash_table_offset: u64, + hash_table_size: u64, + pfs0_offset: u64, + pfs0_size: u64, + }, + RomFs, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +enum NcaFormat { + Nca3, + Nca2, + Nca0, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SectionJson { + media_start_offset: u32, + media_end_offset: u32, + unknown1: u32, + unknown2: u32, + crypto: CryptoType, + fstype: FsType, + nounce: u64, +} + +#[derive(Serialize, Deserialize, Clone, Copy)] +#[repr(transparent)] +pub struct TitleId(u64); + +impl std::fmt::Debug for TitleId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:016x}", self.0) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NcaJson { + format: NcaFormat, + sig: structures::SigDebug, + npdm_sig: structures::SigDebug, + is_gamecard: bool, + content_type: ContentType, + key_revision: u8, + key_type: KeyType, + nca_size: u64, + title_id: TitleId, + sdk_version: u32, // TODO: Better format + xts_key: AesXtsKey, + ctr_key: Aes128Key, + rights_id: Option<[u8; 0x10]>, + sections: [Option; 4], +} + +#[derive(Debug)] +pub struct Nca { + stream: R, + json: NcaJson, +} + +fn get_key_area_key(pki: &Keys, key_version: usize, key_type: KeyType) -> Result { + let key = match key_type { + KeyType::Application => pki.key_area_key_application()[key_version], + KeyType::Ocean => pki.key_area_key_ocean()[key_version], + KeyType::System => pki.key_area_key_system()[key_version], + }; + key.ok_or(Error::MissingKey { + key_name: Box::leak( + format!("key_area_key_application_{:02x}", key_version).into_boxed_str(), + ), + backtrace: Backtrace::generate(), + }) +} + +// Crypto is stupid. First, we need to get the max of crypto_type and crypto_type2. +// Then, nintendo uses both 0 and 1 as master key 0, and then everything is shifted by one. +// So we sub by 1. +fn get_master_key_revision(crypto_type: u8, crypto_type2: u8) -> u8 { + max(crypto_type2, crypto_type).saturating_sub(1) +} + +fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { + // Decrypt header. + let mut header = [0; 0xC00]; + let mut decrypted_header = [0; 0xC00]; + + file.read_exact(&mut header)?; + + // TODO: Check if NCA is already decrypted + + let header_key = pki.header_key().as_ref().ok_or(Error::MissingKey { + key_name: "header_key", + backtrace: Backtrace::generate(), + })?; + decrypted_header[..0x400].copy_from_slice(&header[..0x400]); + header_key.decrypt(&mut decrypted_header[..0x400], 0, 0x200)?; + + // skip 2 signature blocks + let magic = &decrypted_header[0x200..][..4]; + match magic { + b"NCA3" => { + decrypted_header.copy_from_slice(&header); + header_key.decrypt(&mut decrypted_header, 0, 0x200)?; + } + b"NCA2" => { + todo!() + // for (i, fsheader) in raw_nca.fs_headers.iter().enumerate() { + // let offset = 0x400 + i * 0x200; + // if &fsheader._0x148[..] != &[0; 0xB8][..] { + // decrypted_header[offset..offset + 0x200] + // .copy_from_slice(&header[offset..offset + 0x200]); + // header_key.decrypt(&mut decrypted_header[offset..offset + 0x200], 0, 0x200)?; + // } else { + // decrypted_header[offset..offset + 0x200].copy_from_slice(&[0; 0x200]); + // } + // } + } + b"NCA0" => unimplemented!("NCA0 parsing is not implemented yet"), + _ => { + return Err(Error::NcaParse { + key_name: "header_key", + backtrace: Backtrace::generate(), + }) + } + } + + // println!("{}", pretty_hex::pretty_hex(&decrypted_header)); + + let mut raw_nca = std::io::Cursor::new(decrypted_header); + let raw_nca = RawNca::read_le(&mut raw_nca).expect("RawNca to be of the right size"); + Ok(raw_nca) +} + +impl Nca { + pub fn from_file(pki: &Keys, mut file: R) -> Result, Error> { + let header = decrypt_header(pki, &mut file)?; + let format = match &header.magic { + b"NCA3" => NcaFormat::Nca3, + b"NCA2" => NcaFormat::Nca2, + b"NCA0" => NcaFormat::Nca0, + _ => unreachable!(), + }; + + // TODO: NCA: Verify header with RSA2048 PSS + // BODY: We want to make sure the NCAs have a valid signature before + // BODY: decrypting. Maybe put it behind a flag that accepts invalidly + // BODY: signed NCAs? + + let master_key_revision = get_master_key_revision(header.crypto_type, header.crypto_type2); + + // Handle Rights ID. + let has_rights_id = header.rights_id != [0; 0x10]; + + let key_area_key = get_key_area_key(pki, master_key_revision as _, header.key_type)?; + + let decrypted_keys = if !has_rights_id { + // TODO: NCA0 => return + ( + key_area_key.derive_xts_key(&header.encrypted_xts_key)?, + key_area_key.derive_key(&header.encrypted_ctr_key)?, + ) + } else { + // TODO: Implement RightsID crypto. + unimplemented!("Rights ID"); + }; + + // Parse sections + let mut sections = [None, None, None, None]; + for (idx, (section, fs)) in header + .section_entries + .iter() + .zip(header.fs_headers.iter()) + .enumerate() + { + // Check if section is present + if let Some(fs) = fs { + if has_rights_id { + unimplemented!("Rights ID"); + } else { + assert_eq!(fs.version, 2, "Invalid NCA FS Header version"); + unsafe { + sections[idx] = Some(SectionJson { + crypto: fs.crypt_type.into(), + fstype: match fs.superblock { + RawSuperblock::Pfs0(s) => FsType::Pfs0 { + master_hash: Hash(s.master_hash), + block_size: s.block_size, + hash_table_offset: s.hash_table_offset, + hash_table_size: s.hash_table_size, + pfs0_offset: s.pfs0_offset, + pfs0_size: s.pfs0_size, + }, + // RawSuperblock::RomFs => FsType::RomFs, + _ => unreachable!(), + }, + nounce: fs.section_ctr, + media_start_offset: section.media_start_offset, + media_end_offset: section.media_end_offset, + unknown1: section.unknown1, + unknown2: section.unknown2, + }); + } + } + } + } + + let nca = Nca { + stream: file, + json: NcaJson { + format, + sig: header.fixed_key_sig, + npdm_sig: header.npdm_sig, + is_gamecard: header.is_gamecard != 0, + content_type: ContentType::from(header.content_type), + key_revision: master_key_revision, + key_type: KeyType::from(header.key_type), + nca_size: header.nca_size, + title_id: TitleId(header.title_id), + // TODO: Store the SDK version in a more human readable format. + sdk_version: header.sdk_version, + xts_key: decrypted_keys.0, + ctr_key: decrypted_keys.1, + // TODO: Implement rights id. + rights_id: None, + sections: sections, + }, + }; + + Ok(nca) + } +} diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs new file mode 100644 index 0000000..f9da73a --- /dev/null +++ b/src/format/nca/structures.rs @@ -0,0 +1,164 @@ +//! Raw NCA structures +//! +//! Those are used by the NCA parsing code, basically casting the byte slices +//! to those types through the plain crate. This only works on Little Endian +//! hosts! Ideally, we would have a derive macro that generates an +//! appropriate parser based on the machine's endianness. + +use crate::impl_debug_deserialize_serialize_hexstring; +use binrw::{BinRead, BinResult, BinWrite, ReadOptions}; +use serde_derive::{Deserialize, Serialize}; +use std::fmt; +use std::io::{Read, Seek}; + +#[repr(transparent)] +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct SigDebug(pub [u8; 0x100]); + +impl_debug_deserialize_serialize_hexstring!(SigDebug); + +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct SkipDebug + BinWrite + 'static>(pub T); + +impl + BinWrite + 'static> fmt::Debug for SkipDebug { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SkipDebug")?; + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] +#[brw(repr = u8)] +pub enum KeyType { + Application = 0, + Ocean = 1, + System = 2, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] +#[brw(repr = u8)] +pub enum ContentType { + Program = 0, + Meta = 1, + Control = 2, + Manual = 3, + Data = 4, + PublicData = 5, +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct RawNca { + pub fixed_key_sig: SigDebug, + pub npdm_sig: SigDebug, + pub magic: [u8; 4], + pub is_gamecard: u8, + pub content_type: ContentType, + pub crypto_type: u8, + pub key_type: KeyType, + pub nca_size: u64, + pub title_id: u64, + pub _padding0: SkipDebug, + pub sdk_version: u32, + pub crypto_type2: u8, + pub _padding1: SkipDebug<[u8; 0xF]>, + pub rights_id: [u8; 0x10], + pub section_entries: [RawSectionTableEntry; 4], + pub section_hashes: [[u8; 0x20]; 4], + pub encrypted_xts_key: [u8; 0x20], + pub encrypted_ctr_key: [u8; 0x10], + pub unknown_new_key: [u8; 0x10], + pub _padding2: SkipDebug<[u8; 0xC0]>, + #[br(parse_with = read_fs_headers(section_entries))] + // #[bw(write_with = "binrw::io::write_zeroes")] // TODO: we need to write zeroes in case of None here! + pub fs_headers: [Option; 4], +} +// assert_eq_size!(assert_nca_size; RawNca, [u8; 0xC00]); + +fn read_fs_headers( + section_entries: [RawSectionTableEntry; 4], +) -> impl FnOnce(&mut R, &ReadOptions, ()) -> BinResult<[Option; 4]> { + move |reader, options, _| { + let mut res = [None, None, None, None]; + + for i in 0..4 { + res[i] = if section_entries[i].media_start_offset != 0 { + Some(RawNcaFsHeader::read_options(reader, options, ())?) + } else { + <[u8; 0x200]>::read_options(reader, options, ())?; + None + } + } + + Ok(res) + } +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct RawPfs0Superblock { + pub master_hash: [u8; 0x20], + pub block_size: u32, + pub always_2: u32, + pub hash_table_offset: u64, + pub hash_table_size: u64, + pub pfs0_offset: u64, + // #[brw(align_after = 0x138)] + pub pfs0_size: u64, + pub _0x48: SkipDebug<[u8; 0xF0]>, +} + +#[derive(Clone, Copy, Debug, BinRead, BinWrite)] +#[br(import(partition_type: RawPartitionType, crypt_type: CryptoType))] +pub enum RawSuperblock { + #[br(pre_assert(partition_type == RawPartitionType::Pfs0))] + Pfs0(RawPfs0Superblock), + // Romfs(RomfsSuperblock), + // Bktr(BktrSuperblock), + // Nca0Romfs(Nca0RomfsSuperblock), + Raw([u8; 0x138]), +} +// assert_eq_size!(assert_superblock_size; RawSuperblock, [u8; 0x138]); + +#[derive(Clone, Copy, Debug, BinRead, BinWrite)] +pub struct RawNcaFsHeader { + pub version: u16, + pub partition_type: RawPartitionType, + pub fs_type: RawFsType, + pub crypt_type: CryptoType, + pub _0x5: [u8; 0x3], + #[br(args(partition_type, crypt_type))] + pub superblock: RawSuperblock, + pub section_ctr: u64, + pub _0x148: [u8; 0xB8], +} +// assert_eq_size!(assert_nca_fs_header_size; RawNcaFsHeader, [u8; 0x148 + 0xB8]); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, BinRead, BinWrite)] +#[brw(repr = u8)] +pub enum RawPartitionType { + RomFs = 0, + Pfs0 = 1, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, BinRead, BinWrite)] +#[brw(repr = u8)] +pub enum RawFsType { + Pfs0 = 2, + RomFs = 3, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, BinRead, BinWrite)] +#[brw(repr = u8)] +pub enum CryptoType { + None = 1, + Xts = 2, + Ctr = 3, + Bktr = 4, +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct RawSectionTableEntry { + pub media_start_offset: u32, + pub media_end_offset: u32, + pub unknown1: u32, + pub unknown2: u32, +} diff --git a/src/pki.rs b/src/pki.rs index 037ff19..139946a 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -1,49 +1,32 @@ use crate::error::Error; +use crate::impl_debug_deserialize_serialize_hexstring; use aes::Aes128; use cipher::{ generic_array::GenericArray, BlockDecrypt, BlockEncrypt, KeyInit, KeyIvInit, StreamCipher, }; use cmac::{Cmac, Mac}; use ctr::Ctr128BE; +use getset::Getters; use ini::{self, Properties}; use snafu::{Backtrace, GenerateImplicitData}; -use std::fmt; use std::fs::File; use std::io::{self, ErrorKind, Write}; use std::path::Path; +use xts_mode::Xts128; -struct Aes128Key([u8; 0x10]); -struct AesXtsKey([u8; 0x20]); -struct EncryptedKeyblob([u8; 0xB0]); -struct Keyblob([u8; 0x90]); -struct Modulus([u8; 0x100]); - -macro_rules! impl_debug { - ($for:ident) => { - impl fmt::Debug for $for { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in &self.0[..] { - write!(f, "{:02X}", byte)?; - } - Ok(()) - } - } - impl fmt::Display for $for { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in &self.0[..] { - write!(f, "{:02X}", byte)?; - } - Ok(()) - } - } - }; -} +#[derive(Clone, Copy)] +pub struct Aes128Key(pub [u8; 0x10]); +#[derive(Clone, Copy)] +pub struct AesXtsKey(pub [u8; 0x20]); +pub struct EncryptedKeyblob(pub [u8; 0xB0]); +pub struct Keyblob(pub [u8; 0x90]); +pub struct Modulus(pub [u8; 0x100]); -impl_debug!(Aes128Key); -impl_debug!(AesXtsKey); -impl_debug!(EncryptedKeyblob); -impl_debug!(Keyblob); -impl_debug!(Modulus); +impl_debug_deserialize_serialize_hexstring!(Aes128Key); +impl_debug_deserialize_serialize_hexstring!(AesXtsKey); +impl_debug_deserialize_serialize_hexstring!(EncryptedKeyblob); +impl_debug_deserialize_serialize_hexstring!(Keyblob); +impl_debug_deserialize_serialize_hexstring!(Modulus); impl Keyblob { fn encrypt( @@ -94,7 +77,7 @@ impl EncryptedKeyblob { } impl Aes128Key { - fn derive_key(&self, source: &[u8; 0x10]) -> Result { + pub fn derive_key(&self, source: &[u8; 0x10]) -> Result { let mut newkey = *source; let crypter = Aes128::new(GenericArray::from_slice(&self.0)); @@ -112,7 +95,7 @@ impl Aes128Key { Ok(Aes128Key(newkey)) } - fn derive_xts_key(&self, source: &[u8; 0x20]) -> Result { + pub fn derive_xts_key(&self, source: &[u8; 0x20]) -> Result { let mut newkey = *source; let crypter = Aes128::new(GenericArray::from_slice(&self.0)); @@ -123,6 +106,68 @@ impl Aes128Key { } } +fn get_tweak(mut sector: usize) -> [u8; 0x10] { + let mut tweak = [0; 0x10]; + for tweak in tweak.iter_mut().rev() { + /* Nintendo LE custom tweak... */ + *tweak = (sector & 0xFF) as u8; + sector >>= 8; + } + tweak +} + +impl AesXtsKey { + pub fn decrypt( + &self, + data: &mut [u8], + mut sector: usize, + sector_size: usize, + ) -> Result<(), Error> { + if data.len() % sector_size != 0 { + return Err(Error::Crypto { + error: String::from("Length must be multiple of sectors!"), + backtrace: Backtrace::generate(), + }); + } + + for i in (0..data.len()).step_by(sector_size) { + let tweak = get_tweak(sector); + + let key1 = Aes128::new(GenericArray::from_slice(&self.0[0x00..0x10])); + let key2 = Aes128::new(GenericArray::from_slice(&self.0[0x10..0x20])); + let crypter = Xts128::::new(key1, key2); + crypter.decrypt_sector(&mut data[i..i + sector_size], tweak); + sector += 1; + } + Ok(()) + } + + pub fn encrypt( + &self, + data: &mut [u8], + mut sector: usize, + sector_size: usize, + ) -> Result<(), Error> { + if data.len() % sector_size != 0 { + return Err(Error::Crypto { + error: String::from("Length must be multiple of sectors!"), + backtrace: Backtrace::generate(), + }); + } + + for i in (0..data.len()).step_by(sector_size) { + let tweak = get_tweak(sector); + + let key1 = Aes128::new(GenericArray::from_slice(&self.0[0x00..0x10])); + let key2 = Aes128::new(GenericArray::from_slice(&self.0[0x10..0x20])); + let crypter = Xts128::::new(key1, key2); + crypter.decrypt_sector(&mut data[i..i + sector_size], tweak); + sector += 1; + } + Ok(()) + } +} + fn key_to_aes(keys: &Properties, name: &str, key: &mut [u8]) -> Result, Error> { let value = keys.get(name); if let Some(value) = value { @@ -172,63 +217,114 @@ impl OptionExt for Option { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Getters)] pub struct Keys { + #[get = "pub"] secure_boot_key: Option, + #[get = "pub"] tsec_key: Option, + #[get = "pub"] device_key: Option, + #[get = "pub"] keyblob_keys: [Option; 0x20], + #[get = "pub"] keyblob_mac_keys: [Option; 0x20], + #[get = "pub"] encrypted_keyblobs: [Option; 0x20], + #[get = "pub"] mariko_aes_class_keys: [Option; 0xC], + #[get = "pub"] mariko_kek: Option, + #[get = "pub"] mariko_bek: Option, + #[get = "pub"] keyblobs: [Option; 0x20], + #[get = "pub"] keyblob_key_sources: [Option; 0x20], + #[get = "pub"] keyblob_mac_key_source: Option, + #[get = "pub"] tsec_root_kek: Option, + #[get = "pub"] package1_mac_kek: Option, + #[get = "pub"] package1_kek: Option, + #[get = "pub"] tsec_auth_signatures: [Option; 0x20], + #[get = "pub"] tsec_root_key: [Option; 0x20], + #[get = "pub"] master_kek_sources: [Option; 0x20], + #[get = "pub"] mariko_master_kek_sources: [Option; 0x20], + #[get = "pub"] master_keks: [Option; 0x20], + #[get = "pub"] master_key_source: Option, + #[get = "pub"] master_keys: [Option; 0x20], + #[get = "pub"] package1_mac_keys: [Option; 0x20], + #[get = "pub"] package1_keys: [Option; 0x20], + #[get = "pub"] package2_keys: [Option; 0x20], + #[get = "pub"] package2_key_source: Option, + #[get = "pub"] per_console_key_source: Option, + #[get = "pub"] aes_kek_generation_source: Option, + #[get = "pub"] aes_key_generation_source: Option, + #[get = "pub"] key_area_key_application_source: Option, + #[get = "pub"] key_area_key_ocean_source: Option, + #[get = "pub"] key_area_key_system_source: Option, + #[get = "pub"] titlekek_source: Option, + #[get = "pub"] header_kek_source: Option, + #[get = "pub"] sd_card_kek_source: Option, + #[get = "pub"] sd_card_save_key_source: Option, + #[get = "pub"] sd_card_nca_key_source: Option, + #[get = "pub"] save_mac_kek_source: Option, + #[get = "pub"] save_mac_key_source: Option, + #[get = "pub"] header_key_source: Option, + #[get = "pub"] header_key: Option, + #[get = "pub"] titlekeks: [Option; 0x20], + #[get = "pub"] key_area_key_application: [Option; 0x20], + #[get = "pub"] key_area_key_ocean: [Option; 0x20], + #[get = "pub"] key_area_key_system: [Option; 0x20], + #[get = "pub"] xci_header_key: Option, + #[get = "pub"] save_mac_key: Option, + #[get = "pub"] sd_card_save_key: Option, + #[get = "pub"] sd_card_nca_key: Option, - // these fields are not used yet, but will be when we implement the NCA decryption #[allow(dead_code)] + #[get = "pub"] nca_hdr_fixed_key_modulus: [Option; 2], #[allow(dead_code)] + #[get = "pub"] acid_fixed_key_modulus: [Option; 2], #[allow(dead_code)] + #[get = "pub"] package2_fixed_key_modulus: Option, } @@ -593,7 +689,7 @@ fn generate_kek( impl Keys { #[allow(clippy::new_ret_no_self)] - fn new( + fn new_with_modulus( key_path: Option<&Path>, default_key_name: &Path, modulus: ([Modulus; 2], [Modulus; 2], Modulus), @@ -647,7 +743,7 @@ impl Keys { } pub fn new_retail(key_path: Option<&Path>) -> Result { - Keys::new( + Keys::new_with_modulus( key_path, Path::new("prod.keys"), ( @@ -781,7 +877,7 @@ impl Keys { } pub fn new_dev(key_path: Option<&Path>) -> Result { - Keys::new( + Keys::new_with_modulus( key_path, Path::new("dev.keys"), ( @@ -914,6 +1010,14 @@ impl Keys { ) } + pub fn new(key_path: Option<&Path>, is_dev: bool) -> Result { + if is_dev { + Self::new_dev(key_path) + } else { + Self::new_retail(key_path) + } + } + #[allow(clippy::cognitive_complexity)] fn read_from_ini(&mut self, mut file: File) -> Result<(), Error> { let config = ini::Ini::read_from(&mut file)?; diff --git a/src/utils.rs b/src/utils.rs index 7cdd709..0804aaa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,89 @@ use core::ops::{BitAnd, Not}; use num_traits::Num; use std::io; +pub struct Hexstring<'a>(pub &'a [u8]); + +impl<'a> core::fmt::Debug for Hexstring<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for byte in self.0 { + write!(f, "{:02X}", byte)?; + } + Ok(()) + } +} + +#[macro_export] +macro_rules! impl_debug_deserialize_serialize_hexstring { + ($for:ident) => { + impl std::fmt::Debug for $for { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(stringify!($for)) + .field(&$crate::utils::Hexstring(&self.0[..])) + .finish() + } + } + + impl std::fmt::Display for $for { + fn fmt(&self, f: &mut core::fmt::Formatter) -> std::fmt::Result { + std::fmt::Debug::fmt(&$crate::utils::Hexstring(&self.0[..]), f) + } + } + + impl<'de> serde::Deserialize<'de> for $for { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct StrVisitor; + impl<'de> serde::de::Visitor<'de> for StrVisitor { + type Value = $for; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a character hexstring") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let mut value = [0; std::mem::size_of::<$for>()]; + if s.len() != std::mem::size_of::<$for>() * 2 { + return Err(E::invalid_length(s.len(), &self)); + } + for (idx, c) in s.bytes().enumerate() { + let c = match c { + b'a'..=b'z' => c - b'a' + 10, + b'A'..=b'Z' => c - b'A' + 10, + b'0'..=b'9' => c - b'0', + _ => { + return Err(E::invalid_value( + serde::de::Unexpected::Str(s), + &self, + )) + } + }; + value[idx / 2] |= c << if idx % 2 == 0 { 4 } else { 0 } + } + + Ok($for(value)) + } + } + + deserializer.deserialize_str(StrVisitor) + } + } + + impl serde::Serialize for $for { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } + } + }; +} + pub fn align_down + BitAnd + Copy>(addr: T, align: T) -> T { addr & !(align - T::one()) } From 817e072275a33cc591df999ad0d82407dadc9e4a Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Sun, 8 Jan 2023 20:42:40 +0300 Subject: [PATCH 2/6] Make the NCA decryption nicer with binrw magic --- src/format/nca/mod.rs | 95 +++++++++++++------------ src/format/nca/structures.rs | 130 +++++++++++++++++++++++++++++++++-- 2 files changed, 174 insertions(+), 51 deletions(-) diff --git a/src/format/nca/mod.rs b/src/format/nca/mod.rs index 9778e9f..9e319b4 100644 --- a/src/format/nca/mod.rs +++ b/src/format/nca/mod.rs @@ -21,7 +21,9 @@ //! [switchbrew page]: https://switchbrew.org/w/index.php?title=NCA_Format use crate::error::Error; -use crate::format::nca::structures::{ContentType, CryptoType, KeyType, RawNca, RawSuperblock}; +use crate::format::nca::structures::{ + ContentType, CryptoType, KeyType, NcaMagic, RawNca, RawSuperblock, +}; use crate::impl_debug_deserialize_serialize_hexstring; use crate::pki::{Aes128Key, AesXtsKey, Keys}; use binrw::BinRead; @@ -126,7 +128,7 @@ fn get_master_key_revision(crypto_type: u8, crypto_type2: u8) -> u8 { fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { // Decrypt header. let mut header = [0; 0xC00]; - let mut decrypted_header = [0; 0xC00]; + // let mut decrypted_header = [0; 0xC00]; file.read_exact(&mut header)?; @@ -136,52 +138,57 @@ fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { key_name: "header_key", backtrace: Backtrace::generate(), })?; - decrypted_header[..0x400].copy_from_slice(&header[..0x400]); - header_key.decrypt(&mut decrypted_header[..0x400], 0, 0x200)?; - - // skip 2 signature blocks - let magic = &decrypted_header[0x200..][..4]; - match magic { - b"NCA3" => { - decrypted_header.copy_from_slice(&header); - header_key.decrypt(&mut decrypted_header, 0, 0x200)?; - } - b"NCA2" => { - todo!() - // for (i, fsheader) in raw_nca.fs_headers.iter().enumerate() { - // let offset = 0x400 + i * 0x200; - // if &fsheader._0x148[..] != &[0; 0xB8][..] { - // decrypted_header[offset..offset + 0x200] - // .copy_from_slice(&header[offset..offset + 0x200]); - // header_key.decrypt(&mut decrypted_header[offset..offset + 0x200], 0, 0x200)?; - // } else { - // decrypted_header[offset..offset + 0x200].copy_from_slice(&[0; 0x200]); - // } - // } - } - b"NCA0" => unimplemented!("NCA0 parsing is not implemented yet"), - _ => { - return Err(Error::NcaParse { - key_name: "header_key", - backtrace: Backtrace::generate(), - }) - } - } + // decrypted_header[..0x400].copy_from_slice(&header[..0x400]); + // header_key.decrypt(&mut decrypted_header[..0x400], 0, 0x200)?; + // + // // skip 2 signature blocks + // let magic = &decrypted_header[0x200..][..4]; + // match magic { + // b"NCA3" => { + // decrypted_header.copy_from_slice(&header); + // header_key.decrypt(&mut decrypted_header, 0, 0x200)?; + // } + // b"NCA2" => { + // todo!() + // // for (i, fsheader) in raw_nca.fs_headers.iter().enumerate() { + // // let offset = 0x400 + i * 0x200; + // // if &fsheader._0x148[..] != &[0; 0xB8][..] { + // // decrypted_header[offset..offset + 0x200] + // // .copy_from_slice(&header[offset..offset + 0x200]); + // // header_key.decrypt(&mut decrypted_header[offset..offset + 0x200], 0, 0x200)?; + // // } else { + // // decrypted_header[offset..offset + 0x200].copy_from_slice(&[0; 0x200]); + // // } + // // } + // } + // b"NCA0" => unimplemented!("NCA0 parsing is not implemented yet"), + // _ => { + // return Err(Error::NcaParse { + // key_name: "header_key", + // backtrace: Backtrace::generate(), + // }) + // } + // } // println!("{}", pretty_hex::pretty_hex(&decrypted_header)); - let mut raw_nca = std::io::Cursor::new(decrypted_header); - let raw_nca = RawNca::read_le(&mut raw_nca).expect("RawNca to be of the right size"); + let mut raw_nca = std::io::Cursor::new(header); + let raw_nca = RawNca::read_le_args(&mut raw_nca, (header_key.clone(),)) + .expect("RawNca to be of the right size"); Ok(raw_nca) } impl Nca { pub fn from_file(pki: &Keys, mut file: R) -> Result, Error> { - let header = decrypt_header(pki, &mut file)?; + let RawNca { + sigs, + header, + fs_headers, + } = decrypt_header(pki, &mut file)?; let format = match &header.magic { - b"NCA3" => NcaFormat::Nca3, - b"NCA2" => NcaFormat::Nca2, - b"NCA0" => NcaFormat::Nca0, + NcaMagic::Nca3 => NcaFormat::Nca3, + NcaMagic::Nca2 => NcaFormat::Nca2, + NcaMagic::Nca0 => NcaFormat::Nca0, _ => unreachable!(), }; @@ -213,7 +220,7 @@ impl Nca { for (idx, (section, fs)) in header .section_entries .iter() - .zip(header.fs_headers.iter()) + .zip(fs_headers.iter()) .enumerate() { // Check if section is present @@ -252,12 +259,12 @@ impl Nca { stream: file, json: NcaJson { format, - sig: header.fixed_key_sig, - npdm_sig: header.npdm_sig, + sig: sigs.fixed_key_sig, + npdm_sig: sigs.npdm_sig, is_gamecard: header.is_gamecard != 0, - content_type: ContentType::from(header.content_type), + content_type: header.content_type, key_revision: master_key_revision, - key_type: KeyType::from(header.key_type), + key_type: header.key_type, nca_size: header.nca_size, title_id: TitleId(header.title_id), // TODO: Store the SDK version in a more human readable format. diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs index f9da73a..e2bcb53 100644 --- a/src/format/nca/structures.rs +++ b/src/format/nca/structures.rs @@ -6,10 +6,12 @@ //! appropriate parser based on the machine's endianness. use crate::impl_debug_deserialize_serialize_hexstring; -use binrw::{BinRead, BinResult, BinWrite, ReadOptions}; +use crate::pki::AesXtsKey; +use binrw::{BinRead, BinResult, BinWrite, BinrwNamedArgs, ReadOptions}; use serde_derive::{Deserialize, Serialize}; use std::fmt; -use std::io::{Read, Seek}; +use std::io::{Read, Seek, Write}; +use std::ops::Deref; #[repr(transparent)] #[derive(Clone, Copy, BinRead, BinWrite)] @@ -17,6 +19,76 @@ pub struct SigDebug(pub [u8; 0x100]); impl_debug_deserialize_serialize_hexstring!(SigDebug); +const XTS_SECTOR_SIZE: usize = 0x200; + +#[derive(Debug, Clone, Copy)] +pub struct XtsCryptSector(pub T); + +impl Deref for XtsCryptSector { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Copy, Clone, BinrwNamedArgs)] +pub struct XtsCryptArgs { + pub key: AesXtsKey, + pub sector: usize, +} + +impl, const Size: usize> BinRead for XtsCryptSector { + type Args = XtsCryptArgs; + + fn read_options( + reader: &mut R, + options: &ReadOptions, + args: Self::Args, + ) -> BinResult { + let mut buf = [0u8; Size]; + reader.read_exact(&mut buf)?; + + args.key + .decrypt(&mut buf, args.sector, Size) + .map_err(|e| binrw::Error::Custom { + pos: 0, + err: Box::new(e), + })?; + + let mut buf = std::io::Cursor::new(buf); + T::read_options(&mut buf, options, ()).map(XtsCryptSector) + } +} + +impl, const Size: usize> BinWrite for XtsCryptSector { + type Args = XtsCryptArgs; + + fn write_options( + &self, + writer: &mut W, + options: &binrw::WriteOptions, + args: Self::Args, + ) -> BinResult<()> { + let mut buf = [0u8; Size]; + let mut buf = std::io::Cursor::new(&mut buf[..]); + self.0.write_options(&mut buf, options, ())?; + + assert_eq!(buf.position() as usize, Size, "Buffer not fully written"); + + let buf = buf.into_inner(); + + args.key + .encrypt(buf, args.sector, Size) + .map_err(|e| binrw::Error::Custom { + pos: 0, + err: Box::new(e), + })?; + writer.write_all(buf)?; + Ok(()) + } +} + #[derive(Clone, Copy, BinRead, BinWrite)] pub struct SkipDebug + BinWrite + 'static>(pub T); @@ -46,11 +118,27 @@ pub enum ContentType { PublicData = 5, } +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, BinRead, BinWrite)] +pub enum NcaMagic { + #[brw(magic = b"NCA0")] + Nca0, + #[brw(magic = b"NCA1")] + Nca1, + #[brw(magic = b"NCA2")] + Nca2, + #[brw(magic = b"NCA3")] + Nca3, +} + #[derive(Debug, Clone, Copy, BinRead, BinWrite)] -pub struct RawNca { +pub struct NcaSigs { pub fixed_key_sig: SigDebug, pub npdm_sig: SigDebug, - pub magic: [u8; 4], +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct RawNcaHeader { + pub magic: NcaMagic, pub is_gamecard: u8, pub content_type: ContentType, pub crypto_type: u8, @@ -68,21 +156,49 @@ pub struct RawNca { pub encrypted_ctr_key: [u8; 0x10], pub unknown_new_key: [u8; 0x10], pub _padding2: SkipDebug<[u8; 0xC0]>, - #[br(parse_with = read_fs_headers(section_entries))] +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[brw(import(key: AesXtsKey))] +pub struct RawNca { + #[brw(args { key, sector: 0 })] + pub sigs: XtsCryptSector, + #[brw(args { key, sector: 1 })] + pub header: XtsCryptSector, + #[br(parse_with = read_fs_headers(&header.0, key))] // #[bw(write_with = "binrw::io::write_zeroes")] // TODO: we need to write zeroes in case of None here! pub fs_headers: [Option; 4], } // assert_eq_size!(assert_nca_size; RawNca, [u8; 0xC00]); fn read_fs_headers( - section_entries: [RawSectionTableEntry; 4], + header: &RawNcaHeader, + key: AesXtsKey, ) -> impl FnOnce(&mut R, &ReadOptions, ()) -> BinResult<[Option; 4]> { + let magic = header.magic; + let section_entries = header.section_entries; + move |reader, options, _| { let mut res = [None, None, None, None]; for i in 0..4 { res[i] = if section_entries[i].media_start_offset != 0 { - Some(RawNcaFsHeader::read_options(reader, options, ())?) + let section_header = >::read_options( + reader, + options, + XtsCryptArgs { + key, + sector: match magic { + // For pre-1.0.0 "NCA2" NCAs, the first 0x400 byte are encrypted the same way as in NCA3. + // However, each section header is individually encrypted as though it were sector 0, instead of the appropriate sector as in NCA3. + NcaMagic::Nca3 => 2 + i, + NcaMagic::Nca2 => 0, + _ => todo!("{:?}", magic), + }, + }, + )?; + + Some(section_header.0) } else { <[u8; 0x200]>::read_options(reader, options, ())?; None From e4579aa3dacbe46b36824b9ad2164ac6ebf68a2c Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Mon, 9 Jan 2023 01:50:52 +0300 Subject: [PATCH 3/6] Implement the title_key derivation, along with improvements to key handling --- Cargo.lock | 7 + Cargo.toml | 1 + src/bin/linkle_clap.rs | 13 +- src/error.rs | 11 +- src/format/nca/mod.rs | 150 ++++++------- src/format/nca/structures.rs | 86 ++++++-- src/pki.rs | 394 ++++++++++++++++++++++++++--------- 7 files changed, 481 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c69367..d193fcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,6 +491,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "inout" version = "0.1.3" @@ -561,6 +567,7 @@ dependencies = [ "elf", "getset", "goblin", + "hex", "lz4", "num-traits", "pretty-hex", diff --git a/Cargo.toml b/Cargo.toml index 5d6daca..f8d44ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ cargo-toml2 = { version = "1.3.2", optional = true } binrw = "0.10.0" getset = "0.1.2" pretty-hex = "0.3.0" +hex = "0.4.3" cipher = "0.4.3" diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 87aa55a..f32ffb5 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -130,6 +130,9 @@ enum Opt { #[structopt(parse(from_os_str), long = "section3")] section3_file: Option, + /// Sets the title key to use (if the NCA has RightsId crypto). + title_key: Option, + /// Use development keys instead of retail #[structopt(short = "d", long = "dev")] dev: bool, @@ -276,7 +279,7 @@ fn print_keys( console_unique: bool, minimal: bool, ) -> Result<(), linkle::error::Error> { - let keys = linkle::pki::Keys::new(key_path, is_dev).unwrap(); + let keys = linkle::pki::Keys::new(key_path, is_dev)?; keys.write(&mut std::io::stdout(), console_unique, minimal) .unwrap(); @@ -287,14 +290,16 @@ fn extract_nca( input_file: &Path, is_dev: bool, key_path: Option<&Path>, + title_key: Option<&str>, output_header_json: Option<&Path>, output_section0: Option<&Path>, output_section1: Option<&Path>, output_section2: Option<&Path>, output_section3: Option<&Path>, ) -> Result<(), linkle::error::Error> { - let keys = linkle::pki::Keys::new(key_path, is_dev).unwrap(); - let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?).unwrap(); + let keys = linkle::pki::Keys::new(key_path, is_dev)?; + let title_key = title_key.map(linkle::pki::parse_title_key).transpose()?; + let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?, title_key)?; todo!() } @@ -356,12 +361,14 @@ fn process_args(app: &Opt) { ref section1_file, ref section2_file, ref section3_file, + title_key, dev, ref keyfile, } => extract_nca( input_file, *dev, to_opt_ref(keyfile), + to_opt_ref(title_key), to_opt_ref(header_file), to_opt_ref(section0_file), to_opt_ref(section1_file), diff --git a/src/error.rs b/src/error.rs index 8e0a457..55be0b9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use crate::format::nca::RightsId; +use crate::pki::KeyName; use snafu::Snafu; use snafu::{Backtrace, GenerateImplicitData}; use std::io; @@ -57,9 +59,14 @@ pub enum Error { error: PathBuf, backtrace: Backtrace, }, - #[snafu(display("Missing key {}. Make sure your keyfile is complete", key_name))] + #[snafu(display("Missing key {:?}. Make sure your keyfile is complete", key_name))] MissingKey { - key_name: &'static str, + key_name: KeyName, + backtrace: Backtrace, + }, + #[snafu(display("Missing titlekey for {}. Make sure you have provided it", rights_id))] + MissingTitleKey { + rights_id: RightsId, backtrace: Backtrace, }, #[snafu(display("Failed to parse NCA. Make sure your {} key is correct.", key_name))] diff --git a/src/format/nca/mod.rs b/src/format/nca/mod.rs index 9e319b4..f718fc1 100644 --- a/src/format/nca/mod.rs +++ b/src/format/nca/mod.rs @@ -22,10 +22,9 @@ use crate::error::Error; use crate::format::nca::structures::{ - ContentType, CryptoType, KeyType, NcaMagic, RawNca, RawSuperblock, + ContentType, CryptoType, Hash, KeyType, NcaMagic, RawNca, RawSuperblock, }; -use crate::impl_debug_deserialize_serialize_hexstring; -use crate::pki::{Aes128Key, AesXtsKey, Keys}; +use crate::pki::{Aes128Key, AesXtsKey, KeyName, Keys, TitleKey}; use binrw::BinRead; use serde_derive::{Deserialize, Serialize}; use snafu::{Backtrace, GenerateImplicitData}; @@ -34,10 +33,7 @@ use std::io::Read; mod structures; -#[repr(transparent)] -#[derive(Clone, Copy)] -struct Hash([u8; 0x20]); -impl_debug_deserialize_serialize_hexstring!(Hash); +pub use structures::RightsId; #[derive(Debug, Serialize, Deserialize, Clone)] enum FsType { @@ -63,9 +59,7 @@ enum NcaFormat { pub struct SectionJson { media_start_offset: u32, media_end_offset: u32, - unknown1: u32, - unknown2: u32, - crypto: CryptoType, + crypto: NcaCrypto, fstype: FsType, nounce: u64, } @@ -92,9 +86,7 @@ pub struct NcaJson { nca_size: u64, title_id: TitleId, sdk_version: u32, // TODO: Better format - xts_key: AesXtsKey, - ctr_key: Aes128Key, - rights_id: Option<[u8; 0x10]>, + rights_id: RightsId, sections: [Option; 4], } @@ -104,18 +96,13 @@ pub struct Nca { json: NcaJson, } -fn get_key_area_key(pki: &Keys, key_version: usize, key_type: KeyType) -> Result { - let key = match key_type { - KeyType::Application => pki.key_area_key_application()[key_version], - KeyType::Ocean => pki.key_area_key_ocean()[key_version], - KeyType::System => pki.key_area_key_system()[key_version], +fn get_key_area_key(pki: &Keys, key_version: u8, key_type: KeyType) -> Result { + let key_name = match key_type { + KeyType::Application => KeyName::KeyAreaKeyApplication(key_version), + KeyType::Ocean => KeyName::KeyAreaKeyOcean(key_version), + KeyType::System => KeyName::KeyAreaKeySystem(key_version), }; - key.ok_or(Error::MissingKey { - key_name: Box::leak( - format!("key_area_key_application_{:02x}", key_version).into_boxed_str(), - ), - backtrace: Backtrace::generate(), - }) + pki.get_key(key_name) } // Crypto is stupid. First, we need to get the max of crypto_type and crypto_type2. @@ -134,10 +121,7 @@ fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { // TODO: Check if NCA is already decrypted - let header_key = pki.header_key().as_ref().ok_or(Error::MissingKey { - key_name: "header_key", - backtrace: Backtrace::generate(), - })?; + let header_key = pki.get_xts_key(KeyName::HeaderKey)?; // decrypted_header[..0x400].copy_from_slice(&header[..0x400]); // header_key.decrypt(&mut decrypted_header[..0x400], 0, 0x200)?; // @@ -173,13 +157,25 @@ fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { // println!("{}", pretty_hex::pretty_hex(&decrypted_header)); let mut raw_nca = std::io::Cursor::new(header); - let raw_nca = RawNca::read_le_args(&mut raw_nca, (header_key.clone(),)) - .expect("RawNca to be of the right size"); + let raw_nca = + RawNca::read_le_args(&mut raw_nca, (header_key,)).expect("RawNca to be of the right size"); Ok(raw_nca) } +#[derive(Debug, Serialize, Deserialize, Clone)] +enum NcaCrypto { + None, + Ctr(Aes128Key), + Bktr(Aes128Key), + Xts(AesXtsKey), +} + impl Nca { - pub fn from_file(pki: &Keys, mut file: R) -> Result, Error> { + pub fn from_file( + pki: &Keys, + mut file: R, + title_key: Option, // TODO: get titlekey from a DB? + ) -> Result, Error> { let RawNca { sigs, header, @@ -200,19 +196,27 @@ impl Nca { let master_key_revision = get_master_key_revision(header.crypto_type, header.crypto_type2); // Handle Rights ID. - let has_rights_id = header.rights_id != [0; 0x10]; + let has_rights_id = !header.rights_id.is_empty(); - let key_area_key = get_key_area_key(pki, master_key_revision as _, header.key_type)?; + let key_area_key = get_key_area_key(pki, master_key_revision, header.key_type)?; - let decrypted_keys = if !has_rights_id { - // TODO: NCA0 => return - ( - key_area_key.derive_xts_key(&header.encrypted_xts_key)?, - key_area_key.derive_key(&header.encrypted_ctr_key)?, + let ctr_crypto_key = key_area_key.derive_key(&header.encrypted_ctr_key)?; + let xts_crypto_key = key_area_key.derive_xts_key(&header.encrypted_xts_key)?; + let title_key = if has_rights_id { + let titlekek = pki.get_key(KeyName::Titlekek(master_key_revision))?; + + Some( + titlekek.derive_key( + &title_key + .ok_or_else(|| Error::MissingTitleKey { + rights_id: header.rights_id, + backtrace: Backtrace::generate(), + })? + .0, + )?, ) } else { - // TODO: Implement RightsID crypto. - unimplemented!("Rights ID"); + None }; // Parse sections @@ -225,33 +229,42 @@ impl Nca { { // Check if section is present if let Some(fs) = fs { - if has_rights_id { - unimplemented!("Rights ID"); + assert_eq!(fs.version, 2, "Invalid NCA FS Header version"); + + let crypto = if has_rights_id { + match fs.crypt_type { + CryptoType::Ctr => NcaCrypto::Ctr(title_key.unwrap()), + CryptoType::Bktr => NcaCrypto::Bktr(title_key.unwrap()), + CryptoType::None => NcaCrypto::None, + CryptoType::Xts => unreachable!(), + } } else { - assert_eq!(fs.version, 2, "Invalid NCA FS Header version"); - unsafe { - sections[idx] = Some(SectionJson { - crypto: fs.crypt_type.into(), - fstype: match fs.superblock { - RawSuperblock::Pfs0(s) => FsType::Pfs0 { - master_hash: Hash(s.master_hash), - block_size: s.block_size, - hash_table_offset: s.hash_table_offset, - hash_table_size: s.hash_table_size, - pfs0_offset: s.pfs0_offset, - pfs0_size: s.pfs0_size, - }, - // RawSuperblock::RomFs => FsType::RomFs, - _ => unreachable!(), - }, - nounce: fs.section_ctr, - media_start_offset: section.media_start_offset, - media_end_offset: section.media_end_offset, - unknown1: section.unknown1, - unknown2: section.unknown2, - }); + match fs.crypt_type { + CryptoType::None => NcaCrypto::None, + CryptoType::Xts => NcaCrypto::Xts(xts_crypto_key), + CryptoType::Ctr => NcaCrypto::Ctr(ctr_crypto_key), + CryptoType::Bktr => NcaCrypto::Bktr(ctr_crypto_key), } - } + }; + + sections[idx] = Some(SectionJson { + crypto, + fstype: match fs.superblock { + RawSuperblock::Pfs0(s) => FsType::Pfs0 { + master_hash: s.master_hash, + block_size: s.block_size, + hash_table_offset: s.hash_table_offset, + hash_table_size: s.hash_table_size, + pfs0_offset: s.pfs0_offset, + pfs0_size: s.pfs0_size, + }, + RawSuperblock::RomFs(_) => FsType::RomFs, + _ => panic!("Unknown superblock type"), + }, + nounce: fs.section_ctr, + media_start_offset: section.media_start_offset, + media_end_offset: section.media_end_offset, + }); } } @@ -269,11 +282,8 @@ impl Nca { title_id: TitleId(header.title_id), // TODO: Store the SDK version in a more human readable format. sdk_version: header.sdk_version, - xts_key: decrypted_keys.0, - ctr_key: decrypted_keys.1, - // TODO: Implement rights id. - rights_id: None, - sections: sections, + rights_id: header.rights_id, + sections, }, }; diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs index e2bcb53..f4dffa7 100644 --- a/src/format/nca/structures.rs +++ b/src/format/nca/structures.rs @@ -99,6 +99,11 @@ impl + BinWrite + 'static> fmt::Debug for SkipD } } +#[repr(transparent)] +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct Hash([u8; 0x20]); +impl_debug_deserialize_serialize_hexstring!(Hash); + #[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] #[brw(repr = u8)] pub enum KeyType { @@ -136,6 +141,17 @@ pub struct NcaSigs { pub npdm_sig: SigDebug, } +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct RightsId(pub [u8; 0x10]); + +impl RightsId { + pub fn is_empty(&self) -> bool { + self.0.iter().all(|&b| b == 0) + } +} + +impl_debug_deserialize_serialize_hexstring!(RightsId); + #[derive(Debug, Clone, Copy, BinRead, BinWrite)] pub struct RawNcaHeader { pub magic: NcaMagic, @@ -149,9 +165,9 @@ pub struct RawNcaHeader { pub sdk_version: u32, pub crypto_type2: u8, pub _padding1: SkipDebug<[u8; 0xF]>, - pub rights_id: [u8; 0x10], + pub rights_id: RightsId, pub section_entries: [RawSectionTableEntry; 4], - pub section_hashes: [[u8; 0x20]; 4], + pub section_hashes: [Hash; 4], pub encrypted_xts_key: [u8; 0x20], pub encrypted_ctr_key: [u8; 0x10], pub unknown_new_key: [u8; 0x10], @@ -210,8 +226,8 @@ fn read_fs_headers( } #[derive(Debug, Clone, Copy, BinRead, BinWrite)] -pub struct RawPfs0Superblock { - pub master_hash: [u8; 0x20], +pub struct Pfs0Superblock { + pub master_hash: Hash, pub block_size: u32, pub always_2: u32, pub hash_table_offset: u64, @@ -222,13 +238,60 @@ pub struct RawPfs0Superblock { pub _0x48: SkipDebug<[u8; 0xF0]>, } +pub const IVFC_MAX_LEVEL: usize = 6; + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct BktrHeader { + pub offset: u64, + pub size: u64, + pub magic: [u8; 4], /* "BKTR" */ + pub _0x14: u32, /* Version? */ + pub num_entries: u32, + pub _0x1c: u32, /* Reserved? */ +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct IvfcLevelHeader { + pub logical_offset: u64, + pub hash_data_size: u64, + pub block_size: u32, + pub reserved: u32, +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[brw(magic = b"IVFC")] +pub struct IvfcHeader { + pub id: u32, + pub master_hash_size: u32, + pub num_levels: u32, + pub level_headers: [IvfcLevelHeader; IVFC_MAX_LEVEL], + pub _0xa0: [u8; 0x20], + pub master_hash: Hash, +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct BktrSuperblock { + pub ivfc_header: IvfcHeader, + pub _0xe0: [u8; 0x18], + pub relocation_header: BktrHeader, + pub subsection_header: BktrHeader, +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct RomfsSuperblock { + pub ivfc_header: IvfcHeader, + pub _0xe0: [u8; 0x58], +} + #[derive(Clone, Copy, Debug, BinRead, BinWrite)] -#[br(import(partition_type: RawPartitionType, crypt_type: CryptoType))] +#[br(import(partition_type: RawPartitionType, fs_type: RawFsType, crypto_type: CryptoType))] pub enum RawSuperblock { - #[br(pre_assert(partition_type == RawPartitionType::Pfs0))] - Pfs0(RawPfs0Superblock), - // Romfs(RomfsSuperblock), - // Bktr(BktrSuperblock), + #[br(pre_assert(partition_type == RawPartitionType::Pfs0 && fs_type == RawFsType::Pfs0))] + Pfs0(Pfs0Superblock), + #[br(pre_assert(partition_type == RawPartitionType::RomFs && fs_type == RawFsType::RomFs && crypto_type == CryptoType::Bktr))] + Bktr(BktrSuperblock), + #[br(pre_assert(partition_type == RawPartitionType::RomFs && fs_type == RawFsType::RomFs))] + RomFs(RomfsSuperblock), // Nca0Romfs(Nca0RomfsSuperblock), Raw([u8; 0x138]), } @@ -241,7 +304,7 @@ pub struct RawNcaFsHeader { pub fs_type: RawFsType, pub crypt_type: CryptoType, pub _0x5: [u8; 0x3], - #[br(args(partition_type, crypt_type))] + #[br(args(partition_type, fs_type, crypt_type))] pub superblock: RawSuperblock, pub section_ctr: u64, pub _0x148: [u8; 0xB8], @@ -275,6 +338,5 @@ pub enum CryptoType { pub struct RawSectionTableEntry { pub media_start_offset: u32, pub media_end_offset: u32, - pub unknown1: u32, - pub unknown2: u32, + pub padding: [u8; 0x8], } diff --git a/src/pki.rs b/src/pki.rs index 139946a..21d526d 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -7,8 +7,10 @@ use cipher::{ use cmac::{Cmac, Mac}; use ctr::Ctr128BE; use getset::Getters; +use hex::FromHexError; use ini::{self, Properties}; use snafu::{Backtrace, GenerateImplicitData}; +use std::fmt::Debug; use std::fs::File; use std::io::{self, ErrorKind, Write}; use std::path::Path; @@ -18,6 +20,9 @@ use xts_mode::Xts128; pub struct Aes128Key(pub [u8; 0x10]); #[derive(Clone, Copy)] pub struct AesXtsKey(pub [u8; 0x20]); +// title key is not a real key, you have to do some more derivations to get the content key from it +#[derive(Clone, Copy)] +pub struct TitleKey(pub [u8; 0x10]); pub struct EncryptedKeyblob(pub [u8; 0xB0]); pub struct Keyblob(pub [u8; 0x90]); pub struct Modulus(pub [u8; 0x100]); @@ -168,41 +173,85 @@ impl AesXtsKey { } } -fn key_to_aes(keys: &Properties, name: &str, key: &mut [u8]) -> Result, Error> { - let value = keys.get(name); - if let Some(value) = value { - if value.len() != key.len() * 2 { - return Err(Error::Crypto { - error: format!( - "Key {} is not of the right size. It should be a {} byte hexstring", - name, - key.len() * 2 - ), - backtrace: Backtrace::generate(), - }); - } - for (idx, c) in value.bytes().enumerate() { - let c = match c { - b'a'..=b'z' => c - b'a' + 10, - b'A'..=b'Z' => c - b'A' + 10, - b'0'..=b'9' => c - b'0', - c => return Err(Error::Crypto { error: format!("Key {} contains invalid character {}. Each character should be a hexadecimal digit.", name, c as char), backtrace: Backtrace::generate()}) - }; - key[idx / 2] |= c << if idx % 2 == 0 { 4 } else { 0 }; - } - Ok(Some(())) - } else { - Ok(None) - } +fn parse_any_key(value: &str, name: &str, key: &mut [u8]) -> Result<(), Error> { + hex::decode_to_slice(value, key).map_err(|err| match err { + FromHexError::InvalidHexCharacter { c, .. } => + Error::Crypto { error: format!("Key {} contains invalid character {}. Each character should be a hexadecimal digit.", name, c as char), backtrace: Backtrace::generate()}, + FromHexError::OddLength | FromHexError::InvalidStringLength => Error::Crypto { + error: format!( + "Key {} is not of the right size. It should be a {} byte hexstring", + name, + key.len() * 2 + ), + backtrace: Backtrace::generate(), + }, + })?; + Ok(()) +} + +// TODO: I kinda want to have the name be a KeyName enum, but that would require rewriting the ini parsing... +pub fn parse_aes_key(value: &str, name: &str) -> Result { + let mut key = [0; 0x10]; + parse_any_key(value, name, &mut key)?; + Ok(Aes128Key(key)) +} + +pub fn parse_title_key(value: &str) -> Result { + let mut key = [0; 0x10]; + parse_any_key(value, "title key", &mut key)?; + Ok(TitleKey(key)) +} + +pub fn parse_aes_xts_key(value: &str, name: &str) -> Result { + let mut key = [0; 0x20]; + parse_any_key(value, name, &mut key)?; + Ok(AesXtsKey(key)) +} + +pub fn parse_keyblob(value: &str, name: &str) -> Result { + let mut keyblob = [0; 0x90]; + parse_any_key(value, name, &mut keyblob)?; + Ok(Keyblob(keyblob)) +} + +pub fn parse_encrypted_keyblob(value: &str, name: &str) -> Result { + let mut keyblob = [0; 0xB0]; + parse_any_key(value, name, &mut keyblob)?; + Ok(EncryptedKeyblob(keyblob)) +} + +// helper functions to parse the key files +fn key_to_aes(keys: &Properties, name: &str) -> Result, Error> { + keys.get(name).map(|v| parse_aes_key(v, name)).transpose() +} +fn key_to_xts(keys: &Properties, name: &str) -> Result, Error> { + keys.get(name) + .map(|v| parse_aes_xts_key(v, name)) + .transpose() } -fn key_to_aes_array( +fn key_to_aes_array(keys: &Properties, name: &str, idx: usize) -> Result, Error> { + key_to_aes(keys, &format!("{}_{:02x}", name, idx)) +} + +fn key_to_keyblob_array( keys: &Properties, name: &str, idx: usize, - key: &mut [u8], -) -> Result, Error> { - key_to_aes(keys, &format!("{}_{:02x}", name, idx), key) +) -> Result, Error> { + keys.get(&format!("{}_{:02x}", name, idx)) + .map(|v| parse_keyblob(v, name)) + .transpose() +} + +fn key_to_encrypted_keyblob_array( + keys: &Properties, + name: &str, + idx: usize, +) -> Result, Error> { + keys.get(&format!("{}_{:02x}", name, idx)) + .map(|v| parse_encrypted_keyblob(v, name)) + .transpose() } trait OptionExt { @@ -217,117 +266,280 @@ impl OptionExt for Option { } } -#[derive(Default, Debug, Getters)] +#[derive(Copy, Clone)] +pub enum KeyName { + SecureBootKey, + TsecKey, + DeviceKey, + KeyblobKey(u8), + KeyblobMacKey(u8), + MarikoAesClassKey(u8), + MarikoKek, + MarikoBek, + KeyblobKeySource(u8), + KeyblobMacKeySource, + TsecRootKek, + Package1MacKek, + TsecAuthSignature(u8), + TsecRootKey(u8), + MasterKekSource(u8), + MarikoMasterKekSource(u8), + MasterKek(u8), + MasterKeySource, + MasterKey(u8), + Package1MacKey(u8), + Package1Key(u8), + Package2Key(u8), + Package2KeySource, + PerConsoleKeySource, + AesKekGenerationSource, + AesKeyGenerationSource, + KeyAreaKeyApplicationSource, + KeyAreaKeyOceanSource, + KeyAreaKeySystemSource, + TitlekekSource, + HeaderKekSource, + SdCardKekSource, + SdCardSaveKeySource, + SdCardNcaKeySource, + SaveMacKekSource, + SaveMacKeySource, + HeaderKeySource, + HeaderKey, + Titlekek(u8), + KeyAreaKeyApplication(u8), + KeyAreaKeyOcean(u8), + KeyAreaKeySystem(u8), + XciHeaderKey, + SaveMacKey, + SdCardSaveKey, + SdCardNcaKey, +} + +impl Debug for KeyName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use KeyName::*; + // I wanna derive macros =( + match self { + SecureBootKey => write!(f, "secure_boot_key"), + TsecKey => write!(f, "tsec_key"), + DeviceKey => write!(f, "device_key"), + KeyblobKey(idx) => write!(f, "keyblob_key_{:02x}", idx), + KeyblobMacKey(idx) => write!(f, "keyblob_mac_key_{:02x}", idx), + MarikoAesClassKey(idx) => write!(f, "mariko_aes_class_key_{:02x}", idx), + MarikoKek => write!(f, "mariko_kek"), + MarikoBek => write!(f, "mariko_bek"), + KeyblobKeySource(idx) => write!(f, "keyblob_key_source_{:02x}", idx), + KeyblobMacKeySource => write!(f, "keyblob_mac_key_source"), + TsecRootKek => write!(f, "tsec_root_kek"), + Package1MacKek => write!(f, "package1_mac_kek"), + TsecAuthSignature(idx) => write!(f, "tsec_auth_signature_{:02x}", idx), + TsecRootKey(idx) => write!(f, "tsec_root_key_{:02x}", idx), + MasterKekSource(idx) => write!(f, "master_kek_source_{:02x}", idx), + MarikoMasterKekSource(idx) => write!(f, "mariko_master_kek_source_{:02x}", idx), + MasterKek(idx) => write!(f, "master_kek_{:02x}", idx), + MasterKeySource => write!(f, "master_key_source"), + MasterKey(idx) => write!(f, "master_key_{:02x}", idx), + Package1MacKey(idx) => write!(f, "package_1_mac_key_{:02x}", idx), + Package1Key(idx) => write!(f, "package_1_key_{:02x}", idx), + Package2Key(idx) => write!(f, "package_2_key_{:02x}", idx), + Package2KeySource => write!(f, "package_2_key_source"), + PerConsoleKeySource => write!(f, "per_console_key_source"), + AesKekGenerationSource => write!(f, "aes_kek_generation_source"), + AesKeyGenerationSource => write!(f, "aes_key_generation_source"), + KeyAreaKeyApplicationSource => write!(f, "key_area_key_application_source"), + KeyAreaKeyOceanSource => write!(f, "key_area_key_ocean_source"), + KeyAreaKeySystemSource => write!(f, "key_area_key_system_source"), + TitlekekSource => write!(f, "titlekek_source"), + HeaderKekSource => write!(f, "header_kek_source"), + SdCardKekSource => write!(f, "sd_card_kek_source"), + SdCardSaveKeySource => write!(f, "sd_card_save_key_source"), + SdCardNcaKeySource => write!(f, "sd_card_nca_key_source"), + SaveMacKekSource => write!(f, "save_mac_kek_source"), + SaveMacKeySource => write!(f, "save_mac_key_source"), + HeaderKeySource => write!(f, "header_key_source"), + HeaderKey => write!(f, "header_key"), + Titlekek(idx) => write!(f, "titlekek_{:02x}", idx), + KeyAreaKeyApplication(idx) => write!(f, "key_area_key_application_{:02x}", idx), + KeyAreaKeyOcean(idx) => write!(f, "key_area_key_ocean_{:02x}", idx), + KeyAreaKeySystem(idx) => write!(f, "key_area_key_system_{:02x}", idx), + XciHeaderKey => write!(f, "xci_header_key"), + SaveMacKey => write!(f, "save_mac_key"), + SdCardSaveKey => write!(f, "sd_card_save_key"), + SdCardNcaKey => write!(f, "sd_card_nca_key"), + } + } +} + +#[derive(Default, Debug)] pub struct Keys { - #[get = "pub"] secure_boot_key: Option, - #[get = "pub"] tsec_key: Option, - #[get = "pub"] device_key: Option, - #[get = "pub"] keyblob_keys: [Option; 0x20], - #[get = "pub"] keyblob_mac_keys: [Option; 0x20], - #[get = "pub"] encrypted_keyblobs: [Option; 0x20], - #[get = "pub"] mariko_aes_class_keys: [Option; 0xC], - #[get = "pub"] mariko_kek: Option, - #[get = "pub"] mariko_bek: Option, - #[get = "pub"] keyblobs: [Option; 0x20], - #[get = "pub"] keyblob_key_sources: [Option; 0x20], - #[get = "pub"] keyblob_mac_key_source: Option, - #[get = "pub"] tsec_root_kek: Option, - #[get = "pub"] package1_mac_kek: Option, - #[get = "pub"] package1_kek: Option, - #[get = "pub"] tsec_auth_signatures: [Option; 0x20], - #[get = "pub"] tsec_root_key: [Option; 0x20], - #[get = "pub"] master_kek_sources: [Option; 0x20], - #[get = "pub"] mariko_master_kek_sources: [Option; 0x20], - #[get = "pub"] master_keks: [Option; 0x20], - #[get = "pub"] master_key_source: Option, - #[get = "pub"] master_keys: [Option; 0x20], - #[get = "pub"] package1_mac_keys: [Option; 0x20], - #[get = "pub"] package1_keys: [Option; 0x20], - #[get = "pub"] package2_keys: [Option; 0x20], - #[get = "pub"] package2_key_source: Option, - #[get = "pub"] per_console_key_source: Option, - #[get = "pub"] aes_kek_generation_source: Option, - #[get = "pub"] aes_key_generation_source: Option, - #[get = "pub"] key_area_key_application_source: Option, - #[get = "pub"] key_area_key_ocean_source: Option, - #[get = "pub"] key_area_key_system_source: Option, - #[get = "pub"] titlekek_source: Option, - #[get = "pub"] header_kek_source: Option, - #[get = "pub"] sd_card_kek_source: Option, - #[get = "pub"] sd_card_save_key_source: Option, - #[get = "pub"] sd_card_nca_key_source: Option, - #[get = "pub"] save_mac_kek_source: Option, - #[get = "pub"] save_mac_key_source: Option, - #[get = "pub"] header_key_source: Option, - #[get = "pub"] header_key: Option, - #[get = "pub"] titlekeks: [Option; 0x20], - #[get = "pub"] key_area_key_application: [Option; 0x20], - #[get = "pub"] key_area_key_ocean: [Option; 0x20], - #[get = "pub"] key_area_key_system: [Option; 0x20], - #[get = "pub"] xci_header_key: Option, - #[get = "pub"] save_mac_key: Option, - #[get = "pub"] sd_card_save_key: Option, - #[get = "pub"] sd_card_nca_key: Option, #[allow(dead_code)] - #[get = "pub"] nca_hdr_fixed_key_modulus: [Option; 2], #[allow(dead_code)] - #[get = "pub"] acid_fixed_key_modulus: [Option; 2], #[allow(dead_code)] - #[get = "pub"] package2_fixed_key_modulus: Option, } +impl Keys { + pub fn get_key(&self, key_name: KeyName) -> Result { + use KeyName::*; + match key_name { + SecureBootKey => self.secure_boot_key, + TsecKey => self.tsec_key, + DeviceKey => self.device_key, + KeyblobKey(u8) => self.keyblob_keys[u8 as usize], + KeyblobMacKey(u8) => self.keyblob_mac_keys[u8 as usize], + MarikoAesClassKey(u8) => self.mariko_aes_class_keys[u8 as usize], + MarikoKek => self.mariko_kek, + MarikoBek => self.mariko_bek, + KeyblobKeySource(u8) => self.keyblob_key_sources[u8 as usize], + KeyblobMacKeySource => self.keyblob_mac_key_source, + TsecRootKek => self.tsec_root_kek, + Package1MacKek => self.package1_mac_kek, + TsecAuthSignature(u8) => self.tsec_auth_signatures[u8 as usize], + TsecRootKey(u8) => self.tsec_root_key[u8 as usize], + MasterKekSource(u8) => self.master_kek_sources[u8 as usize], + MarikoMasterKekSource(u8) => self.mariko_master_kek_sources[u8 as usize], + MasterKek(u8) => self.master_keks[u8 as usize], + MasterKeySource => self.master_key_source, + MasterKey(u8) => self.master_keys[u8 as usize], + Package1MacKey(u8) => self.package1_mac_keys[u8 as usize], + Package1Key(u8) => self.package1_keys[u8 as usize], + Package2Key(u8) => self.package2_keys[u8 as usize], + Package2KeySource => self.package2_key_source, + PerConsoleKeySource => self.per_console_key_source, + AesKekGenerationSource => self.aes_kek_generation_source, + AesKeyGenerationSource => self.aes_key_generation_source, + KeyAreaKeyApplicationSource => self.key_area_key_application_source, + KeyAreaKeyOceanSource => self.key_area_key_ocean_source, + KeyAreaKeySystemSource => self.key_area_key_system_source, + TitlekekSource => self.titlekek_source, + HeaderKekSource => self.header_kek_source, + SdCardKekSource => self.sd_card_kek_source, + SaveMacKekSource => self.save_mac_kek_source, + SaveMacKeySource => self.save_mac_key_source, + Titlekek(u8) => self.titlekeks[u8 as usize], + KeyAreaKeyApplication(u8) => self.key_area_key_application[u8 as usize], + KeyAreaKeyOcean(u8) => self.key_area_key_ocean[u8 as usize], + KeyAreaKeySystem(u8) => self.key_area_key_system[u8 as usize], + XciHeaderKey => self.xci_header_key, + SaveMacKey => self.save_mac_key, + SdCardSaveKeySource | SdCardNcaKeySource | HeaderKeySource | HeaderKey + | SdCardSaveKey | SdCardNcaKey => panic!("Attempt to get an XTS key as a normal key"), + } + .ok_or(Error::MissingKey { + key_name, + backtrace: Backtrace::generate(), + }) + } + + pub fn get_xts_key(&self, key_name: KeyName) -> Result { + use KeyName::*; + match key_name { + SdCardSaveKeySource => self.sd_card_save_key_source, + SdCardNcaKeySource => self.sd_card_nca_key_source, + HeaderKeySource => self.header_key_source, + HeaderKey => self.header_key, + SdCardSaveKey => self.sd_card_save_key, + SdCardNcaKey => self.sd_card_nca_key, + SecureBootKey + | TsecKey + | DeviceKey + | KeyblobKey(_) + | KeyblobMacKey(_) + | MarikoAesClassKey(_) + | MarikoKek + | MarikoBek + | KeyblobKeySource(_) + | KeyblobMacKeySource + | TsecRootKek + | Package1MacKek + | TsecAuthSignature(_) + | TsecRootKey(_) + | MasterKekSource(_) + | MarikoMasterKekSource(_) + | MasterKek(_) + | MasterKeySource + | MasterKey(_) + | Package1MacKey(_) + | Package1Key(_) + | Package2Key(_) + | Package2KeySource + | PerConsoleKeySource + | AesKekGenerationSource + | AesKeyGenerationSource + | KeyAreaKeyApplicationSource + | KeyAreaKeyOceanSource + | KeyAreaKeySystemSource + | TitlekekSource + | HeaderKekSource + | SdCardKekSource + | SaveMacKekSource + | SaveMacKeySource + | Titlekek(_) + | KeyAreaKeyApplication(_) + | KeyAreaKeyOcean(_) + | KeyAreaKeySystem(_) + | XciHeaderKey + | SaveMacKey => panic!("Attempt to get a normal key as an XTS key"), + } + .ok_or(Error::MissingKey { + key_name, + backtrace: Backtrace::generate(), + }) + } +} + macro_rules! make_key_macros_write { ($d:tt, $self:ident, $w:ident, $show_console_unique:expr, $minimal:expr) => { macro_rules! single_key { @@ -490,18 +702,16 @@ macro_rules! make_key_macros { ($d:tt, $self:ident, $section:ident) => { macro_rules! single_key { ($keyname:tt, $doc:expr, $console_unique:expr, [$d ($parent:expr),*]) => { - let mut key = [0; 0x10]; $self.$keyname.or_in( - key_to_aes($section, stringify!($keyname), &mut key)?.map(|()| Aes128Key(key)), + key_to_aes($section, stringify!($keyname))? ); }; } macro_rules! single_key_xts { ($keyname:tt, $doc:expr, $console_unique:expr, [$d ($parent:expr),*]) => { - let mut key = [0; 0x20]; $self.$keyname.or_in( - key_to_aes($section, stringify!($keyname), &mut key)?.map(|()| AesXtsKey(key)), + key_to_xts($section, stringify!($keyname))?, ); }; } @@ -509,14 +719,13 @@ macro_rules! make_key_macros { macro_rules! multi_key { ($keyname:tt, $doc:expr, $console_unique:expr, $idx:ident => $d ([$d ($parent:expr),*]),*) => { for (idx, v) in $self.$keyname.iter_mut().enumerate() { - let mut key = [0; 0x10]; // remove trailing s let mut name = String::from(stringify!($keyname)); if name.bytes().last() == Some(b's') { name.pop(); } v.or_in( - key_to_aes_array($section, &name, idx, &mut key)?.map(|()| Aes128Key(key)), + key_to_aes_array($section, &name, idx)?, ); } }; @@ -525,14 +734,13 @@ macro_rules! make_key_macros { macro_rules! multi_keyblob { ($keyname:tt, $doc:expr, $console_unique:expr) => { for (idx, v) in $self.$keyname.iter_mut().enumerate() { - let mut key = [0; 0x90]; // remove trailing s let mut name = String::from(stringify!($keyname)); if name.bytes().last() == Some(b's') { name.pop(); } v.or_in( - key_to_aes_array($section, &name, idx, &mut key)?.map(|()| Keyblob(key)), + key_to_keyblob_array($section, &name, idx)?, ); } }; @@ -541,15 +749,13 @@ macro_rules! make_key_macros { macro_rules! multi_encrypted_keyblob { ($keyname:tt, $doc:expr, $console_unique:expr) => { for (idx, v) in $self.$keyname.iter_mut().enumerate() { - let mut key = [0; 0xB0]; // remove trailing s let mut name = String::from(stringify!($keyname)); if name.bytes().last() == Some(b's') { name.pop(); } v.or_in( - key_to_aes_array($section, &name, idx, &mut key)? - .map(|()| EncryptedKeyblob(key)), + key_to_encrypted_keyblob_array($section, &name, idx)?, ); } }; From 3244ddb2281cc625a141e02b0edb648d4b02a360 Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Mon, 9 Jan 2023 02:55:38 +0300 Subject: [PATCH 4/6] Make the CTR decryption of sections work (at least seemingly...) --- Cargo.lock | 13 -- Cargo.toml | 1 - src/bin/linkle_clap.rs | 26 +++- src/error.rs | 2 + src/format/nca/crypto_stream.rs | 205 ++++++++++++++++++++++++++++++++ src/format/nca/mod.rs | 58 ++++++++- src/format/nca/structures.rs | 16 +-- src/pki.rs | 36 +++++- 8 files changed, 332 insertions(+), 25 deletions(-) create mode 100644 src/format/nca/crypto_stream.rs diff --git a/Cargo.lock b/Cargo.lock index d193fcb..7bbdac6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -420,18 +420,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getset" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "gimli" version = "0.27.0" @@ -565,7 +553,6 @@ dependencies = [ "digest", "dirs-next", "elf", - "getset", "goblin", "hex", "lz4", diff --git a/Cargo.toml b/Cargo.toml index f8d44ec..366ebc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,6 @@ blz-nx = "1.0" bit_field = "0.10" cargo-toml2 = { version = "1.3.2", optional = true } binrw = "0.10.0" -getset = "0.1.2" pretty-hex = "0.3.0" hex = "0.4.3" diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index f32ffb5..1068970 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -300,7 +300,31 @@ fn extract_nca( let keys = linkle::pki::Keys::new(key_path, is_dev)?; let title_key = title_key.map(linkle::pki::parse_title_key).transpose()?; let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?, title_key)?; - todo!() + // if let Some(output_header_json) = output_header_json { + // let mut output_header_json = File::create(output_header_json)?; + // nca.write_json(&mut output_header_json).unwrap(); + // } + if let Some(output_section0) = output_section0 { + let mut output_section0 = File::create(output_section0)?; + let mut section = nca.raw_section(0).unwrap(); + std::io::copy(&mut section, &mut output_section0)?; + } + if let Some(output_section1) = output_section1 { + let mut output_section1 = File::create(output_section1)?; + let mut section = nca.raw_section(1).unwrap(); + std::io::copy(&mut section, &mut output_section1)?; + } + if let Some(output_section2) = output_section2 { + let mut output_section2 = File::create(output_section2)?; + let mut section = nca.raw_section(2).unwrap(); + std::io::copy(&mut section, &mut output_section2)?; + } + if let Some(output_section3) = output_section3 { + let mut output_section3 = File::create(output_section3)?; + let mut section = nca.raw_section(3).unwrap(); + std::io::copy(&mut section, &mut output_section3)?; + } + Ok(()) } fn to_opt_ref>(s: &Option) -> Option<&U> { diff --git a/src/error.rs b/src/error.rs index 55be0b9..418c5e0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -74,6 +74,8 @@ pub enum Error { key_name: &'static str, backtrace: Backtrace, }, + #[snafu(display("Missing section {}.", index))] + MissingSection { index: usize, backtrace: Backtrace }, } impl Error { diff --git a/src/format/nca/crypto_stream.rs b/src/format/nca/crypto_stream.rs new file mode 100644 index 0000000..b53d19f --- /dev/null +++ b/src/format/nca/crypto_stream.rs @@ -0,0 +1,205 @@ +use crate::error::Error; +use crate::format::nca::{NcaCrypto, SectionJson}; +use crate::utils::align_down; +use byteorder::{ByteOrder, BE}; +use std::cmp::min; +use std::io; +use std::io::{Read, Seek, Write}; + +/// A wrapper around a Read/Seek stream, decrypting its contents based of an +/// NCA Section. +#[derive(Debug)] +pub struct CryptoStream { + pub(super) stream: R, + // Hello borrowck my old friend. We need to keep the state separate from the + // buffer, otherwise we get borrow problems. + pub(super) state: CryptoStreamState, + // Keep a 1-block large buffer of data in case of partial reads. + pub(super) buffer: [u8; 0x10], +} + +#[derive(Debug)] +pub struct CryptoStreamState { + pub(super) offset: u64, + pub(super) json: SectionJson, +} + +impl CryptoStream { + fn seek_aligned(&mut self, from: io::SeekFrom) -> io::Result<()> { + let new_offset = match from { + io::SeekFrom::Start(cur) => cur, + io::SeekFrom::Current(val) => (self.state.offset as i64 + val) as u64, + io::SeekFrom::End(val) => (self.state.json.size() as i64 + val) as u64, + }; + if new_offset % 16 != 0 { + panic!("Seek not aligned"); + } + self.stream.seek(io::SeekFrom::Start(new_offset))?; + self.state.offset = new_offset; + Ok(()) + } +} + +impl CryptoStreamState { + fn get_ctr(&self) -> [u8; 0x10] { + let offset = self.json.start_offset() / 16 + self.offset / 16; + let mut ctr = [0; 0x10]; + // Write section nounce in Big Endian. + BE::write_u64(&mut ctr[..8], self.json.nounce); + // Set ctr to offset / BLOCK_SIZE, in big endian. + BE::write_u64(&mut ctr[8..], offset); + ctr + } + + fn decrypt(&mut self, buf: &mut [u8]) -> Result<(), Error> { + match self.json.crypto { + NcaCrypto::None => { + // Nothing to do. + Ok(()) + } + NcaCrypto::Ctr(key) => key.decrypt_ctr(buf, &self.get_ctr()), + NcaCrypto::Bktr(_) => todo!(), + NcaCrypto::Xts(_) => todo!(), + } + } + + fn encrypt(&mut self, buf: &mut [u8]) -> Result<(), Error> { + match self.json.crypto { + NcaCrypto::None => { + // Nothing to do. + Ok(()) + } + NcaCrypto::Ctr(key) => key.encrypt_ctr(buf, &self.get_ctr()), + NcaCrypto::Bktr(_) => todo!(), + NcaCrypto::Xts(_) => todo!(), + } + } +} + +/// Read implementation for CryptoStream. +impl Read for CryptoStream { + fn read(&mut self, mut buf: &mut [u8]) -> io::Result { + let previous_leftovers = (self.state.offset % 16) as usize; + let previous_leftovers_read = if previous_leftovers != 0 { + // First, handle leftovers from a previous read call, so we go back + // to a properly block-aligned read. + let to = min(previous_leftovers + buf.len(), 16); + let size = to - previous_leftovers; + buf[..size].copy_from_slice(&self.buffer[previous_leftovers..to]); + self.state.offset += size as u64; + + buf = &mut buf[size..]; + size + } else { + 0 + }; + + let read = self.stream.read(buf)?; + buf = &mut buf[..read]; + + // Decrypt all the non-leftover bytes. + let len_no_leftovers = align_down(buf.len(), 16); + self.state.decrypt(&mut buf[..len_no_leftovers]).unwrap(); + self.state.offset += len_no_leftovers as u64; + let leftovers = buf.len() % 16; + if leftovers != 0 { + // We got some leftover, save them in the internal buffer, finish + // reading it, decrypt it, and copy the part we want back. + // + // Why not delay decryption until we have a full block? Well, that's + // because the read interface is **stupid**. If we ever return 0, + // the file is assumed to be finished - instead of signaling "herp, + // needs more bytes". So we play greedy. + let from = align_down(buf.len(), 16); + self.buffer[..leftovers].copy_from_slice(&buf[from..buf.len()]); + self.stream.read_exact(&mut self.buffer[leftovers..])?; + // TODO: Bubble up the error. + self.state.decrypt(&mut self.buffer).unwrap(); + buf[from..].copy_from_slice(&self.buffer[..leftovers]); + self.state.offset += leftovers as u64; + } + + Ok(previous_leftovers_read + read) + } +} + +impl Write for CryptoStream { + fn write(&mut self, mut buf: &[u8]) -> io::Result { + let previous_leftovers = (self.state.offset % 16) as usize; + let previous_leftovers_written = if previous_leftovers != 0 { + // We need to do two things: Rewrite the block on disk with the + // encrypted data, and update the leftover buffer with the decrypted + // data. + let to = min(previous_leftovers + buf.len(), 16); + let size = to - previous_leftovers; + self.buffer[previous_leftovers..to].copy_from_slice(&buf[..size]); + + // We are done handling this block. Write it to disk. + // TODO: Bubble up the error. + self.state.encrypt(&mut self.buffer).unwrap(); + self.stream.write_all(&self.buffer)?; + self.state.decrypt(&mut self.buffer).unwrap(); + + if to != 16 { + self.stream.seek(io::SeekFrom::Current(-16))?; + } else { + self.buffer = [0; 16]; + } + + self.state.offset += size as u64; + + buf = &buf[size..]; + size + } else { + 0 + }; + + // Encrypt chunk by chunk + for chunk in buf.chunks_exact(16) { + self.buffer.copy_from_slice(chunk); + self.state.encrypt(&mut self.buffer).unwrap(); + self.stream.write_all(&self.buffer)?; + self.state.offset += 16 + } + + // Store all leftover bytes. + let leftovers = buf.len() % 16; + if leftovers != 0 { + // We got some leftover, save them in the internal buffer so they can + // be processed in a subsequent write. Note that this will not work + // at all if you mix reads and writes... + let from = align_down(buf.len(), 16); + self.buffer = [0; 16]; + self.buffer[..leftovers].copy_from_slice(&buf[from..buf.len()]); + self.state.encrypt(&mut self.buffer).unwrap(); + self.stream.write_all(&self.buffer)?; + self.state.decrypt(&mut self.buffer).unwrap(); + self.stream.seek(io::SeekFrom::Current(-16))?; + self.state.offset += leftovers as u64; + } + + Ok(previous_leftovers + buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl Seek for CryptoStream { + fn seek(&mut self, from: io::SeekFrom) -> io::Result { + self.state.offset = match from { + io::SeekFrom::Start(cur) => cur, + io::SeekFrom::Current(val) => (self.state.offset as i64 + val) as u64, + io::SeekFrom::End(val) => (self.state.json.size() as i64 + val) as u64, + }; + + let aligned_offset = align_down(self.state.offset, 16); + self.stream.seek(io::SeekFrom::Start(aligned_offset))?; + if self.state.offset % 16 != 0 { + self.stream.read_exact(&mut self.buffer)?; + self.state.decrypt(&mut self.buffer).unwrap(); + } + Ok(self.state.offset) + } +} diff --git a/src/format/nca/mod.rs b/src/format/nca/mod.rs index f718fc1..edd7b7b 100644 --- a/src/format/nca/mod.rs +++ b/src/format/nca/mod.rs @@ -29,10 +29,13 @@ use binrw::BinRead; use serde_derive::{Deserialize, Serialize}; use snafu::{Backtrace, GenerateImplicitData}; use std::cmp::max; -use std::io::Read; +use std::io::{Read, Seek}; +mod crypto_stream; mod structures; +use crate::format::nca::crypto_stream::{CryptoStream, CryptoStreamState}; +use crate::utils::{ReadRange, TryClone}; pub use structures::RightsId; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -64,6 +67,15 @@ pub struct SectionJson { nounce: u64, } +impl SectionJson { + fn start_offset(&self) -> u64 { + self.media_start_offset as u64 * 0x200 + } + fn size(&self) -> u64 { + (self.media_end_offset - self.media_start_offset) as u64 * 0x200 + } +} + #[derive(Serialize, Deserialize, Clone, Copy)] #[repr(transparent)] pub struct TitleId(u64); @@ -290,3 +302,47 @@ impl Nca { Ok(nca) } } + +impl Nca { + pub fn raw_section(&self, id: usize) -> Result>, Error> { + if let Some(section) = &self.json.sections[id] { + // TODO: Nca::raw_section should reopen the file, not dup2 the handle. + // (why though?) + let mut stream = self.stream.try_clone()?; + stream.seek(std::io::SeekFrom::Start(section.start_offset()))?; + + Ok(CryptoStream { + stream: ReadRange::new(stream, section.start_offset(), section.size()), + // Keep a 1-block large buffer of data in case of partial reads. + buffer: [0; 0x10], + state: CryptoStreamState { + json: section.clone(), + offset: 0, // the offset is relative to the start of the section + }, + }) + } else { + Err(Error::MissingSection { + index: id, + backtrace: Backtrace::generate(), + }) + } + } + + // pub fn section( + // &self, + // id: usize, + // ) -> Result>>, Error> { + // let mut raw_section = self.raw_section(id)?; + // let (start_offset, size) = match raw_section.state.json.fstype { + // FsType::Pfs0 { + // pfs0_offset, + // pfs0_size, + // .. + // } => (pfs0_offset, pfs0_size), + // _ => (0, raw_section.state.json.size()), + // }; + // raw_section.seek_aligned(io::SeekFrom::Start(start_offset)); + // let json = raw_section.state.json.clone(); + // Ok(VerificationStream::new(raw_section, json)) + // } +} diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs index f4dffa7..5e36f2c 100644 --- a/src/format/nca/structures.rs +++ b/src/format/nca/structures.rs @@ -22,7 +22,7 @@ impl_debug_deserialize_serialize_hexstring!(SigDebug); const XTS_SECTOR_SIZE: usize = 0x200; #[derive(Debug, Clone, Copy)] -pub struct XtsCryptSector(pub T); +pub struct XtsCryptSector(pub T); impl Deref for XtsCryptSector { type Target = T; @@ -38,7 +38,7 @@ pub struct XtsCryptArgs { pub sector: usize, } -impl, const Size: usize> BinRead for XtsCryptSector { +impl, const SIZE: usize> BinRead for XtsCryptSector { type Args = XtsCryptArgs; fn read_options( @@ -46,11 +46,11 @@ impl, const Size: usize> BinRead for XtsCryptSector BinResult { - let mut buf = [0u8; Size]; + let mut buf = [0u8; SIZE]; reader.read_exact(&mut buf)?; args.key - .decrypt(&mut buf, args.sector, Size) + .decrypt(&mut buf, args.sector, SIZE) .map_err(|e| binrw::Error::Custom { pos: 0, err: Box::new(e), @@ -61,7 +61,7 @@ impl, const Size: usize> BinRead for XtsCryptSector, const Size: usize> BinWrite for XtsCryptSector { +impl, const SIZE: usize> BinWrite for XtsCryptSector { type Args = XtsCryptArgs; fn write_options( @@ -70,16 +70,16 @@ impl, const Size: usize> BinWrite for XtsCryptSector BinResult<()> { - let mut buf = [0u8; Size]; + let mut buf = [0u8; SIZE]; let mut buf = std::io::Cursor::new(&mut buf[..]); self.0.write_options(&mut buf, options, ())?; - assert_eq!(buf.position() as usize, Size, "Buffer not fully written"); + assert_eq!(buf.position() as usize, SIZE, "Buffer not fully written"); let buf = buf.into_inner(); args.key - .encrypt(buf, args.sector, Size) + .encrypt(buf, args.sector, SIZE) .map_err(|e| binrw::Error::Custom { pos: 0, err: Box::new(e), diff --git a/src/pki.rs b/src/pki.rs index 21d526d..a534729 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -6,7 +6,6 @@ use cipher::{ }; use cmac::{Cmac, Mac}; use ctr::Ctr128BE; -use getset::Getters; use hex::FromHexError; use ini::{self, Properties}; use snafu::{Backtrace, GenerateImplicitData}; @@ -109,6 +108,41 @@ impl Aes128Key { Ok(AesXtsKey(newkey)) } + + /// Decrypt blocks in CTR mode. + pub fn decrypt_ctr(&self, buf: &mut [u8], ctr: &[u8; 0x10]) -> Result<(), Error> { + if buf.len() % 16 != 0 { + return Err(Error::Crypto { + error: String::from( + "buf length should be a multiple of 16, the size of an AES block.", + ), + backtrace: Backtrace::generate(), + }); + } + + let key = GenericArray::from_slice(&self.0); + let iv = GenericArray::from_slice(ctr); + let mut crypter = as KeyIvInit>::new(key, iv); + crypter.apply_keystream(buf); + Ok(()) + } + + pub fn encrypt_ctr(&self, buf: &mut [u8], ctr: &[u8; 0x10]) -> Result<(), Error> { + if buf.len() % 16 != 0 { + return Err(Error::Crypto { + error: String::from( + "buf length should be a multiple of 16, the size of an AES block.", + ), + backtrace: Backtrace::generate(), + }); + } + + let key = GenericArray::from_slice(&self.0); + let iv = GenericArray::from_slice(ctr); + let mut crypter = as KeyIvInit>::new(key, iv); + crypter.apply_keystream(buf); + Ok(()) + } } fn get_tweak(mut sector: usize) -> [u8; 0x10] { From 1295f46adf1be172e62ae7403b9e2ef76ea7f7e4 Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Mon, 9 Jan 2023 14:30:35 +0300 Subject: [PATCH 5/6] Rename some stuff, add header json saving --- src/bin/linkle_clap.rs | 8 ++--- src/format/nca/crypto_stream.rs | 8 ++--- src/format/nca/mod.rs | 62 +++++++++------------------------ 3 files changed, 25 insertions(+), 53 deletions(-) diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 1068970..5e94578 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -300,10 +300,10 @@ fn extract_nca( let keys = linkle::pki::Keys::new(key_path, is_dev)?; let title_key = title_key.map(linkle::pki::parse_title_key).transpose()?; let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?, title_key)?; - // if let Some(output_header_json) = output_header_json { - // let mut output_header_json = File::create(output_header_json)?; - // nca.write_json(&mut output_header_json).unwrap(); - // } + if let Some(output_header_json) = output_header_json { + let mut output_header_json = File::create(output_header_json)?; + serde_json::to_writer_pretty(&mut output_header_json, &nca.header())?; + } if let Some(output_section0) = output_section0 { let mut output_section0 = File::create(output_section0)?; let mut section = nca.raw_section(0).unwrap(); diff --git a/src/format/nca/crypto_stream.rs b/src/format/nca/crypto_stream.rs index b53d19f..1c2c32d 100644 --- a/src/format/nca/crypto_stream.rs +++ b/src/format/nca/crypto_stream.rs @@ -1,5 +1,5 @@ use crate::error::Error; -use crate::format::nca::{NcaCrypto, SectionJson}; +use crate::format::nca::{NcaCrypto, NcaSectionHeader}; use crate::utils::align_down; use byteorder::{ByteOrder, BE}; use std::cmp::min; @@ -21,7 +21,7 @@ pub struct CryptoStream { #[derive(Debug)] pub struct CryptoStreamState { pub(super) offset: u64, - pub(super) json: SectionJson, + pub(super) json: NcaSectionHeader, } impl CryptoStream { @@ -44,8 +44,8 @@ impl CryptoStreamState { fn get_ctr(&self) -> [u8; 0x10] { let offset = self.json.start_offset() / 16 + self.offset / 16; let mut ctr = [0; 0x10]; - // Write section nounce in Big Endian. - BE::write_u64(&mut ctr[..8], self.json.nounce); + // Write section nonce in Big Endian. + BE::write_u64(&mut ctr[..8], self.json.nonce); // Set ctr to offset / BLOCK_SIZE, in big endian. BE::write_u64(&mut ctr[8..], offset); ctr diff --git a/src/format/nca/mod.rs b/src/format/nca/mod.rs index edd7b7b..1e0490a 100644 --- a/src/format/nca/mod.rs +++ b/src/format/nca/mod.rs @@ -29,7 +29,7 @@ use binrw::BinRead; use serde_derive::{Deserialize, Serialize}; use snafu::{Backtrace, GenerateImplicitData}; use std::cmp::max; -use std::io::{Read, Seek}; +use std::io::{Read, Seek, Write}; mod crypto_stream; mod structures; @@ -59,15 +59,15 @@ enum NcaFormat { } #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct SectionJson { +pub struct NcaSectionHeader { media_start_offset: u32, media_end_offset: u32, crypto: NcaCrypto, fstype: FsType, - nounce: u64, + nonce: u64, } -impl SectionJson { +impl NcaSectionHeader { fn start_offset(&self) -> u64 { self.media_start_offset as u64 * 0x200 } @@ -87,7 +87,7 @@ impl std::fmt::Debug for TitleId { } #[derive(Debug, Serialize, Deserialize)] -pub struct NcaJson { +pub struct NcaHeader { format: NcaFormat, sig: structures::SigDebug, npdm_sig: structures::SigDebug, @@ -99,13 +99,13 @@ pub struct NcaJson { title_id: TitleId, sdk_version: u32, // TODO: Better format rights_id: RightsId, - sections: [Option; 4], + sections: [Option; 4], } #[derive(Debug)] pub struct Nca { stream: R, - json: NcaJson, + info: NcaHeader, } fn get_key_area_key(pki: &Keys, key_version: u8, key_type: KeyType) -> Result { @@ -127,46 +127,12 @@ fn get_master_key_revision(crypto_type: u8, crypto_type2: u8) -> u8 { fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { // Decrypt header. let mut header = [0; 0xC00]; - // let mut decrypted_header = [0; 0xC00]; file.read_exact(&mut header)?; // TODO: Check if NCA is already decrypted let header_key = pki.get_xts_key(KeyName::HeaderKey)?; - // decrypted_header[..0x400].copy_from_slice(&header[..0x400]); - // header_key.decrypt(&mut decrypted_header[..0x400], 0, 0x200)?; - // - // // skip 2 signature blocks - // let magic = &decrypted_header[0x200..][..4]; - // match magic { - // b"NCA3" => { - // decrypted_header.copy_from_slice(&header); - // header_key.decrypt(&mut decrypted_header, 0, 0x200)?; - // } - // b"NCA2" => { - // todo!() - // // for (i, fsheader) in raw_nca.fs_headers.iter().enumerate() { - // // let offset = 0x400 + i * 0x200; - // // if &fsheader._0x148[..] != &[0; 0xB8][..] { - // // decrypted_header[offset..offset + 0x200] - // // .copy_from_slice(&header[offset..offset + 0x200]); - // // header_key.decrypt(&mut decrypted_header[offset..offset + 0x200], 0, 0x200)?; - // // } else { - // // decrypted_header[offset..offset + 0x200].copy_from_slice(&[0; 0x200]); - // // } - // // } - // } - // b"NCA0" => unimplemented!("NCA0 parsing is not implemented yet"), - // _ => { - // return Err(Error::NcaParse { - // key_name: "header_key", - // backtrace: Backtrace::generate(), - // }) - // } - // } - - // println!("{}", pretty_hex::pretty_hex(&decrypted_header)); let mut raw_nca = std::io::Cursor::new(header); let raw_nca = @@ -259,7 +225,7 @@ impl Nca { } }; - sections[idx] = Some(SectionJson { + sections[idx] = Some(NcaSectionHeader { crypto, fstype: match fs.superblock { RawSuperblock::Pfs0(s) => FsType::Pfs0 { @@ -273,7 +239,7 @@ impl Nca { RawSuperblock::RomFs(_) => FsType::RomFs, _ => panic!("Unknown superblock type"), }, - nounce: fs.section_ctr, + nonce: fs.section_ctr, media_start_offset: section.media_start_offset, media_end_offset: section.media_end_offset, }); @@ -282,7 +248,7 @@ impl Nca { let nca = Nca { stream: file, - json: NcaJson { + info: NcaHeader { format, sig: sigs.fixed_key_sig, npdm_sig: sigs.npdm_sig, @@ -305,7 +271,7 @@ impl Nca { impl Nca { pub fn raw_section(&self, id: usize) -> Result>, Error> { - if let Some(section) = &self.json.sections[id] { + if let Some(section) = &self.info.sections[id] { // TODO: Nca::raw_section should reopen the file, not dup2 the handle. // (why though?) let mut stream = self.stream.try_clone()?; @@ -346,3 +312,9 @@ impl Nca { // Ok(VerificationStream::new(raw_section, json)) // } } + +impl Nca { + pub fn header(&self) -> &NcaHeader { + &self.info + } +} From 01b1d05c3766b9acbe2193783c3a7d78dafe1a3c Mon Sep 17 00:00:00 2001 From: Nikita Strygin Date: Mon, 9 Jan 2023 17:57:15 +0300 Subject: [PATCH 6/6] Clean the API up a bit, add some docs --- Cargo.lock | 1 - Cargo.toml | 3 +- src/bin/linkle_clap.rs | 2 +- src/format/nacp.rs | 2 +- src/format/nca/crypto_stream.rs | 10 +- src/format/nca/mod.rs | 200 +++++++++++++------------------- src/format/nca/structures.rs | 183 ++++++++++++++++++++--------- src/format/npdm.rs | 2 +- src/format/nxo.rs | 2 +- 9 files changed, 216 insertions(+), 189 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bbdac6..4d499e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -562,7 +562,6 @@ dependencies = [ "scroll", "semver", "serde", - "serde_derive", "serde_json", "sha2", "snafu", diff --git a/Cargo.toml b/Cargo.toml index 366ebc7..f807e5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,7 @@ clap = { version = "4.0.32", optional = true, features = ["cargo"] } structopt = { version = "0.3", optional = true } sha2 = "0.10.6" scroll = { version = "0.11.0", optional = true } -serde = "1" -serde_derive = "1" +serde = { version = "1", features = ["derive"] } serde_json = "1" cargo_metadata = { version = "0.15.2", optional = true } semver = { version = "1.0.16", optional = true } diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 5e94578..b605a57 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -302,7 +302,7 @@ fn extract_nca( let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?, title_key)?; if let Some(output_header_json) = output_header_json { let mut output_header_json = File::create(output_header_json)?; - serde_json::to_writer_pretty(&mut output_header_json, &nca.header())?; + serde_json::to_writer_pretty(&mut output_header_json, &nca.info())?; } if let Some(output_section0) = output_section0 { let mut output_section0 = File::create(output_section0)?; diff --git a/src/format/nacp.rs b/src/format/nacp.rs index 012cb34..cdabd55 100644 --- a/src/format/nacp.rs +++ b/src/format/nacp.rs @@ -1,6 +1,6 @@ use crate::format::utils; use byteorder::{LittleEndian, WriteBytesExt}; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::Write; diff --git a/src/format/nca/crypto_stream.rs b/src/format/nca/crypto_stream.rs index 1c2c32d..2fb6976 100644 --- a/src/format/nca/crypto_stream.rs +++ b/src/format/nca/crypto_stream.rs @@ -1,5 +1,5 @@ use crate::error::Error; -use crate::format::nca::{NcaCrypto, NcaSectionHeader}; +use crate::format::nca::{NcaCrypto, NcaSectionInfo}; use crate::utils::align_down; use byteorder::{ByteOrder, BE}; use std::cmp::min; @@ -21,11 +21,11 @@ pub struct CryptoStream { #[derive(Debug)] pub struct CryptoStreamState { pub(super) offset: u64, - pub(super) json: NcaSectionHeader, + pub(super) json: NcaSectionInfo, } impl CryptoStream { - fn seek_aligned(&mut self, from: io::SeekFrom) -> io::Result<()> { + pub fn seek_aligned(&mut self, from: io::SeekFrom) -> io::Result<()> { let new_offset = match from { io::SeekFrom::Start(cur) => cur, io::SeekFrom::Current(val) => (self.state.offset as i64 + val) as u64, @@ -42,7 +42,7 @@ impl CryptoStream { impl CryptoStreamState { fn get_ctr(&self) -> [u8; 0x10] { - let offset = self.json.start_offset() / 16 + self.offset / 16; + let offset = self.json.media_start_offset / 16 + self.offset / 16; let mut ctr = [0; 0x10]; // Write section nonce in Big Endian. BE::write_u64(&mut ctr[..8], self.json.nonce); @@ -178,7 +178,7 @@ impl Write for CryptoStream { self.state.offset += leftovers as u64; } - Ok(previous_leftovers + buf.len()) + Ok(previous_leftovers_written + buf.len()) } fn flush(&mut self) -> io::Result<()> { diff --git a/src/format/nca/mod.rs b/src/format/nca/mod.rs index 1e0490a..c9ae578 100644 --- a/src/format/nca/mod.rs +++ b/src/format/nca/mod.rs @@ -5,107 +5,96 @@ //! the Horizon/NX OS are stored in this container, as it guarantees its //! authenticity, preventing tampering. //! +//! NCAs consist of up to 4 sections, each containing some kind of file system. +//! +//! Generally, you can find three types of sections: +//! - PartitionFs (aka pfs0) - A file system used mostly to contain exefs and metadata +//! - RomFs - A file system used to contain game assets +//! - RomFs patch - used to patch the RomFs when distributing updates +//! //! For more information about the NCA file format, see the [switchbrew page]. //! //! In order to parse an NCA, you may use the `from_file` method: //! //! ``` -//! # fn get_nca_file() -> std::io::Result { -//! # std::fs::File::open("tests/fixtures/test.nca") -//! # } -//! let f = get_nca_file()?; -//! let nca = Nca::from_file(nca)?; -//! let section = nca.section(0); +//! use std::fs::File; +//! use linkle::format::nca::Nca; +//! use linkle::pki::Keys; +//! +//! let pki = Keys::new(None, false)?; +//! let f = File::open("tests/fixtures/test.nca")?; +//! let nca = Nca::from_file(&pki, nca, None)?; +//! let section = nca.raw_section(0); //! ``` //! +//! Writing NCA files is not yet implemented. +//! //! [switchbrew page]: https://switchbrew.org/w/index.php?title=NCA_Format use crate::error::Error; -use crate::format::nca::structures::{ - ContentType, CryptoType, Hash, KeyType, NcaMagic, RawNca, RawSuperblock, -}; +use crate::format::nca::structures::{ContentType, CryptoType, KeyType, RawNca}; use crate::pki::{Aes128Key, AesXtsKey, KeyName, Keys, TitleKey}; use binrw::BinRead; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use snafu::{Backtrace, GenerateImplicitData}; use std::cmp::max; -use std::io::{Read, Seek, Write}; +use std::io::{Read, Seek}; mod crypto_stream; mod structures; -use crate::format::nca::crypto_stream::{CryptoStream, CryptoStreamState}; +pub use crate::format::nca::crypto_stream::CryptoStream; +use crate::format::nca::crypto_stream::CryptoStreamState; use crate::utils::{ReadRange, TryClone}; -pub use structures::RightsId; - -#[derive(Debug, Serialize, Deserialize, Clone)] -enum FsType { - Pfs0 { - master_hash: Hash, - block_size: u32, - hash_table_offset: u64, - hash_table_size: u64, - pfs0_offset: u64, - pfs0_size: u64, - }, - RomFs, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -enum NcaFormat { - Nca3, - Nca2, - Nca0, -} +pub use structures::{ + BktrSuperblock, NcaMagic, Pfs0Superblock, RightsId, RomfsSuperblock, SdkVersion, SigDebug, + Superblock, TitleId, +}; +/// Contains information about NCA section collected from the header. #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct NcaSectionHeader { - media_start_offset: u32, - media_end_offset: u32, - crypto: NcaCrypto, - fstype: FsType, - nonce: u64, +pub struct NcaSectionInfo { + /// Offset of the section in the NCA file. + pub media_start_offset: u64, + /// Offset of the end of the section in the NCA file. + pub media_end_offset: u64, + /// Cryptographic algorithm & key used to decrypt the section. + pub crypto: NcaCrypto, + /// Nonce used to decrypt the section. + pub nonce: u64, + /// Superblock of the section filesystem. + pub superblock: Superblock, } -impl NcaSectionHeader { - fn start_offset(&self) -> u64 { - self.media_start_offset as u64 * 0x200 - } +impl NcaSectionInfo { + /// Get size of the section. fn size(&self) -> u64 { - (self.media_end_offset - self.media_start_offset) as u64 * 0x200 - } -} - -#[derive(Serialize, Deserialize, Clone, Copy)] -#[repr(transparent)] -pub struct TitleId(u64); - -impl std::fmt::Debug for TitleId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:016x}", self.0) + self.media_end_offset - self.media_start_offset } } +/// Contains information about NCA collected from the header. #[derive(Debug, Serialize, Deserialize)] -pub struct NcaHeader { - format: NcaFormat, - sig: structures::SigDebug, - npdm_sig: structures::SigDebug, - is_gamecard: bool, - content_type: ContentType, - key_revision: u8, - key_type: KeyType, - nca_size: u64, - title_id: TitleId, - sdk_version: u32, // TODO: Better format - rights_id: RightsId, - sections: [Option; 4], +pub struct NcaInfo { + pub format: NcaMagic, + pub sig: SigDebug, + pub npdm_sig: SigDebug, + pub is_gamecard: bool, + pub content_type: ContentType, + pub key_revision: u8, + pub key_type: KeyType, + pub nca_size: u64, + pub title_id: TitleId, + pub sdk_version: SdkVersion, + pub rights_id: RightsId, + pub sections: [Option; 4], } +/// Represents an open NCA file available for reading. #[derive(Debug)] pub struct Nca { stream: R, - info: NcaHeader, + info: NcaInfo, } fn get_key_area_key(pki: &Keys, key_version: u8, key_type: KeyType) -> Result { @@ -130,7 +119,7 @@ fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { file.read_exact(&mut header)?; - // TODO: Check if NCA is already decrypted + // NOTE: no support for decrypted NCAs let header_key = pki.get_xts_key(KeyName::HeaderKey)?; @@ -141,10 +130,14 @@ fn decrypt_header(pki: &Keys, file: &mut dyn Read) -> Result { } #[derive(Debug, Serialize, Deserialize, Clone)] -enum NcaCrypto { +pub enum NcaCrypto { + /// No ecryption, the section is in plaintext. None, + /// AES-128-CTR encryption Ctr(Aes128Key), + /// Special variation of AES-128-CTR used for RomFs patching in updates Bktr(Aes128Key), + /// AES-128-XTS encryption (TODO: where is it used?) Xts(AesXtsKey), } @@ -159,12 +152,6 @@ impl Nca { header, fs_headers, } = decrypt_header(pki, &mut file)?; - let format = match &header.magic { - NcaMagic::Nca3 => NcaFormat::Nca3, - NcaMagic::Nca2 => NcaFormat::Nca2, - NcaMagic::Nca0 => NcaFormat::Nca0, - _ => unreachable!(), - }; // TODO: NCA: Verify header with RSA2048 PSS // BODY: We want to make sure the NCAs have a valid signature before @@ -207,14 +194,12 @@ impl Nca { { // Check if section is present if let Some(fs) = fs { - assert_eq!(fs.version, 2, "Invalid NCA FS Header version"); - let crypto = if has_rights_id { match fs.crypt_type { CryptoType::Ctr => NcaCrypto::Ctr(title_key.unwrap()), CryptoType::Bktr => NcaCrypto::Bktr(title_key.unwrap()), CryptoType::None => NcaCrypto::None, - CryptoType::Xts => unreachable!(), + CryptoType::Xts => unreachable!("Xts is not supported for RightsId crypto"), } } else { match fs.crypt_type { @@ -225,40 +210,28 @@ impl Nca { } }; - sections[idx] = Some(NcaSectionHeader { + sections[idx] = Some(NcaSectionInfo { crypto, - fstype: match fs.superblock { - RawSuperblock::Pfs0(s) => FsType::Pfs0 { - master_hash: s.master_hash, - block_size: s.block_size, - hash_table_offset: s.hash_table_offset, - hash_table_size: s.hash_table_size, - pfs0_offset: s.pfs0_offset, - pfs0_size: s.pfs0_size, - }, - RawSuperblock::RomFs(_) => FsType::RomFs, - _ => panic!("Unknown superblock type"), - }, + superblock: fs.superblock, nonce: fs.section_ctr, - media_start_offset: section.media_start_offset, - media_end_offset: section.media_end_offset, + media_start_offset: section.media_start_offset as u64 * 0x200, + media_end_offset: section.media_end_offset as u64 * 0x200, }); } } let nca = Nca { stream: file, - info: NcaHeader { - format, + info: NcaInfo { + format: header.magic, sig: sigs.fixed_key_sig, npdm_sig: sigs.npdm_sig, - is_gamecard: header.is_gamecard != 0, + is_gamecard: header.is_gamecard, content_type: header.content_type, key_revision: master_key_revision, key_type: header.key_type, nca_size: header.nca_size, - title_id: TitleId(header.title_id), - // TODO: Store the SDK version in a more human readable format. + title_id: header.title_id, sdk_version: header.sdk_version, rights_id: header.rights_id, sections, @@ -270,15 +243,20 @@ impl Nca { } impl Nca { + /// Get access to raw reader for a specific section. + /// + /// Note: this provides access to the raw NCA section data, doing just the decryption. + /// It does not perform any hash verification, or any other checks + /// (as these are dependent on the FS inside the section). pub fn raw_section(&self, id: usize) -> Result>, Error> { if let Some(section) = &self.info.sections[id] { // TODO: Nca::raw_section should reopen the file, not dup2 the handle. // (why though?) let mut stream = self.stream.try_clone()?; - stream.seek(std::io::SeekFrom::Start(section.start_offset()))?; + stream.seek(std::io::SeekFrom::Start(section.media_start_offset))?; Ok(CryptoStream { - stream: ReadRange::new(stream, section.start_offset(), section.size()), + stream: ReadRange::new(stream, section.media_start_offset, section.size()), // Keep a 1-block large buffer of data in case of partial reads. buffer: [0; 0x10], state: CryptoStreamState { @@ -293,28 +271,10 @@ impl Nca { }) } } - - // pub fn section( - // &self, - // id: usize, - // ) -> Result>>, Error> { - // let mut raw_section = self.raw_section(id)?; - // let (start_offset, size) = match raw_section.state.json.fstype { - // FsType::Pfs0 { - // pfs0_offset, - // pfs0_size, - // .. - // } => (pfs0_offset, pfs0_size), - // _ => (0, raw_section.state.json.size()), - // }; - // raw_section.seek_aligned(io::SeekFrom::Start(start_offset)); - // let json = raw_section.state.json.clone(); - // Ok(VerificationStream::new(raw_section, json)) - // } } impl Nca { - pub fn header(&self) -> &NcaHeader { + pub fn info(&self) -> &NcaInfo { &self.info } } diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs index 5e36f2c..a657395 100644 --- a/src/format/nca/structures.rs +++ b/src/format/nca/structures.rs @@ -1,15 +1,15 @@ //! Raw NCA structures //! -//! Those are used by the NCA parsing code, basically casting the byte slices -//! to those types through the plain crate. This only works on Little Endian -//! hosts! Ideally, we would have a derive macro that generates an -//! appropriate parser based on the machine's endianness. +//! Those are used by the NCA parsing code, some of them exposed to the user. +//! +//! The parsing is implemented declaratively using `binrw` use crate::impl_debug_deserialize_serialize_hexstring; use crate::pki::AesXtsKey; -use binrw::{BinRead, BinResult, BinWrite, BinrwNamedArgs, ReadOptions}; -use serde_derive::{Deserialize, Serialize}; +use binrw::{BinRead, BinResult, BinWrite, BinrwNamedArgs, ReadOptions, WriteOptions}; +use serde::{Deserialize, Serialize}; use std::fmt; +use std::fmt::Debug; use std::io::{Read, Seek, Write}; use std::ops::Deref; @@ -89,20 +89,75 @@ impl, const SIZE: usize> BinWrite for XtsCryptSector + BinWrite + 'static>(pub T); +pub struct Hash([u8; 0x20]); +impl_debug_deserialize_serialize_hexstring!(Hash); -impl + BinWrite + 'static> fmt::Debug for SkipDebug { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "SkipDebug")?; - Ok(()) +#[repr(transparent)] +#[derive(Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] +pub struct TitleId(u64); + +impl Debug for TitleId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:016x}", self.0) } } -#[repr(transparent)] #[derive(Clone, Copy, BinRead, BinWrite)] -pub struct Hash([u8; 0x20]); -impl_debug_deserialize_serialize_hexstring!(Hash); +pub struct RightsId(pub [u8; 0x10]); + +impl RightsId { + pub fn is_empty(&self) -> bool { + self.0.iter().all(|&b| b == 0) + } +} + +impl_debug_deserialize_serialize_hexstring!(RightsId); + +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct SdkVersion { + pub revision: u8, + pub micro: u8, + pub minor: u8, + pub major: u8, +} + +impl Debug for SdkVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // it's in little endian, so the major version is the last byte + write!( + f, + "{}.{}.{}.{}", + self.major, self.minor, self.micro, self.revision + ) + } +} + +impl Serialize for SdkVersion { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&format!("{:?}", self)) + } +} + +impl<'de> Deserialize<'de> for SdkVersion { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + let mut parts = s.split('.'); + // TODO: make this fallible + let major = parts.next().unwrap().parse::().unwrap(); + let minor = parts.next().unwrap().parse::().unwrap(); + let micro = parts.next().unwrap().parse::().unwrap(); + let revision = parts.next().unwrap().parse::().unwrap(); + + Ok(SdkVersion { + revision, + micro, + minor, + major, + }) + } +} #[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] #[brw(repr = u8)] @@ -141,37 +196,44 @@ pub struct NcaSigs { pub npdm_sig: SigDebug, } -#[derive(Clone, Copy, BinRead, BinWrite)] -pub struct RightsId(pub [u8; 0x10]); - -impl RightsId { - pub fn is_empty(&self) -> bool { - self.0.iter().all(|&b| b == 0) - } -} - -impl_debug_deserialize_serialize_hexstring!(RightsId); - #[derive(Debug, Clone, Copy, BinRead, BinWrite)] pub struct RawNcaHeader { pub magic: NcaMagic, - pub is_gamecard: u8, + #[br(parse_with = read_bool)] + #[bw(write_with = write_bool)] + pub is_gamecard: bool, pub content_type: ContentType, pub crypto_type: u8, pub key_type: KeyType, pub nca_size: u64, - pub title_id: u64, - pub _padding0: SkipDebug, - pub sdk_version: u32, + #[brw(pad_after = 4)] + pub title_id: TitleId, + pub sdk_version: SdkVersion, pub crypto_type2: u8, - pub _padding1: SkipDebug<[u8; 0xF]>, + #[brw(pad_after = 0xf)] pub rights_id: RightsId, pub section_entries: [RawSectionTableEntry; 4], pub section_hashes: [Hash; 4], pub encrypted_xts_key: [u8; 0x20], pub encrypted_ctr_key: [u8; 0x10], + #[brw(pad_after = 0xc0)] pub unknown_new_key: [u8; 0x10], - pub _padding2: SkipDebug<[u8; 0xC0]>, +} + +fn read_bool(reader: &mut R, _options: &ReadOptions, _args: ()) -> BinResult { + let mut buf = [0u8; 1]; + reader.read_exact(&mut buf)?; + Ok(buf[0] != 0) +} + +fn write_bool( + value: &bool, + writer: &mut W, + _options: &WriteOptions, + _args: (), +) -> BinResult<()> { + writer.write_all(&[u8::from(*value)])?; + Ok(()) } #[derive(Debug, Clone, Copy, BinRead, BinWrite)] @@ -185,7 +247,6 @@ pub struct RawNca { // #[bw(write_with = "binrw::io::write_zeroes")] // TODO: we need to write zeroes in case of None here! pub fs_headers: [Option; 4], } -// assert_eq_size!(assert_nca_size; RawNca, [u8; 0xC00]); fn read_fs_headers( header: &RawNcaHeader, @@ -205,8 +266,8 @@ fn read_fs_headers( XtsCryptArgs { key, sector: match magic { - // For pre-1.0.0 "NCA2" NCAs, the first 0x400 byte are encrypted the same way as in NCA3. - // However, each section header is individually encrypted as though it were sector 0, instead of the appropriate sector as in NCA3. + // switchbrew: For pre-1.0.0 "NCA2" NCAs, the first 0x400 byte are encrypted the same way as in NCA3. + // However, each section header is individually encrypted as though it were sector 0, instead of the appropriate sector as in NCA3. NcaMagic::Nca3 => 2 + i, NcaMagic::Nca2 => 0, _ => todo!("{:?}", magic), @@ -225,32 +286,33 @@ fn read_fs_headers( } } -#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] pub struct Pfs0Superblock { pub master_hash: Hash, pub block_size: u32, + #[br(assert(always_2 == 0x2))] + #[bw(assert(*always_2 == 0x2))] pub always_2: u32, pub hash_table_offset: u64, pub hash_table_size: u64, pub pfs0_offset: u64, - // #[brw(align_after = 0x138)] + #[brw(pad_after = 0xF0)] pub pfs0_size: u64, - pub _0x48: SkipDebug<[u8; 0xF0]>, } pub const IVFC_MAX_LEVEL: usize = 6; -#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] pub struct BktrHeader { pub offset: u64, pub size: u64, - pub magic: [u8; 4], /* "BKTR" */ - pub _0x14: u32, /* Version? */ + #[brw(magic = b"BKTR")] // why the magic is in the middle of the struct??? + pub version: u32, /* Version? */ + #[brw(pad_after = 0x4)] // reserved pub num_entries: u32, - pub _0x1c: u32, /* Reserved? */ } -#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] pub struct IvfcLevelHeader { pub logical_offset: u64, pub hash_data_size: u64, @@ -258,58 +320,64 @@ pub struct IvfcLevelHeader { pub reserved: u32, } -#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] #[brw(magic = b"IVFC")] pub struct IvfcHeader { pub id: u32, pub master_hash_size: u32, pub num_levels: u32, pub level_headers: [IvfcLevelHeader; IVFC_MAX_LEVEL], - pub _0xa0: [u8; 0x20], + #[brw(pad_before = 0x20)] pub master_hash: Hash, } -#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] pub struct BktrSuperblock { pub ivfc_header: IvfcHeader, - pub _0xe0: [u8; 0x18], + #[brw(pad_before = 0x18)] pub relocation_header: BktrHeader, pub subsection_header: BktrHeader, } -#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] pub struct RomfsSuperblock { + #[brw(pad_after = 0x58)] pub ivfc_header: IvfcHeader, - pub _0xe0: [u8; 0x58], } +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct UnknownSuperblock([u8; 0x138]); +impl_debug_deserialize_serialize_hexstring!(UnknownSuperblock); -#[derive(Clone, Copy, Debug, BinRead, BinWrite)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, BinRead, BinWrite)] #[br(import(partition_type: RawPartitionType, fs_type: RawFsType, crypto_type: CryptoType))] -pub enum RawSuperblock { +#[serde(tag = "type")] +pub enum Superblock { #[br(pre_assert(partition_type == RawPartitionType::Pfs0 && fs_type == RawFsType::Pfs0))] Pfs0(Pfs0Superblock), #[br(pre_assert(partition_type == RawPartitionType::RomFs && fs_type == RawFsType::RomFs && crypto_type == CryptoType::Bktr))] Bktr(BktrSuperblock), #[br(pre_assert(partition_type == RawPartitionType::RomFs && fs_type == RawFsType::RomFs))] RomFs(RomfsSuperblock), + // no NCA0 support for now // Nca0Romfs(Nca0RomfsSuperblock), - Raw([u8; 0x138]), + /// Catchall for all unknown superblocks or weird header combinations + Unknown(UnknownSuperblock), } -// assert_eq_size!(assert_superblock_size; RawSuperblock, [u8; 0x138]); #[derive(Clone, Copy, Debug, BinRead, BinWrite)] +#[br(assert(version == 2))] +#[bw(assert(*version == 2))] pub struct RawNcaFsHeader { pub version: u16, pub partition_type: RawPartitionType, pub fs_type: RawFsType, + #[brw(pad_after = 0x3)] pub crypt_type: CryptoType, - pub _0x5: [u8; 0x3], #[br(args(partition_type, fs_type, crypt_type))] - pub superblock: RawSuperblock, + pub superblock: Superblock, + #[brw(pad_after = 0xB8)] pub section_ctr: u64, - pub _0x148: [u8; 0xB8], } -// assert_eq_size!(assert_nca_fs_header_size; RawNcaFsHeader, [u8; 0x148 + 0xB8]); #[derive(Clone, Copy, Debug, PartialEq, Eq, BinRead, BinWrite)] #[brw(repr = u8)] @@ -336,7 +404,8 @@ pub enum CryptoType { #[derive(Debug, Clone, Copy, BinRead, BinWrite)] pub struct RawSectionTableEntry { + // note: these offsets are divided by 0x200 pub media_start_offset: u32, + #[brw(pad_after = 0x8)] pub media_end_offset: u32, - pub padding: [u8; 0x8], } diff --git a/src/format/npdm.rs b/src/format/npdm.rs index 6561efd..692b05e 100644 --- a/src/format/npdm.rs +++ b/src/format/npdm.rs @@ -1,6 +1,6 @@ use crate::format::utils::HexOrNum; use bit_field::BitField; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryFrom; diff --git a/src/format/nxo.rs b/src/format/nxo.rs index d70b9b7..ecde068 100644 --- a/src/format/nxo.rs +++ b/src/format/nxo.rs @@ -2,7 +2,7 @@ use crate::format::utils::HexOrNum; use crate::format::{nacp::NacpFile, npdm::KernelCapability, romfs::RomFs, utils}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use elf::types::{Machine, ProgramHeader, SectionHeader, EM_AARCH64, EM_ARM, PT_LOAD, SHT_NOTE}; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::convert::{TryFrom, TryInto}; use std::fs::File;