mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-05 23:11:02 -04:00
Added content region parsing and content encryption/decryption
This commit is contained in:
parent
247f120da4
commit
93f2103763
38
Cargo.lock
generated
38
Cargo.lock
generated
@ -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]]
|
||||||
|
@ -16,3 +16,5 @@ doc = true
|
|||||||
byteorder = "1"
|
byteorder = "1"
|
||||||
cbc = "0"
|
cbc = "0"
|
||||||
aes = "0"
|
aes = "0"
|
||||||
|
hex = "0"
|
||||||
|
sha1 = "0"
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
127
src/title/content.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)?;
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user