From 93f2103763ccda06028e08cc9f5eefc13ddc38d0 Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:13:34 -0400 Subject: [PATCH] Added content region parsing and content encryption/decryption --- Cargo.lock | 38 ++++++++++++ Cargo.toml | 2 + src/bin/rustii/main.rs | 13 +++- src/title/commonkeys.rs | 18 +++--- src/title/content.rs | 127 ++++++++++++++++++++++++++++++++++++++++ src/title/crypto.rs | 12 ++++ src/title/mod.rs | 2 +- src/title/ticket.rs | 2 +- src/title/tmd.rs | 20 +------ 9 files changed, 202 insertions(+), 32 deletions(-) create mode 100644 src/title/content.rs diff --git a/Cargo.lock b/Cargo.lock index 2aec9f7..d61c663 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -72,6 +81,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -82,6 +101,12 @@ dependencies = [ "version_check", ] +[[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.4" @@ -105,6 +130,19 @@ dependencies = [ "aes", "byteorder", "cbc", + "hex", + "sha1", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 569cc8c..ca31a02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ doc = true byteorder = "1" cbc = "0" aes = "0" +hex = "0" +sha1 = "0" diff --git a/src/bin/rustii/main.rs b/src/bin/rustii/main.rs index 40a97bd..6fe3748 100644 --- a/src/bin/rustii/main.rs +++ b/src/bin/rustii/main.rs @@ -1,5 +1,5 @@ use std::fs; -use rustii::title::{tmd, ticket, crypto}; +use rustii::title::{tmd, ticket, content}; fn main() { let data = fs::read("title.tmd").unwrap(); @@ -15,7 +15,14 @@ fn main() { println!("title key (dec): {:?}", tik.dec_title_key()); assert_eq!(data, tik.to_vec().unwrap()); + let data = fs::read("content-blob").unwrap(); + let content_region = content::ContentRegion::from_bytes(&data, tmd.content_records).unwrap(); + assert_eq!(data, content_region.to_bytes().unwrap()); + println!("content OK"); - assert_eq!(tik.title_key, crypto::encrypt_title_key(tik.dec_title_key(), tik.common_key_index, tik.title_id)); - println!("re-encrypted key matched"); + let content_dec = content_region.get_content_by_index(0, tik.dec_title_key()).unwrap(); + println!("content dec from index: {:?}", content_dec); + + let content = content_region.get_content_by_cid(150, tik.dec_title_key()).unwrap(); + println!("content dec from cid: {:?}", content); } diff --git a/src/title/commonkeys.rs b/src/title/commonkeys.rs index 11d5a21..3a22078 100644 --- a/src/title/commonkeys.rs +++ b/src/title/commonkeys.rs @@ -1,25 +1,27 @@ // title/commonkeys.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustii-lib -const COMMON_KEY: [u8; 16] = [0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7]; -const KOREAN_KEY: [u8; 16] = [0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e]; -const VWII_KEY: [u8; 16] = [0x30, 0xbf, 0xc7, 0x6e, 0x7c, 0x19, 0xaf, 0xbb, 0x23, 0x16, 0x33, 0x30, 0xce, 0xd7, 0xc2, 0x8d]; -const DEV_COMMON_KEY: [u8; 16] = [0xa1, 0x60, 0x4a, 0x6a, 0x71, 0x23, 0xb5, 0x29, 0xae, 0x8b, 0xec, 0x32, 0xc8, 0x16, 0xfc, 0xaa]; +const COMMON_KEY: &str = "ebe42a225e8593e448d9c5457381aaf7"; +const KOREAN_KEY: &str = "63b82bb4f4614e2e13f2fefbba4c9b7e"; +const VWII_KEY: &str = "30bfc76e7c19afbb23163330ced7c28d"; +const DEV_COMMON_KEY: &str = "a1604a6a7123b529ae8bec32c816fcaa"; pub fn get_common_key(index: u8, is_dev: Option) -> [u8; 16] { // Match the Korean and vWii keys, and if they don't match then fall back on the common key. // The is_dev argument is an option, and if it's set to false or None, then the regular // common key will be used. + let selected_key: &str; match index { - 1 => KOREAN_KEY, - 2 => VWII_KEY, + 1 => selected_key = KOREAN_KEY, + 2 => selected_key = VWII_KEY, _ => { match is_dev { - Some(true) => DEV_COMMON_KEY, - _ => COMMON_KEY + Some(true) => selected_key = DEV_COMMON_KEY, + _ => selected_key = COMMON_KEY, } } } + hex::decode(selected_key).unwrap().try_into().unwrap() } #[cfg(test)] diff --git a/src/title/content.rs b/src/title/content.rs new file mode 100644 index 0000000..4b91754 --- /dev/null +++ b/src/title/content.rs @@ -0,0 +1,127 @@ +// title/content.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors +// https://github.com/NinjaCheetah/rustii-lib +// +// Implements content parsing and editing. + +use std::error::Error; +use std::fmt; +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use sha1::{Sha1, Digest}; +use crate::title::tmd::ContentRecord; +use crate::title::crypto::decrypt_content; + +#[derive(Debug)] +pub enum ContentError { + IndexNotFound, + CIDNotFound, + BadHash, +} + +impl fmt::Display for ContentError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = match *self { + ContentError::IndexNotFound => "The specified content index does not exist.", + ContentError::CIDNotFound => "The specified Content ID does not exist.", + ContentError::BadHash => "The content hash does not match the expected hash.", + }; + f.write_str(description) + } +} + +impl Error for ContentError {} + +#[derive(Debug)] +pub struct ContentRegion { + pub content_records: Vec, + pub content_region_size: u32, + pub num_contents: u16, + pub content_start_offsets: Vec, + pub contents: Vec>, +} + +impl ContentRegion { + pub fn from_bytes(data: &[u8], content_records: Vec) -> Result { + let content_region_size = data.len() as u32; + let num_contents = content_records.len() as u16; + // Calculate the starting offsets of each content. + let content_start_offsets: Vec = std::iter::once(0) + .chain(content_records.iter().scan(0, |offset, record| { + *offset += record.content_size; + if record.content_size % 64 != 0 { + *offset += 64 - (record.content_size % 64); + } + Some(*offset) + })).take(content_records.len()).collect(); // Trims the extra final entry. + let total_content_size: u64 = content_records.iter().map(|x| (x.content_size + 63) & !63).sum(); + // Parse the content blob and create a vector of vectors from it. + // Check that the content blob matches the total size of all the contents in the records. + if content_region_size != total_content_size as u32 { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid content blob for content records")); + } + let mut contents: Vec> = Vec::with_capacity(num_contents as usize); + let mut buf = Cursor::new(data); + for i in 0..num_contents { + buf.seek(SeekFrom::Start(content_start_offsets[i as usize]))?; + let size = (content_records[i as usize].content_size + 15) & !15; + let mut content = vec![0u8; size as usize]; + buf.read_exact(&mut content)?; + contents.push(content); + } + Ok(ContentRegion { + content_records, + content_region_size, + num_contents, + content_start_offsets, + contents, + }) + } + + pub fn to_bytes(&self) -> Result, std::io::Error> { + let mut buf: Vec = Vec::new(); + for i in 0..self.num_contents { + let mut content = self.contents[i as usize].clone(); + // Round up size to nearest 64 to add appropriate padding. + content.resize((content.len() + 63) & !63, 0); + buf.write_all(&content)?; + } + Ok(buf) + } + + pub fn get_enc_content_by_index(&self, index: usize) -> Result, ContentError> { + let content = self.contents.get(index).ok_or(ContentError::IndexNotFound)?; + Ok(content.clone()) + } + + pub fn get_content_by_index(&self, index: usize, title_key: [u8; 16]) -> Result, ContentError> { + let content = self.get_enc_content_by_index(index)?; + // Verify the hash of the decrypted content against its record. + let content_dec = decrypt_content(&content, title_key, self.content_records[index].index); + let mut hasher = Sha1::new(); + hasher.update(content_dec.clone()); + let result = hasher.finalize(); + if result[..] != self.content_records[index].content_hash { + return Err(ContentError::BadHash); + } + Ok(content_dec) + } + + pub fn get_enc_content_by_cid(&self, cid: u32) -> Result, ContentError> { + let index = self.content_records.iter().position(|x| x.content_id == cid); + if let Some(index) = index { + let content = self.get_enc_content_by_index(index).map_err(|_| ContentError::CIDNotFound)?; + Ok(content) + } else { + Err(ContentError::CIDNotFound) + } + } + + pub fn get_content_by_cid(&self, cid: u32, title_key: [u8; 16]) -> Result, ContentError> { + let index = self.content_records.iter().position(|x| x.content_id == cid); + if let Some(index) = index { + let content_dec = self.get_content_by_index(index, title_key)?; + Ok(content_dec) + } else { + Err(ContentError::CIDNotFound) + } + } +} diff --git a/src/title/crypto.rs b/src/title/crypto.rs index 66aff20..341320a 100644 --- a/src/title/crypto.rs +++ b/src/title/crypto.rs @@ -33,3 +33,15 @@ pub fn encrypt_title_key(title_key_dec: [u8; 16], common_key_index: u8, title_id encryptor.encrypt_padded_mut::(&mut title_key, 16).unwrap(); title_key } + +// Decrypt content using a Title Key. +pub fn decrypt_content(data: &[u8], title_key: [u8; 16], index: u16) -> Vec { + let mut iv = Vec::from(index.to_be_bytes()); + iv.resize(16, 0); + type Aes128CbcDec = cbc::Decryptor; + println!("{:?}", iv); + let decryptor = Aes128CbcDec::new(&title_key.into(), iv.as_slice().into()); + let mut buf = data.to_owned(); + decryptor.decrypt_padded_mut::(&mut buf).unwrap(); + buf +} diff --git a/src/title/mod.rs b/src/title/mod.rs index e62913c..46ac81d 100644 --- a/src/title/mod.rs +++ b/src/title/mod.rs @@ -2,7 +2,7 @@ // https://github.com/NinjaCheetah/rustii-lib pub mod commonkeys; +pub mod content; pub mod crypto; pub mod ticket; pub mod tmd; - diff --git a/src/title/ticket.rs b/src/title/ticket.rs index e2416aa..d346729 100644 --- a/src/title/ticket.rs +++ b/src/title/ticket.rs @@ -119,7 +119,7 @@ impl Ticket { } pub fn to_vec(&self) -> Result, std::io::Error> { - let mut buf = Vec::new(); + let mut buf: Vec = Vec::new(); buf.write_u32::(self.signature_type)?; buf.write_all(&self.signature)?; buf.write_all(&self.padding1)?; diff --git a/src/title/tmd.rs b/src/title/tmd.rs index a6f90bb..3829dfa 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -130,7 +130,7 @@ impl TMD { } pub fn to_vec(&self) -> Result, std::io::Error> { - let mut buf = Vec::new(); + let mut buf: Vec = Vec::new(); buf.write_u32::(self.signature_type)?; buf.write_all(&self.signature)?; buf.write_all(&self.padding1)?; @@ -164,22 +164,4 @@ impl TMD { } Ok(buf) } - - pub fn title_version(&self) -> u16 { - self.title_version - } } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - - #[test] - fn test_load_tmd() { - let data = fs::read("title.tmd").unwrap(); - let tmd = TMD::from_bytes(&data).unwrap(); - assert_eq!(tmd.tmd_version, 1); - } -} -