Added content region parsing and content encryption/decryption

This commit is contained in:
Campbell 2025-03-18 09:13:34 -04:00
parent 247f120da4
commit 93f2103763
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
9 changed files with 202 additions and 32 deletions

38
Cargo.lock generated
View File

@ -13,6 +13,15 @@ dependencies = [
"cpufeatures", "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]] [[package]]
name = "block-padding" name = "block-padding"
version = "0.3.3" version = "0.3.3"
@ -72,6 +81,16 @@ dependencies = [
"typenum", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@ -82,6 +101,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "inout" name = "inout"
version = "0.1.4" version = "0.1.4"
@ -105,6 +130,19 @@ dependencies = [
"aes", "aes",
"byteorder", "byteorder",
"cbc", "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]] [[package]]

View File

@ -16,3 +16,5 @@ doc = true
byteorder = "1" byteorder = "1"
cbc = "0" cbc = "0"
aes = "0" aes = "0"
hex = "0"
sha1 = "0"

View File

@ -1,5 +1,5 @@
use std::fs; use std::fs;
use rustii::title::{tmd, ticket, crypto}; use rustii::title::{tmd, ticket, content};
fn main() { fn main() {
let data = fs::read("title.tmd").unwrap(); let data = fs::read("title.tmd").unwrap();
@ -15,7 +15,14 @@ fn main() {
println!("title key (dec): {:?}", tik.dec_title_key()); println!("title key (dec): {:?}", tik.dec_title_key());
assert_eq!(data, tik.to_vec().unwrap()); 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)); let content_dec = content_region.get_content_by_index(0, tik.dec_title_key()).unwrap();
println!("re-encrypted key matched"); 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);
} }

View File

@ -1,25 +1,27 @@
// title/commonkeys.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors // title/commonkeys.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii-lib // 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 COMMON_KEY: &str = "ebe42a225e8593e448d9c5457381aaf7";
const KOREAN_KEY: [u8; 16] = [0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e]; const KOREAN_KEY: &str = "63b82bb4f4614e2e13f2fefbba4c9b7e";
const VWII_KEY: [u8; 16] = [0x30, 0xbf, 0xc7, 0x6e, 0x7c, 0x19, 0xaf, 0xbb, 0x23, 0x16, 0x33, 0x30, 0xce, 0xd7, 0xc2, 0x8d]; const VWII_KEY: &str = "30bfc76e7c19afbb23163330ced7c28d";
const DEV_COMMON_KEY: [u8; 16] = [0xa1, 0x60, 0x4a, 0x6a, 0x71, 0x23, 0xb5, 0x29, 0xae, 0x8b, 0xec, 0x32, 0xc8, 0x16, 0xfc, 0xaa]; const DEV_COMMON_KEY: &str = "a1604a6a7123b529ae8bec32c816fcaa";
pub fn get_common_key(index: u8, is_dev: Option<bool>) -> [u8; 16] { pub fn get_common_key(index: u8, is_dev: Option<bool>) -> [u8; 16] {
// Match the Korean and vWii keys, and if they don't match then fall back on the common key. // 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 // The is_dev argument is an option, and if it's set to false or None, then the regular
// common key will be used. // common key will be used.
let selected_key: &str;
match index { match index {
1 => KOREAN_KEY, 1 => selected_key = KOREAN_KEY,
2 => VWII_KEY, 2 => selected_key = VWII_KEY,
_ => { _ => {
match is_dev { match is_dev {
Some(true) => DEV_COMMON_KEY, Some(true) => selected_key = DEV_COMMON_KEY,
_ => COMMON_KEY _ => selected_key = COMMON_KEY,
} }
} }
} }
hex::decode(selected_key).unwrap().try_into().unwrap()
} }
#[cfg(test)] #[cfg(test)]

127
src/title/content.rs Normal file
View File

@ -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<ContentRecord>,
pub content_region_size: u32,
pub num_contents: u16,
pub content_start_offsets: Vec<u64>,
pub contents: Vec<Vec<u8>>,
}
impl ContentRegion {
pub fn from_bytes(data: &[u8], content_records: Vec<ContentRecord>) -> Result<Self, std::io::Error> {
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<u64> = 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<u8>> = 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<Vec<u8>, std::io::Error> {
let mut buf: Vec<u8> = 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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)
}
}
}

View File

@ -33,3 +33,15 @@ pub fn encrypt_title_key(title_key_dec: [u8; 16], common_key_index: u8, title_id
encryptor.encrypt_padded_mut::<ZeroPadding>(&mut title_key, 16).unwrap(); encryptor.encrypt_padded_mut::<ZeroPadding>(&mut title_key, 16).unwrap();
title_key title_key
} }
// Decrypt content using a Title Key.
pub fn decrypt_content(data: &[u8], title_key: [u8; 16], index: u16) -> Vec<u8> {
let mut iv = Vec::from(index.to_be_bytes());
iv.resize(16, 0);
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
println!("{:?}", iv);
let decryptor = Aes128CbcDec::new(&title_key.into(), iv.as_slice().into());
let mut buf = data.to_owned();
decryptor.decrypt_padded_mut::<ZeroPadding>(&mut buf).unwrap();
buf
}

View File

@ -2,7 +2,7 @@
// https://github.com/NinjaCheetah/rustii-lib // https://github.com/NinjaCheetah/rustii-lib
pub mod commonkeys; pub mod commonkeys;
pub mod content;
pub mod crypto; pub mod crypto;
pub mod ticket; pub mod ticket;
pub mod tmd; pub mod tmd;

View File

@ -119,7 +119,7 @@ impl Ticket {
} }
pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> { pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> {
let mut buf = Vec::new(); let mut buf: Vec<u8> = Vec::new();
buf.write_u32::<BigEndian>(self.signature_type)?; buf.write_u32::<BigEndian>(self.signature_type)?;
buf.write_all(&self.signature)?; buf.write_all(&self.signature)?;
buf.write_all(&self.padding1)?; buf.write_all(&self.padding1)?;

View File

@ -130,7 +130,7 @@ impl TMD {
} }
pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> { pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> {
let mut buf = Vec::new(); let mut buf: Vec<u8> = Vec::new();
buf.write_u32::<BigEndian>(self.signature_type)?; buf.write_u32::<BigEndian>(self.signature_type)?;
buf.write_all(&self.signature)?; buf.write_all(&self.signature)?;
buf.write_all(&self.padding1)?; buf.write_all(&self.padding1)?;
@ -164,22 +164,4 @@ impl TMD {
} }
Ok(buf) 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);
}
}