Made a bunch of fields that should be private private

This commit is contained in:
2026-02-22 22:21:37 -05:00
parent 94e0be0eef
commit 836d5e912a
40 changed files with 1499 additions and 929 deletions

View File

@@ -1,5 +1,5 @@
// title/cert.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/cert.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for validating the signatures of Wii titles.
@@ -37,8 +37,8 @@ pub enum CertificateKeyType {
ECC
}
#[derive(Debug, Clone)]
/// A structure that represents the components of a Wii signing certificate.
#[derive(Debug, Clone)]
pub struct Certificate {
signer_key_type: CertificateKeyType,
signature: Vec<u8>,
@@ -165,8 +165,8 @@ impl Certificate {
}
}
#[derive(Debug)]
/// A structure that represents the components of the Wii's signing certificate chain.
#[derive(Debug)]
pub struct CertificateChain {
ca_cert: Certificate,
tmd_cert: Certificate,
@@ -346,7 +346,7 @@ pub fn verify_tmd(tmd_cert: &Certificate, tmd: &tmd::TMD) -> Result<bool, Certif
let public_key_modulus = BigUint::from_bytes_be(&tmd_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(tmd_cert.pub_key_exponent());
let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap();
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &tmd_hash, tmd.signature.as_slice()) {
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &tmd_hash, tmd.signature().as_slice()) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
@@ -368,7 +368,7 @@ pub fn verify_ticket(ticket_cert: &Certificate, ticket: &ticket::Ticket) -> Resu
let public_key_modulus = BigUint::from_bytes_be(&ticket_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(ticket_cert.pub_key_exponent());
let root_key = RsaPublicKey::new(public_key_modulus, public_key_exponent).unwrap();
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &ticket_hash, ticket.signature.as_slice()) {
match root_key.verify(Pkcs1v15Sign::new::<Sha1>(), &ticket_hash, ticket.signature().as_slice()) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}

View File

@@ -1,5 +1,5 @@
// title/commonkeys.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/commonkeys.rs from rustwii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
const COMMON_KEY: &str = "ebe42a225e8593e448d9c5457381aaf7";
const KOREAN_KEY: &str = "63b82bb4f4614e2e13f2fefbba4c9b7e";

View File

@@ -1,11 +1,9 @@
// title/content.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/content.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements content parsing and editing.
use std::cell::RefCell;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::rc::Rc;
use sha1::{Sha1, Digest};
use thiserror::Error;
use crate::title::tmd::{ContentRecord, ContentType};
@@ -37,39 +35,39 @@ pub enum ContentError {
#[derive(Debug)]
/// A structure that represents the block of data containing the content of a digital Wii title.
pub struct ContentRegion {
pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
pub content_region_size: u32,
pub content_start_offsets: Vec<u64>,
pub contents: Vec<Vec<u8>>,
content_records: Vec<ContentRecord>,
content_region_size: u32,
content_start_offsets: Vec<u64>,
contents: Vec<Vec<u8>>,
}
impl ContentRegion {
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a
/// digital Wii title from the content area of a WAD and the ContentRecords from a TMD.
pub fn from_bytes(data: &[u8], content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
pub fn from_bytes(data: &[u8], content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
let content_region_size = data.len() as u32;
let num_contents = content_records.borrow().len() as u16;
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.borrow().iter().scan(0, |offset, record| {
.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.borrow().len()).collect(); // Trims the extra final entry.
})).take(content_records.len()).collect(); // Trims the extra final entry.
// Parse the content blob and create a vector of vectors from it.
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.borrow()[i as usize].content_size + 15) & !15;
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: Rc::clone(&content_records),
content_records,
content_region_size,
content_start_offsets,
contents,
@@ -78,13 +76,13 @@ impl ContentRegion {
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a
/// digital Wii title from a vector of contents and the ContentRecords from a TMD.
pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
if contents.len() != content_records.borrow().len() {
return Err(ContentError::MissingContents { required: content_records.borrow().len(), found: contents.len()});
pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
if contents.len() != content_records.len() {
return Err(ContentError::MissingContents { required: content_records.len(), found: contents.len()});
}
let mut content_region = Self::new(Rc::clone(&content_records))?;
let mut content_region = Self::new(content_records)?;
for i in 0..contents.len() {
let target_index = content_region.content_records.borrow()[i].index;
let target_index = content_region.content_records[i].index;
content_region.load_enc_content(&contents[i], target_index as usize)?;
}
Ok(content_region)
@@ -92,14 +90,14 @@ impl ContentRegion {
/// Creates a ContentRegion instance from the ContentRecords of a TMD that contains no actual
/// content. This can be used to load existing content from files.
pub fn new(content_records: Rc<RefCell<Vec<ContentRecord>>>) -> Result<Self, ContentError> {
let content_region_size: u64 = content_records.borrow().iter().map(|x| (x.content_size + 63) & !63).sum();
pub fn new(content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
let content_region_size: u64 = content_records.iter().map(|x| (x.content_size + 63) & !63).sum();
let content_region_size = content_region_size as u32;
let num_contents = content_records.borrow().len() as u16;
let num_contents = content_records.len() as u16;
let content_start_offsets: Vec<u64> = vec![0; num_contents as usize];
let contents: Vec<Vec<u8>> = vec![Vec::new(); num_contents as usize];
Ok(ContentRegion {
content_records: Rc::clone(&content_records),
content_records,
content_region_size,
content_start_offsets,
contents,
@@ -109,7 +107,7 @@ impl ContentRegion {
/// Dumps the entire ContentRegion back into binary data that can be written to a file.
pub fn to_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
let mut buf: Vec<u8> = Vec::new();
for i in 0..self.content_records.borrow().len() {
for i in 0..self.content_records.len() {
let mut content = self.contents[i].clone();
// Round up size to nearest 64 to add appropriate padding.
content.resize((content.len() + 63) & !63, 0);
@@ -117,12 +115,32 @@ impl ContentRegion {
}
Ok(buf)
}
/// Gets the content records in the ContentRegion.
pub fn content_records(&self) -> &Vec<ContentRecord> {
&self.content_records
}
/// Gets the size of the ContentRegion.
pub fn content_region_size(&self) -> u32 {
self.content_region_size
}
/// Gets the start offsets of the content in the ContentRegion.
pub fn content_start_offsets(&self) -> &Vec<u64> {
&self.content_start_offsets
}
/// Gets the actual data of the content in the ContentRegion.
pub fn contents(&self) -> &Vec<Vec<u8>> {
&self.contents
}
/// Gets the index of content using its Content ID.
pub fn get_index_from_cid(&self, cid: u32) -> Result<usize, ContentError> {
// Use fancy Rust find and map methods to find the index matching the provided CID. Take
// that libWiiPy!
let content_index = self.content_records.borrow().iter()
let content_index = self.content_records.iter()
.find(|record| record.content_id == cid)
.map(|record| record.index);
if let Some(index) = content_index {
@@ -134,7 +152,7 @@ impl ContentRegion {
/// Gets the encrypted content file from the ContentRegion at the specified index.
pub fn get_enc_content_by_index(&self, index: usize) -> Result<Vec<u8>, ContentError> {
let content = self.contents.get(index).ok_or(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 })?;
let content = self.contents.get(index).ok_or(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 })?;
Ok(content.clone())
}
@@ -142,20 +160,20 @@ impl ContentRegion {
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 mut content_dec = crypto::decrypt_content(&content, title_key, self.content_records.borrow()[index].index);
content_dec.resize(self.content_records.borrow()[index].content_size as usize, 0);
let mut content_dec = crypto::decrypt_content(&content, title_key, self.content_records[index].index);
content_dec.resize(self.content_records[index].content_size as usize, 0);
let mut hasher = Sha1::new();
hasher.update(content_dec.clone());
let result = hasher.finalize();
if result[..] != self.content_records.borrow()[index].content_hash {
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[index].content_hash) });
if result[..] != self.content_records[index].content_hash {
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) });
}
Ok(content_dec)
}
/// Gets the encrypted content file from the ContentRegion with the specified Content ID.
pub fn get_enc_content_by_cid(&self, cid: u32) -> Result<Vec<u8>, ContentError> {
let index = self.content_records.borrow().iter().position(|x| x.content_id == cid);
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(cid))?;
Ok(content)
@@ -166,7 +184,7 @@ impl ContentRegion {
/// Gets the decrypted content file from the ContentRegion with the specified Content ID.
pub fn get_content_by_cid(&self, cid: u32, title_key: [u8; 16]) -> Result<Vec<u8>, ContentError> {
let index = self.content_records.borrow().iter().position(|x| x.content_id == cid);
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)
@@ -178,8 +196,8 @@ impl ContentRegion {
/// Loads existing content into the specified index of a ContentRegion instance. This content
/// must be encrypted.
pub fn load_enc_content(&mut self, content: &[u8], index: usize) -> Result<(), ContentError> {
if index >= self.content_records.borrow().len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
if index >= self.content_records.len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
}
self.contents[index] = content.to_vec();
Ok(())
@@ -190,20 +208,20 @@ impl ContentRegion {
/// values can be set in the corresponding content record. Optionally, a new Content ID or
/// content type can be provided, with the existing values being preserved by default.
pub fn set_enc_content(&mut self, content: &[u8], index: usize, content_size: u64, content_hash: [u8; 20], cid: Option<u32>, content_type: Option<ContentType>) -> Result<(), ContentError> {
if index >= self.content_records.borrow().len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
if index >= self.content_records.len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
}
self.content_records.borrow_mut()[index].content_size = content_size;
self.content_records.borrow_mut()[index].content_hash = content_hash;
self.content_records[index].content_size = content_size;
self.content_records[index].content_hash = content_hash;
if cid.is_some() {
// Make sure that the new CID isn't already in use.
if self.content_records.borrow().iter().any(|record| record.content_id == cid.unwrap()) {
if self.content_records.iter().any(|record| record.content_id == cid.unwrap()) {
return Err(ContentError::CIDAlreadyExists(cid.unwrap()));
}
self.content_records.borrow_mut()[index].content_id = cid.unwrap();
self.content_records[index].content_id = cid.unwrap();
}
if content_type.is_some() {
self.content_records.borrow_mut()[index].content_type = content_type.unwrap();
self.content_records[index].content_type = content_type.unwrap();
}
self.contents[index] = content.to_vec();
Ok(())
@@ -213,18 +231,18 @@ impl ContentRegion {
/// must be decrypted and needs to match the size and hash listed in the content record at that
/// index.
pub fn load_content(&mut self, content: &[u8], index: usize, title_key: [u8; 16]) -> Result<(), ContentError> {
if index >= self.content_records.borrow().len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
if index >= self.content_records.len() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
}
// Hash the content we're trying to load to ensure it matches the hash expected in the
// matching record.
let mut hasher = Sha1::new();
hasher.update(content);
let result = hasher.finalize();
if result[..] != self.content_records.borrow()[index].content_hash {
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records.borrow()[index].content_hash) });
if result[..] != self.content_records[index].content_hash {
return Err(ContentError::BadHash { hash: hex::encode(result), expected: hex::encode(self.content_records[index].content_hash) });
}
let content_enc = encrypt_content(content, title_key, self.content_records.borrow()[index].index, self.content_records.borrow()[index].content_size);
let content_enc = encrypt_content(content, title_key, self.content_records[index].index, self.content_records[index].content_size);
self.contents[index] = content_enc;
Ok(())
}
@@ -247,11 +265,11 @@ impl ContentRegion {
/// may leave a gap in the indexes recorded in the content records, but this should not cause
/// issues on the Wii or with correctly implemented WAD parsers.
pub fn remove_content(&mut self, index: usize) -> Result<(), ContentError> {
if self.contents.get(index).is_none() || self.content_records.borrow().get(index).is_none() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.borrow().len() - 1 });
if self.contents.get(index).is_none() || self.content_records.get(index).is_none() {
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
}
self.contents.remove(index);
self.content_records.borrow_mut().remove(index);
self.content_records.remove(index);
Ok(())
}
@@ -259,14 +277,14 @@ impl ContentRegion {
/// Content ID, type, index, and decrypted hash will be added to the record.
pub fn add_enc_content(&mut self, content: &[u8], index: u16, cid: u32, content_type: ContentType, content_size: u64, content_hash: [u8; 20]) -> Result<(), ContentError> {
// Return an error if the specified index or CID already exist in the records.
if self.content_records.borrow().iter().any(|record| record.index == index) {
if self.content_records.iter().any(|record| record.index == index) {
return Err(ContentError::IndexAlreadyExists(index));
}
if self.content_records.borrow().iter().any(|record| record.content_id == cid) {
if self.content_records.iter().any(|record| record.content_id == cid) {
return Err(ContentError::CIDAlreadyExists(cid));
}
self.contents.push(content.to_vec());
self.content_records.borrow_mut().push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
self.content_records.push(ContentRecord { content_id: cid, index, content_type, content_size, content_hash });
Ok(())
}
@@ -275,7 +293,7 @@ impl ContentRegion {
/// index will be automatically assigned based on the highest index currently recorded in the
/// content records.
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: ContentType, title_key: [u8; 16]) -> Result<(), ContentError> {
let max_index = self.content_records.borrow().iter()
let max_index = self.content_records.iter()
.max_by_key(|record| record.index)
.map(|record| record.index)
.unwrap_or(0); // This should be impossible, but I guess 0 is a safe value just in case?

View File

@@ -1,5 +1,5 @@
// title/crypto.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/crypto.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the common crypto functions required to handle Wii content encryption.

View File

@@ -1,5 +1,5 @@
// title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/mod.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Root for all title-related modules and implementation of the high-level Title object.
@@ -13,7 +13,6 @@ pub mod tmd;
pub mod versions;
pub mod wad;
use std::rc::Rc;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -53,7 +52,7 @@ impl Title {
let cert_chain = cert::CertificateChain::from_bytes(&wad.cert_chain()).map_err(TitleError::CertificateError)?;
let ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(TitleError::Ticket)?;
let tmd = tmd::TMD::from_bytes(&wad.tmd()).map_err(TitleError::TMD)?;
let content = content::ContentRegion::from_bytes(&wad.content(), Rc::clone(&tmd.content_records)).map_err(TitleError::Content)?;
let content = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records().clone()).map_err(TitleError::Content)?;
Ok(Title {
cert_chain,
crl: wad.crl(),
@@ -123,13 +122,13 @@ impl Title {
/// Gets the decrypted content file from the Title at the specified index.
pub fn get_content_by_index(&self, index: usize) -> Result<Vec<u8>, content::ContentError> {
let content = self.content.get_content_by_index(index, self.ticket.dec_title_key())?;
let content = self.content.get_content_by_index(index, self.ticket.title_key_dec())?;
Ok(content)
}
/// Gets the decrypted content file from the Title with the specified Content ID.
pub fn get_content_by_cid(&self, cid: u32) -> Result<Vec<u8>, content::ContentError> {
let content = self.content.get_content_by_cid(cid, self.ticket.dec_title_key())?;
let content = self.content.get_content_by_cid(cid, self.ticket.title_key_dec())?;
Ok(content)
}
@@ -137,7 +136,7 @@ impl Title {
/// have its size and hash saved into the matching record. Optionally, a new Content ID or
/// content type can be provided, with the existing values being preserved by default.
pub fn set_content(&mut self, content: &[u8], index: usize, cid: Option<u32>, content_type: Option<tmd::ContentType>) -> Result<(), TitleError> {
self.content.set_content(content, index, cid, content_type, self.ticket.dec_title_key())?;
self.content.set_content(content, index, cid, content_type, self.ticket.title_key_dec())?;
Ok(())
}
@@ -146,7 +145,7 @@ impl Title {
/// index will be automatically assigned based on the highest index currently recorded in the
/// content records.
pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: tmd::ContentType) -> Result<(), TitleError> {
self.content.add_content(content, cid, content_type, self.ticket.dec_title_key())?;
self.content.add_content(content, cid, content_type, self.ticket.title_key_dec())?;
Ok(())
}
@@ -158,7 +157,7 @@ impl Title {
// accurate results.
title_size += self.tmd.to_bytes().map_err(|x| TitleError::TMD(tmd::TMDError::IO(x)))?.len();
title_size += self.ticket.to_bytes().map_err(|x| TitleError::Ticket(ticket::TicketError::IO(x)))?.len();
for record in self.tmd.content_records.borrow().iter() {
for record in self.tmd.content_records().iter() {
if matches!(record.content_type, tmd::ContentType::Shared) {
if absolute == Some(true) {
title_size += record.content_size as usize;

View File

@@ -1,5 +1,5 @@
// title/nus.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/nus.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the functions required for downloading data from the NUS.
@@ -80,7 +80,7 @@ pub fn download_content(title_id: [u8; 8], content_id: u32, wiiu_endpoint: bool)
/// Downloads all contents from the specified title from the NUS.
pub fn download_contents(tmd: &tmd::TMD, wiiu_endpoint: bool) -> Result<Vec<Vec<u8>>, NUSError> {
let content_ids: Vec<u32> = tmd.content_records.borrow().iter().map(|record| { record.content_id }).collect();
let content_ids: Vec<u32> = tmd.content_records().iter().map(|record| { record.content_id }).collect();
let mut contents: Vec<Vec<u8>> = Vec::new();
for id in content_ids {
contents.push(download_content(tmd.title_id(), id, wiiu_endpoint)?);
@@ -112,7 +112,7 @@ pub fn download_title(title_id: [u8; 8], title_version: Option<u16>, wiiu_endpoi
let cert_chain = cert::CertificateChain::from_bytes(&download_cert_chain(wiiu_endpoint)?)?;
let tmd = tmd::TMD::from_bytes(&download_tmd(title_id, title_version, wiiu_endpoint)?)?;
let tik = ticket::Ticket::from_bytes(&download_ticket(title_id, wiiu_endpoint)?)?;
let content_region = content::ContentRegion::from_contents(download_contents(&tmd, wiiu_endpoint)?, tmd.content_records.clone())?;
let content_region = content::ContentRegion::from_contents(download_contents(&tmd, wiiu_endpoint)?, tmd.content_records().clone())?;
let title = title::Title::from_parts(cert_chain, None, tik, tmd, content_region, None)?;
Ok(title)
}

View File

@@ -1,5 +1,5 @@
// title/tik.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/tik.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for Ticket parsing and editing.
@@ -33,28 +33,28 @@ pub struct TitleLimit {
#[derive(Debug)]
/// A structure that represents a Wii Ticket file.
pub struct Ticket {
pub signature_type: u32,
pub signature: [u8; 256],
signature_type: u32,
signature: [u8; 256],
padding1: [u8; 60],
pub signature_issuer: [u8; 64],
pub ecdh_data: [u8; 60],
pub ticket_version: u8,
signature_issuer: [u8; 64],
ecdh_data: [u8; 60],
ticket_version: u8,
reserved1: [u8; 2],
pub title_key: [u8; 16],
title_key: [u8; 16],
unknown1: [u8; 1],
pub ticket_id: [u8; 8],
pub console_id: [u8; 4],
ticket_id: [u8; 8],
console_id: [u8; 4],
title_id: [u8; 8],
unknown2: [u8; 2],
pub title_version: u16,
pub permitted_titles_mask: [u8; 4],
pub permit_mask: [u8; 4],
pub title_export_allowed: u8,
pub common_key_index: u8,
title_version: u16,
permitted_titles_mask: [u8; 4],
permit_mask: [u8; 4],
title_export_allowed: u8,
common_key_index: u8,
unknown3: [u8; 48],
pub content_access_permission: [u8; 64],
content_access_permission: [u8; 64],
padding2: [u8; 2],
pub title_limits: [TitleLimit; 8],
title_limits: [TitleLimit; 8],
}
impl Ticket {
@@ -169,8 +169,87 @@ impl Ticket {
Ok(buf)
}
/// Gets the type of the signature on the Ticket.
pub fn signature_type(&self) -> u32 {
self.signature_type
}
/// Gets the signature of the Ticket.
pub fn signature(&self) -> [u8; 256] {
self.signature
}
/// Gets the ECDH data listed in the Ticket.
pub fn ecdh_data(&self) -> [u8; 60] {
self.ecdh_data
}
/// Gets the version of the Ticket file.
pub fn ticket_version(&self) -> u8 {
self.ticket_version
}
/// Gets the raw encrypted Title Key from the Ticket.
pub fn title_key(&self) -> [u8; 16] {
self.title_key
}
pub fn set_title_key(&mut self, title_key: [u8; 16]) {
self.title_key = title_key;
}
/// Gets the Ticket ID listed in the Ticket.
pub fn ticket_id(&self) -> [u8; 8] {
self.ticket_id
}
/// Gets the console ID listed in the Ticket.
pub fn console_id(&self) -> [u8; 4] {
self.console_id
}
/// Gets the version of the title listed in the Ticket.
pub fn title_version(&self) -> u16 {
self.title_version
}
/// Gets the permitted titles mask listed in the Ticket.
pub fn permitted_titles_mask(&self) -> [u8; 4] {
self.permitted_titles_mask
}
/// Gets the permit mask listed in the Ticket.
pub fn permit_mask(&self) -> [u8; 4] {
self.permit_mask
}
/// Gets whether title export is allowed by the Ticket.
pub fn title_export_allowed(&self) -> bool {
self.title_export_allowed == 1
}
/// Gets the index of the common key used by the Ticket.
pub fn common_key_index(&self) -> u8 {
self.common_key_index
}
/// Sets the index of the common key used by the Ticket.
pub fn set_common_key_index(&mut self, index: u8) {
self.common_key_index = index;
}
/// Gets the content access permissions listed in the Ticket.
pub fn content_access_permission(&self) -> [u8; 64] {
self.content_access_permission
}
/// Gets the title usage limits listed in the Ticket.
pub fn title_limits(&self) -> [TitleLimit; 8] {
self.title_limits
}
/// Gets the decrypted version of the Title Key stored in a Ticket.
pub fn dec_title_key(&self) -> [u8; 16] {
pub fn title_key_dec(&self) -> [u8; 16] {
// Get the dev status of this Ticket so decrypt_title_key knows the right common key.
let is_dev = self.is_dev();
decrypt_title_key(self.title_key, self.common_key_index, self.title_id, is_dev)
@@ -242,7 +321,7 @@ impl Ticket {
/// Sets a new Title ID for the Ticket. This will re-encrypt the Title Key, since the Title ID
/// is used as the IV for decrypting the Title Key.
pub fn set_title_id(&mut self, title_id: [u8; 8]) -> Result<(), TicketError> {
let new_enc_title_key = crypto::encrypt_title_key(self.dec_title_key(), self.common_key_index, title_id, self.is_dev());
let new_enc_title_key = crypto::encrypt_title_key(self.title_key_dec(), self.common_key_index, title_id, self.is_dev());
self.title_key = new_enc_title_key;
self.title_id = title_id;
Ok(())

View File

@@ -1,13 +1,11 @@
// title/tmd.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/tmd.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for TMD parsing and editing.
use std::cell::RefCell;
use std::fmt;
use std::io::{Cursor, Read, Write};
use std::ops::Index;
use std::rc::Rc;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use sha1::{Sha1, Digest};
use thiserror::Error;
@@ -81,8 +79,8 @@ pub enum AccessRight {
DVDVideo = 1,
}
#[derive(Debug, Clone)]
/// A structure that represents the metadata of a content file in a digital Wii title.
#[derive(Debug, Clone)]
pub struct ContentRecord {
pub content_id: u32,
pub index: u16,
@@ -91,33 +89,33 @@ pub struct ContentRecord {
pub content_hash: [u8; 20],
}
#[derive(Debug)]
/// A structure that represents a Wii TMD (Title Metadata) file.
#[derive(Debug)]
pub struct TMD {
pub signature_type: u32,
pub signature: [u8; 256],
signature_type: u32,
signature: [u8; 256],
padding1: [u8; 60],
pub signature_issuer: [u8; 64],
pub tmd_version: u8,
pub ca_crl_version: u8,
pub signer_crl_version: u8,
pub is_vwii: u8,
signature_issuer: [u8; 64],
tmd_version: u8,
ca_crl_version: u8,
signer_crl_version: u8,
is_vwii: u8,
ios_tid: [u8; 8],
title_id: [u8; 8],
title_type: [u8; 4],
pub group_id: u16,
group_id: u16,
padding2: [u8; 2],
region: u16,
pub ratings: [u8; 16],
ratings: [u8; 16],
reserved1: [u8; 12],
pub ipc_mask: [u8; 12],
ipc_mask: [u8; 12],
reserved2: [u8; 18],
pub access_rights: u32,
pub title_version: u16,
pub num_contents: u16,
pub boot_index: u16,
pub minor_version: u16, // Normally unused, but good for fakesigning!
pub content_records: Rc<RefCell<Vec<ContentRecord>>>,
access_rights: u32,
title_version: u16,
num_contents: u16,
boot_index: u16,
minor_version: u16, // Normally unused, but useful when fakesigning.
content_records: Vec<ContentRecord>,
}
impl TMD {
@@ -211,7 +209,7 @@ impl TMD {
num_contents,
boot_index,
minor_version,
content_records: Rc::new(RefCell::new(content_records)),
content_records,
})
}
@@ -238,11 +236,11 @@ impl TMD {
buf.write_all(&self.reserved2)?;
buf.write_u32::<BigEndian>(self.access_rights)?;
buf.write_u16::<BigEndian>(self.title_version)?;
buf.write_u16::<BigEndian>(self.content_records.borrow().len() as u16)?;
buf.write_u16::<BigEndian>(self.content_records.len() as u16)?;
buf.write_u16::<BigEndian>(self.boot_index)?;
buf.write_u16::<BigEndian>(self.minor_version)?;
// Iterate over content records and write out content record data.
for content in self.content_records.borrow().iter() {
for content in self.content_records.iter() {
buf.write_u32::<BigEndian>(content.content_id)?;
buf.write_u16::<BigEndian>(content.index)?;
match content.content_type {
@@ -258,6 +256,76 @@ impl TMD {
Ok(buf)
}
/// Gets the type of the signature on the TMD.
pub fn signature_type(&self) -> u32 {
self.signature_type
}
/// Gets the signature of the TMD.
pub fn signature(&self) -> [u8; 256] {
self.signature
}
/// Gets the version of the TMD file.
pub fn tmd_version(&self) -> u8 {
self.tmd_version
}
/// Gets the version of CA CRL listed in the TMD.
pub fn ca_crl_version(&self) -> u8 {
self.ca_crl_version
}
/// Gets the version of the signer CRL listed in the TMD.
pub fn signer_crl_version(&self) -> u8 {
self.signer_crl_version
}
/// Gets the group ID listed in the TMD.
pub fn group_id(&self) -> u16 {
self.group_id
}
/// Gets the age ratings listed in the TMD.
pub fn ratings(&self) -> [u8; 16] {
self.ratings
}
/// Gets the ipc mask listed in the TMD.
pub fn ipc_mask(&self) -> [u8; 12] {
self.ipc_mask
}
/// Gets the version of title listed in the TMD.
pub fn title_version(&self) -> u16 {
self.title_version
}
/// Gets the number of contents listed in the TMD.
pub fn num_contents(&self) -> u16 {
self.num_contents
}
/// Gets the index of the title's boot content.
pub fn boot_index(&self) -> u16 {
self.boot_index
}
/// Gets the minor version listed in the TMD. This field is typically unused.
pub fn minor_version(&self) -> u16 {
self.minor_version
}
/// Gets a reference to the content records from the TMD.
pub fn content_records(&self) -> &Vec<ContentRecord> {
&self.content_records
}
/// Sets the content records in the TMD.
pub fn set_content_records(&mut self, content_records: &Vec<ContentRecord>) {
self.content_records = content_records.clone()
}
/// Gets whether a TMD is fakesigned using the strncmp (trucha) bug or not.
pub fn is_fakesigned(&self) -> bool {
// Can't be fakesigned without a null signature.
@@ -331,11 +399,11 @@ impl TMD {
// Find possible content indices, because the provided one could exist while the indices
// are out of order, which could cause problems finding the content.
let mut content_indices = Vec::new();
for record in self.content_records.borrow().iter() {
for record in self.content_records.iter() {
content_indices.push(record.index);
}
let target_index = content_indices.index(index);
match self.content_records.borrow()[*target_index as usize].content_type {
match self.content_records[*target_index as usize].content_type {
ContentType::Normal => ContentType::Normal,
ContentType::Development => ContentType::Development,
ContentType::HashTree => ContentType::HashTree,
@@ -365,10 +433,15 @@ impl TMD {
Ok(())
}
/// Gets whether a TMD describes a vWii title or not.
/// Gets whether a TMD describes a vWii title.
pub fn is_vwii(&self) -> bool {
self.is_vwii == 1
}
/// Sets whether a TMD describes a vWii title.
pub fn set_is_vwii(&mut self, value: bool) {
self.is_vwii = value as u8;
}
/// Gets the Title ID of a TMD.
pub fn title_id(&self) -> [u8; 8] {

View File

@@ -1,5 +1,5 @@
// title/versions.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/versions.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Handles converting Title version formats, and provides Wii Menu version constants.

View File

@@ -1,5 +1,5 @@
// title/wad.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
// title/wad.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustwii
//
// Implements the structures and methods required for WAD parsing and editing.
@@ -32,16 +32,16 @@ pub enum WADType {
#[derive(Debug)]
/// A structure that represents an entire WAD file as a separate header and body.
pub struct WAD {
pub header: WADHeader,
pub body: WADBody,
header: WADHeader,
body: WADBody,
}
#[derive(Debug)]
/// A structure that represents the header of a WAD file.
pub struct WADHeader {
pub header_size: u32,
pub wad_type: WADType,
pub wad_version: u16,
header_size: u32,
wad_type: WADType,
wad_version: u16,
cert_chain_size: u32,
crl_size: u32,
ticket_size: u32,
@@ -93,6 +93,51 @@ impl WADHeader {
};
Ok(header)
}
/// Gets the size of the header data.
pub fn header_size(&self) -> u32 {
self.header_size
}
/// Gets the type of WAD described by the header.
pub fn wad_type(&self) -> &WADType {
&self.wad_type
}
/// Gets the version of the WAD described by the header.
pub fn wad_version(&self) -> u16 {
self.wad_version
}
/// Gets the size of the certificate chain defined in the header.
pub fn cert_chain_size(&self) -> u32 {
self.cert_chain_size
}
/// Gets the size of the CRL defined in the header.
pub fn crl_size(&self) -> u32 {
self.crl_size
}
/// Gets the size of the Ticket defined in the header.
pub fn ticket_size(&self) -> u32 {
self.ticket_size
}
/// Gets the size of the TMD defined in the header.
pub fn tmd_size(&self) -> u32 {
self.tmd_size
}
/// Gets the size of the content defined in the header.
pub fn content_size(&self) -> u32 {
self.content_size
}
/// Gets the size of the metadata defined in the header.
pub fn meta_size(&self) -> u32 {
self.meta_size
}
}
impl WADBody {
@@ -236,6 +281,11 @@ impl WAD {
buf.resize((buf.len() + 63) & !63, 0);
Ok(buf)
}
/// Gets the type of the WAD.
pub fn wad_type(&self) -> &WADType {
self.header.wad_type()
}
pub fn cert_chain_size(&self) -> u32 { self.header.cert_chain_size }