Added basic doc strings to all major structs and functions
Some checks failed
Build rustii / build-linux-x86_64 (push) Has been cancelled
Build rustii / build-macos-arm64 (push) Has been cancelled
Build rustii / build-macos-x86_64 (push) Has been cancelled
Build rustii / build-windows-x86_64 (push) Has been cancelled

This commit is contained in:
Campbell 2025-03-28 14:02:03 -04:00
parent edf3af0f7c
commit e147a953a5
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
12 changed files with 91 additions and 19 deletions

6
src/archive/mod.rs Normal file
View File

@ -0,0 +1,6 @@
// archive/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
//
// Root for all archive-related modules.
pub mod u8;

5
src/archive/u8.rs Normal file
View File

@ -0,0 +1,5 @@
// archive/u8.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
//
// Implements the structures and methods required for parsing U8 archives.

View File

@ -1,5 +1,7 @@
// lib.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
//
// Root level module that imports the feature modules.
pub mod archive;
pub mod title;

View File

@ -49,6 +49,7 @@ pub enum CertificateKeyType {
}
#[derive(Debug, Clone)]
/// A structure that represents the components of a Wii signing certificate.
pub struct Certificate {
signer_key_type: CertificateKeyType,
signature: Vec<u8>,
@ -123,7 +124,7 @@ impl Certificate {
})
}
/// Dumps the data in a Certificate back into binary data that can be written to a file.
/// Dumps the data in a Certificate instance 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();
match self.signer_key_type {
@ -154,24 +155,29 @@ impl Certificate {
Ok(buf)
}
/// Gets the name of the certificate used to sign a certificate as a string.
pub fn signature_issuer(&self) -> String {
String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned()
}
/// Gets the name of a certificate's child certificate as a string.
pub fn child_cert_identity(&self) -> String {
String::from_utf8_lossy(&self.child_cert_identity).trim_end_matches('\0').to_owned()
}
/// Gets the modulus of the public key contained in a certificate.
pub fn pub_key_modulus(&self) -> Vec<u8> {
self.pub_key_modulus.clone()
}
/// Gets the exponent of the public key contained in a certificate.
pub fn pub_key_exponent(&self) -> u32 {
self.pub_key_exponent
}
}
#[derive(Debug)]
/// A structure that represents the components of the Wii's signing certificate chain.
pub struct CertificateChain {
ca_cert: Certificate,
tmd_cert: Certificate,
@ -179,6 +185,9 @@ pub struct CertificateChain {
}
impl CertificateChain {
/// Creates a new CertificateChain instance from the binary data of an entire certificate chain.
/// This chain must contain a CA certificate, a TMD certificate, and a Ticket certificate or
/// else this method will return an error.
pub fn from_bytes(data: &[u8]) -> Result<CertificateChain, CertificateError> {
let mut buf = Cursor::new(data);
let mut offset: u64 = 0;
@ -238,6 +247,10 @@ impl CertificateChain {
})
}
/// Creates a new CertificateChain instance from three separate Certificate instances each
/// containing one of the three certificates stored in the chain. You must provide a CA
/// certificate, a TMD certificate, and a Ticket certificate, or this method will return an
/// error.
pub fn from_certs(ca_cert: Certificate, tmd_cert: Certificate, ticket_cert: Certificate) -> Result<Self, CertificateError> {
if String::from_utf8_lossy(&ca_cert.signature_issuer).trim_end_matches('\0').ne("Root") {
return Err(CertificateError::IncorrectCertificate("CA".to_owned()));
@ -255,6 +268,7 @@ impl CertificateChain {
})
}
/// Dumps the entire CertificateChain 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();
buf.write_all(&self.ca_cert().to_bytes()?)?;
@ -316,8 +330,7 @@ pub fn verify_child_cert(ca_cert: &Certificate, child_cert: &Certificate) -> Res
return Err(CertificateError::NonMatchingCertificates)
}
let mut hasher = Sha1::new();
let cert_body = child_cert.to_bytes().unwrap();
hasher.update(&cert_body[320..]);
hasher.update(&child_cert.to_bytes().map_err(CertificateError::IOError)?[320..]);
let cert_hash = hasher.finalize().as_slice().to_owned();
let public_key_modulus = BigUint::from_bytes_be(&ca_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(ca_cert.pub_key_exponent());
@ -339,8 +352,7 @@ pub fn verify_tmd(tmd_cert: &Certificate, tmd: &tmd::TMD) -> Result<bool, Certif
return Err(CertificateError::NonMatchingCertificates)
}
let mut hasher = Sha1::new();
let tmd_body = tmd.to_bytes().unwrap();
hasher.update(&tmd_body[320..]);
hasher.update(&tmd.to_bytes().map_err(CertificateError::IOError)?[320..]);
let tmd_hash = hasher.finalize().as_slice().to_owned();
let public_key_modulus = BigUint::from_bytes_be(&tmd_cert.pub_key_modulus());
let public_key_exponent = BigUint::from(tmd_cert.pub_key_exponent());
@ -362,15 +374,13 @@ pub fn verify_ticket(ticket_cert: &Certificate, ticket: &ticket::Ticket) -> Resu
return Err(CertificateError::NonMatchingCertificates)
}
let mut hasher = Sha1::new();
let tmd_body = ticket.to_bytes().unwrap();
hasher.update(&tmd_body[320..]);
let tmd_hash = hasher.finalize().as_slice().to_owned();
hasher.update(&ticket.to_bytes().map_err(CertificateError::IOError)?[320..]);
let ticket_hash = hasher.finalize().as_slice().to_owned();
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>(), &tmd_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

@ -6,6 +6,8 @@ const KOREAN_KEY: &str = "63b82bb4f4614e2e13f2fefbba4c9b7e";
const VWII_KEY: &str = "30bfc76e7c19afbb23163330ced7c28d";
const DEV_COMMON_KEY: &str = "a1604a6a7123b529ae8bec32c816fcaa";
/// Returns the common key for the specified index. Providing Some(true) for the optional argument
/// is_dev will make index 0 return the development common key instead of the retail common key.
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.
// The is_dev argument is an option, and if it's set to false or None, then the regular

View File

@ -31,6 +31,7 @@ impl fmt::Display for ContentError {
impl Error for 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: Vec<ContentRecord>,
pub content_region_size: u32,
@ -97,6 +98,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.num_contents {
@ -107,12 +109,14 @@ impl ContentRegion {
}
Ok(buf)
}
/// 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::IndexNotFound)?;
Ok(content.clone())
}
/// Gets the decrypted content file from the ContentRegion at the specified index.
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.
@ -127,6 +131,7 @@ impl ContentRegion {
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.iter().position(|x| x.content_id == cid);
if let Some(index) = index {
@ -136,7 +141,8 @@ impl ContentRegion {
Err(ContentError::CIDNotFound)
}
}
/// 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.iter().position(|x| x.content_id == cid);
if let Some(index) = index {
@ -147,6 +153,9 @@ impl ContentRegion {
}
}
/// Loads existing content into the specified index of a ContentRegion instance. This content
/// 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.len() {
return Err(ContentError::IndexNotFound);

View File

@ -14,7 +14,7 @@ fn title_id_to_iv(title_id: [u8; 8]) -> [u8; 16] {
iv.as_slice().try_into().unwrap()
}
// Decrypt a Title Key using the specified common key.
/// Decrypts a Title Key using the specified common key and the corresponding Title ID.
pub fn decrypt_title_key(title_key_enc: [u8; 16], common_key_index: u8, title_id: [u8; 8], is_dev: Option<bool>) -> [u8; 16] {
let iv = title_id_to_iv(title_id);
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
@ -24,7 +24,7 @@ pub fn decrypt_title_key(title_key_enc: [u8; 16], common_key_index: u8, title_id
title_key
}
// Encrypt a Title Key using the specified common key.
/// Encrypts a Title Key using the specified common key and the corresponding Title ID.
pub fn encrypt_title_key(title_key_dec: [u8; 16], common_key_index: u8, title_id: [u8; 8], is_dev: Option<bool>) -> [u8; 16] {
let iv = title_id_to_iv(title_id);
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
@ -34,7 +34,7 @@ pub fn encrypt_title_key(title_key_dec: [u8; 16], common_key_index: u8, title_id
title_key
}
// Decrypt content using a Title Key.
/// Decrypt content using the corresponding Title Key and content index.
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);
@ -45,7 +45,7 @@ pub fn decrypt_content(data: &[u8], title_key: [u8; 16], index: u16) -> Vec<u8>
buf
}
// Encrypt content using a Title Key.
/// Encrypt content using the corresponding Title Key and content index.
pub fn encrypt_content(data: &[u8], title_key: [u8; 16], index: u16, size: u64) -> Vec<u8> {
let mut iv = Vec::from(index.to_be_bytes());
iv.resize(16, 0);

View File

@ -50,6 +50,7 @@ impl fmt::Display for TitleError {
impl Error for TitleError {}
#[derive(Debug)]
/// A structure that represents the components of a digital Wii title.
pub struct Title {
pub cert_chain: cert::CertificateChain,
crl: Vec<u8>,
@ -60,6 +61,7 @@ pub struct Title {
}
impl Title {
/// Creates a new Title instance from an existing WAD instance.
pub fn from_wad(wad: &wad::WAD) -> Result<Title, TitleError> {
let cert_chain = cert::CertificateChain::from_bytes(&wad.cert_chain()).map_err(|_| TitleError::BadCertChain)?;
let ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(|_| TitleError::BadTicket)?;
@ -76,6 +78,7 @@ impl Title {
Ok(title)
}
/// Converts a Title instance into a WAD, which can be used to export the Title back to a file.
pub fn to_wad(&self) -> Result<wad::WAD, TitleError> {
// Create a new WAD from the data in the Title.
let wad = wad::WAD::from_parts(
@ -89,16 +92,19 @@ impl Title {
Ok(wad)
}
/// Creates a new Title instance from the binary data of a WAD file.
pub fn from_bytes(bytes: &[u8]) -> Result<Title, TitleError> {
let wad = wad::WAD::from_bytes(bytes).map_err(|_| TitleError::InvalidWAD)?;
let title = Title::from_wad(&wad)?;
Ok(title)
}
/// Gets whether the TMD and Ticket of a Title are both fakesigned.
pub fn is_fakesigned(&self) -> bool {
self.tmd.is_fakesigned() && self.ticket.is_fakesigned()
}
/// Fakesigns the TMD and Ticket of a Title.
pub fn fakesign(&mut self) -> Result<(), TitleError> {
// Run the fakesign methods on the TMD and Ticket.
self.tmd.fakesign().map_err(TitleError::TMDError)?;
@ -106,11 +112,13 @@ impl Title {
Ok(())
}
/// 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())?;
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())?;
Ok(content)

View File

@ -41,6 +41,7 @@ pub struct TitleLimit {
}
#[derive(Debug)]
/// A structure that represents a Wii Ticket file.
pub struct Ticket {
pub signature_type: u32,
pub signature: [u8; 256],
@ -67,6 +68,7 @@ pub struct Ticket {
}
impl Ticket {
/// Creates a new Ticket instance from the binary data of a Ticket file.
pub fn from_bytes(data: &[u8]) -> Result<Self, TicketError> {
let mut buf = Cursor::new(data);
let signature_type = buf.read_u32::<BigEndian>().map_err(TicketError::IOError)?;
@ -145,6 +147,7 @@ impl Ticket {
})
}
/// Dumps the data in a Ticket instance 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();
buf.write_u32::<BigEndian>(self.signature_type)?;
@ -176,18 +179,21 @@ impl Ticket {
Ok(buf)
}
/// Gets the decrypted version of the Title Key stored in a Ticket.
pub fn dec_title_key(&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, Some(is_dev))
}
/// Gets whether a Ticket was signed for development (true) or retail (false).
pub fn is_dev(&self) -> bool {
// Parse the signature issuer to determine if this is a dev Ticket or not.
let issuer_str = String::from_utf8(Vec::from(&self.signature_issuer)).unwrap_or_default();
issuer_str.contains("Root-CA00000002-XS00000004") || issuer_str.contains("Root-CA00000002-XS00000006")
}
/// Gets whether a Ticket is fakesigned using the strncmp (trucha) bug or not.
pub fn is_fakesigned(&self) -> bool {
// Can't be fakesigned without a null signature.
if self.signature != [0; 256] {
@ -204,6 +210,7 @@ impl Ticket {
true
}
/// Fakesigns a Ticket for use with the strncmp (trucha) bug.
pub fn fakesign(&mut self) -> Result<(), TicketError> {
// Erase the signature.
self.signature = [0; 256];
@ -221,6 +228,7 @@ impl Ticket {
Ok(())
}
/// Gets the name of the certificate used to sign a Ticket as a string.
pub fn signature_issuer(&self) -> String {
String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned()
}

View File

@ -83,6 +83,7 @@ pub enum AccessRight {
}
#[derive(Debug, Clone)]
/// A structure that represents the metadata of a content file in a digital Wii title.
pub struct ContentRecord {
pub content_id: u32,
pub index: u16,
@ -92,6 +93,7 @@ pub struct ContentRecord {
}
#[derive(Debug)]
/// A structure that represents a Wii TMD (Title Metadata) file.
pub struct TMD {
pub signature_type: u32,
pub signature: [u8; 256],
@ -257,6 +259,7 @@ impl TMD {
Ok(buf)
}
/// 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.
if self.signature != [0; 256] {
@ -273,6 +276,7 @@ impl TMD {
true
}
/// Fakesigns a TMD for use with the strncmp (trucha) bug.
pub fn fakesign(&mut self) -> Result<(), TMDError> {
// Erase the signature.
self.signature = [0; 256];
@ -290,6 +294,7 @@ impl TMD {
Ok(())
}
/// Gets the 3-letter code of the region a TMD was created for.
pub fn region(&self) -> &str {
match self.region {
0 => "JPN",
@ -301,6 +306,7 @@ impl TMD {
}
}
/// Gets the type of title described by a TMD.
pub fn title_type(&self) -> TitleType {
match hex::encode(self.title_id)[..8].to_string().as_str() {
"00000001" => TitleType::System,
@ -314,6 +320,7 @@ impl TMD {
}
}
/// Gets the type of content described by a content record in a TMD.
pub fn content_type(&self, index: usize) -> ContentType {
// Find possible content indices, because the provided one could exist while the indices
// are out of order, which could cause problems finding the content.
@ -331,13 +338,15 @@ impl TMD {
}
}
/// Gets whether a specified access right is enabled in a TMD.
pub fn check_access_right(&self, right: AccessRight) -> bool {
match right {
AccessRight::AHB => (self.access_rights & (1 << 0)) != 0,
AccessRight::DVDVideo => (self.access_rights & (1 << 1)) != 0,
}
}
/// Gets the name of the certificate used to sign a TMD as a string.
pub fn signature_issuer(&self) -> String {
String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned()
}

View File

@ -69,6 +69,10 @@ fn wii_menu_versions_map(vwii: Option<bool>) -> HashMap<u16, String> {
menu_versions
}
/// Converts the decimal version of a title (vXXX) into a more standard format for applicable
/// titles. For the Wii Menu, this uses the optional vwii argument and a hash table to determine
/// the user-friendly version number, as there is no way to directly derive it from the decimal
/// format.
pub fn dec_to_standard(version: u16, title_id: &str, vwii: Option<bool>) -> Option<String> {
if title_id == "0000000100000002" {
let map = wii_menu_versions_map(vwii);

View File

@ -41,12 +41,14 @@ 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,
}
#[derive(Debug)]
/// A structure that represents the header of a WAD file.
pub struct WADHeader {
pub header_size: u32,
pub wad_type: WADType,
@ -61,6 +63,7 @@ pub struct WADHeader {
}
#[derive(Debug)]
/// A structure that represent the data contained in the body of a WAD file.
pub struct WADBody {
cert_chain: Vec<u8>,
crl: Vec<u8>,
@ -71,6 +74,7 @@ pub struct WADBody {
}
impl WADHeader {
/// Creates a new WADHeader instance from the binary data of a WAD file's header.
pub fn from_body(body: &WADBody) -> Result<WADHeader, WADError> {
// Generates a new WADHeader from a populated WADBody object.
// Parse the TMD and use that to determine if this is a standard WAD or a boot2 WAD.
@ -103,6 +107,7 @@ impl WADHeader {
}
impl WADBody {
/// Creates a new WADBody instance from instances of the components stored in a WAD file.
pub fn from_parts(cert_chain: &cert::CertificateChain, crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD,
content: &content::ContentRegion, meta: &[u8]) -> Result<WADBody, WADError> {
let body = WADBody {
@ -118,6 +123,7 @@ impl WADBody {
}
impl WAD {
/// Creates a new WAD instance from the binary data of a WAD file.
pub fn from_bytes(data: &[u8]) -> Result<WAD, WADError> {
let mut buf = Cursor::new(data);
let header_size = buf.read_u32::<BigEndian>().map_err(WADError::IOError)?;
@ -196,6 +202,8 @@ impl WAD {
Ok(wad)
}
/// Creates a new WAD instance from instances of the components stored in a WAD file. This
/// first creates a WADBody from the components, then generates a new WADHeader from them.
pub fn from_parts(cert_chain: &cert::CertificateChain, crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD,
content: &content::ContentRegion, meta: &[u8]) -> Result<WAD, WADError> {
let body = WADBody::from_parts(cert_chain, crl, ticket, tmd, content, meta)?;
@ -206,7 +214,8 @@ impl WAD {
};
Ok(wad)
}
/// Dumps the data in a WAD instance back into binary data that can be written to a file.
pub fn to_bytes(&self) -> Result<Vec<u8>, WADError> {
let mut buf = Vec::new();
buf.write_u32::<BigEndian>(self.header.header_size).map_err(WADError::IOError)?;