diff --git a/src/archive/mod.rs b/src/archive/mod.rs new file mode 100644 index 0000000..eca7b5a --- /dev/null +++ b/src/archive/mod.rs @@ -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; diff --git a/src/archive/u8.rs b/src/archive/u8.rs new file mode 100644 index 0000000..98bdbb9 --- /dev/null +++ b/src/archive/u8.rs @@ -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. + diff --git a/src/lib.rs b/src/lib.rs index 93c65c7..18bcfe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/title/cert.rs b/src/title/cert.rs index dc4e14a..80c14d5 100644 --- a/src/title/cert.rs +++ b/src/title/cert.rs @@ -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, @@ -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, std::io::Error> { let mut buf: Vec = 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 { 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 { 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 { 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, std::io::Error> { let mut buf: Vec = 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 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::(), &tmd_hash, ticket.signature.as_slice()) { + match root_key.verify(Pkcs1v15Sign::new::(), &ticket_hash, ticket.signature.as_slice()) { Ok(_) => Ok(true), Err(_) => Ok(false), } } - diff --git a/src/title/commonkeys.rs b/src/title/commonkeys.rs index 2037a24..9697c09 100644 --- a/src/title/commonkeys.rs +++ b/src/title/commonkeys.rs @@ -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) -> [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 diff --git a/src/title/content.rs b/src/title/content.rs index dfd6e9a..2c73a6a 100644 --- a/src/title/content.rs +++ b/src/title/content.rs @@ -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, 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, std::io::Error> { let mut buf: Vec = 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, 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, 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, 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, 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); diff --git a/src/title/crypto.rs b/src/title/crypto.rs index 4996957..2e0befc 100644 --- a/src/title/crypto.rs +++ b/src/title/crypto.rs @@ -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) -> [u8; 16] { let iv = title_id_to_iv(title_id); type Aes128CbcDec = cbc::Decryptor; @@ -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) -> [u8; 16] { let iv = title_id_to_iv(title_id); type Aes128CbcEnc = cbc::Encryptor; @@ -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 { 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 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 { let mut iv = Vec::from(index.to_be_bytes()); iv.resize(16, 0); diff --git a/src/title/mod.rs b/src/title/mod.rs index c3c32c0..de2065a 100644 --- a/src/title/mod.rs +++ b/src/title/mod.rs @@ -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, @@ -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 { 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 { // 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 { 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, 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, content::ContentError> { let content = self.content.get_content_by_cid(cid, self.ticket.dec_title_key())?; Ok(content) diff --git a/src/title/ticket.rs b/src/title/ticket.rs index 345801c..f489f73 100644 --- a/src/title/ticket.rs +++ b/src/title/ticket.rs @@ -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 { let mut buf = Cursor::new(data); let signature_type = buf.read_u32::().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, std::io::Error> { let mut buf: Vec = Vec::new(); buf.write_u32::(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() } diff --git a/src/title/tmd.rs b/src/title/tmd.rs index 4b0e45e..4be766e 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -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() } diff --git a/src/title/versions.rs b/src/title/versions.rs index 40b14d1..5ebef61 100644 --- a/src/title/versions.rs +++ b/src/title/versions.rs @@ -69,6 +69,10 @@ fn wii_menu_versions_map(vwii: Option) -> HashMap { 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) -> Option { if title_id == "0000000100000002" { let map = wii_menu_versions_map(vwii); diff --git a/src/title/wad.rs b/src/title/wad.rs index d65e986..8401f4b 100644 --- a/src/title/wad.rs +++ b/src/title/wad.rs @@ -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, crl: Vec, @@ -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 { // 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 { 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 { let mut buf = Cursor::new(data); let header_size = buf.read_u32::().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 { 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, WADError> { let mut buf = Vec::new(); buf.write_u32::(self.header.header_size).map_err(WADError::IOError)?;