diff --git a/Cargo.lock b/Cargo.lock index ec4dcf1..4d499e3 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" @@ -437,6 +479,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" @@ -491,6 +539,7 @@ name = "linkle" version = "0.2.10" dependencies = [ "aes", + "binrw", "bit_field", "blz-nx", "byteorder", @@ -505,17 +554,19 @@ dependencies = [ "dirs-next", "elf", "goblin", + "hex", "lz4", "num-traits", + "pretty-hex", "rust-ini", "scroll", "semver", "serde", - "serde_derive", "serde_json", "sha2", "snafu", "structopt", + "xts-mode", ] [[package]] @@ -608,12 +659,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 +1107,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..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 } @@ -41,6 +40,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" +pretty-hex = "0.3.0" +hex = "0.4.3" + cipher = "0.4.3" digest = "0.10.6" @@ -48,5 +51,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..b605a57 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -103,6 +103,44 @@ 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, + + /// 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, + + /// Keyfile + #[structopt(parse(from_os_str), short = "k", long = "keyset")] + keyfile: Option, + }, } fn create_nxo( @@ -241,17 +279,54 @@ 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)?; keys.write(&mut std::io::stdout(), console_unique, minimal) .unwrap(); Ok(()) } +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)?; + 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)?; + 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)?; + 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> { s.as_ref().map(AsRef::as_ref) } @@ -303,6 +378,27 @@ 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, + 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), + 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..418c5e0 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,6 +59,23 @@ pub enum Error { error: PathBuf, backtrace: Backtrace, }, + #[snafu(display("Missing key {:?}. Make sure your keyfile is complete", key_name))] + MissingKey { + 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))] + NcaParse { + key_name: &'static str, + backtrace: Backtrace, + }, + #[snafu(display("Missing section {}.", index))] + MissingSection { index: usize, 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/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 new file mode 100644 index 0000000..2fb6976 --- /dev/null +++ b/src/format/nca/crypto_stream.rs @@ -0,0 +1,205 @@ +use crate::error::Error; +use crate::format::nca::{NcaCrypto, NcaSectionInfo}; +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: NcaSectionInfo, +} + +impl CryptoStream { + 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, + 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.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); + // 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_written + 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 new file mode 100644 index 0000000..c9ae578 --- /dev/null +++ b/src/format/nca/mod.rs @@ -0,0 +1,280 @@ +//! 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. +//! +//! 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: +//! +//! ``` +//! 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, KeyType, RawNca}; +use crate::pki::{Aes128Key, AesXtsKey, KeyName, Keys, TitleKey}; +use binrw::BinRead; +use serde::{Deserialize, Serialize}; +use snafu::{Backtrace, GenerateImplicitData}; +use std::cmp::max; +use std::io::{Read, Seek}; + +mod crypto_stream; +mod structures; + +pub use crate::format::nca::crypto_stream::CryptoStream; +use crate::format::nca::crypto_stream::CryptoStreamState; +use crate::utils::{ReadRange, TryClone}; +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 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 NcaSectionInfo { + /// Get size of the section. + fn size(&self) -> u64 { + self.media_end_offset - self.media_start_offset + } +} + +/// Contains information about NCA collected from the header. +#[derive(Debug, Serialize, Deserialize)] +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: NcaInfo, +} + +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), + }; + pki.get_key(key_name) +} + +// 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]; + + file.read_exact(&mut header)?; + + // NOTE: no support for decrypted NCAs + + let header_key = pki.get_xts_key(KeyName::HeaderKey)?; + + let mut raw_nca = std::io::Cursor::new(header); + 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)] +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), +} + +impl Nca { + pub fn from_file( + pki: &Keys, + mut file: R, + title_key: Option, // TODO: get titlekey from a DB? + ) -> Result, Error> { + let RawNca { + sigs, + header, + fs_headers, + } = decrypt_header(pki, &mut file)?; + + // 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.is_empty(); + + let key_area_key = get_key_area_key(pki, master_key_revision, header.key_type)?; + + 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 { + None + }; + + // Parse sections + let mut sections = [None, None, None, None]; + for (idx, (section, fs)) in header + .section_entries + .iter() + .zip(fs_headers.iter()) + .enumerate() + { + // Check if section is present + if let Some(fs) = fs { + 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!("Xts is not supported for RightsId crypto"), + } + } else { + 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(NcaSectionInfo { + crypto, + superblock: fs.superblock, + nonce: fs.section_ctr, + 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: NcaInfo { + format: header.magic, + sig: sigs.fixed_key_sig, + npdm_sig: sigs.npdm_sig, + 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: header.title_id, + sdk_version: header.sdk_version, + rights_id: header.rights_id, + sections, + }, + }; + + Ok(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.media_start_offset))?; + + Ok(CryptoStream { + 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 { + json: section.clone(), + offset: 0, // the offset is relative to the start of the section + }, + }) + } else { + Err(Error::MissingSection { + index: id, + backtrace: Backtrace::generate(), + }) + } + } +} + +impl Nca { + pub fn info(&self) -> &NcaInfo { + &self.info + } +} diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs new file mode 100644 index 0000000..a657395 --- /dev/null +++ b/src/format/nca/structures.rs @@ -0,0 +1,411 @@ +//! Raw NCA structures +//! +//! 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, WriteOptions}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::Debug; +use std::io::{Read, Seek, Write}; +use std::ops::Deref; + +#[repr(transparent)] +#[derive(Clone, Copy, BinRead, BinWrite)] +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(()) + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct Hash([u8; 0x20]); +impl_debug_deserialize_serialize_hexstring!(Hash); + +#[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) + } +} + +#[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(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)] +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, 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 NcaSigs { + pub fixed_key_sig: SigDebug, + pub npdm_sig: SigDebug, +} + +#[derive(Debug, Clone, Copy, BinRead, BinWrite)] +pub struct RawNcaHeader { + pub magic: NcaMagic, + #[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, + #[brw(pad_after = 4)] + pub title_id: TitleId, + pub sdk_version: SdkVersion, + pub crypto_type2: u8, + #[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], +} + +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)] +#[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], +} + +fn read_fs_headers( + 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 { + let section_header = >::read_options( + reader, + options, + XtsCryptArgs { + key, + sector: match magic { + // 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), + }, + }, + )?; + + Some(section_header.0) + } else { + <[u8; 0x200]>::read_options(reader, options, ())?; + None + } + } + + Ok(res) + } +} + +#[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(pad_after = 0xF0)] + pub pfs0_size: u64, +} + +pub const IVFC_MAX_LEVEL: usize = 6; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] +pub struct BktrHeader { + pub offset: u64, + pub size: u64, + #[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, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, 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, 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], + #[brw(pad_before = 0x20)] + pub master_hash: Hash, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] +pub struct BktrSuperblock { + pub ivfc_header: IvfcHeader, + #[brw(pad_before = 0x18)] + pub relocation_header: BktrHeader, + pub subsection_header: BktrHeader, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BinRead, BinWrite)] +pub struct RomfsSuperblock { + #[brw(pad_after = 0x58)] + pub ivfc_header: IvfcHeader, +} +#[derive(Clone, Copy, BinRead, BinWrite)] +pub struct UnknownSuperblock([u8; 0x138]); +impl_debug_deserialize_serialize_hexstring!(UnknownSuperblock); + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, BinRead, BinWrite)] +#[br(import(partition_type: RawPartitionType, fs_type: RawFsType, crypto_type: CryptoType))] +#[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), + /// Catchall for all unknown superblocks or weird header combinations + Unknown(UnknownSuperblock), +} + +#[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, + #[br(args(partition_type, fs_type, crypt_type))] + pub superblock: Superblock, + #[brw(pad_after = 0xB8)] + pub section_ctr: u64, +} + +#[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 { + // note: these offsets are divided by 0x200 + pub media_start_offset: u32, + #[brw(pad_after = 0x8)] + pub media_end_offset: u32, +} 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; diff --git a/src/pki.rs b/src/pki.rs index 037ff19..a534729 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -1,49 +1,36 @@ 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 hex::FromHexError; use ini::{self, Properties}; use snafu::{Backtrace, GenerateImplicitData}; -use std::fmt; +use std::fmt::Debug; 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]); +// 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]); -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 +81,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 +99,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)); @@ -121,43 +108,184 @@ impl Aes128Key { Ok(AesXtsKey(newkey)) } -} -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 { + /// 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: format!( - "Key {} is not of the right size. It should be a {} byte hexstring", - name, - key.len() * 2 + error: String::from( + "buf length should be a multiple of 16, the size of an AES block.", ), 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 }; + + 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(), + }); } - Ok(Some(())) - } else { - Ok(None) + + 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] { + let mut tweak = [0; 0x10]; + for tweak in tweak.iter_mut().rev() { + /* Nintendo LE custom tweak... */ + *tweak = (sector & 0xFF) as u8; + sector >>= 8; } + tweak } -fn key_to_aes_array( +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 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(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 { @@ -172,6 +300,111 @@ impl OptionExt for Option { } } +#[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 { secure_boot_key: Option, @@ -223,7 +456,6 @@ pub struct Keys { save_mac_key: Option, sd_card_save_key: Option, sd_card_nca_key: Option, - // these fields are not used yet, but will be when we implement the NCA decryption #[allow(dead_code)] nca_hdr_fixed_key_modulus: [Option; 2], #[allow(dead_code)] @@ -232,6 +464,116 @@ pub struct Keys { 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 { @@ -394,18 +736,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))?, ); }; } @@ -413,14 +753,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)?, ); } }; @@ -429,14 +768,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)?, ); } }; @@ -445,15 +783,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)?, ); } }; @@ -593,7 +929,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 +983,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 +1117,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 +1250,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()) }