diff --git a/.gitignore b/.gitignore index 8814bc1..6da4660 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ target/ *.tmd *.tik *.cert +*.footer +*.app diff --git a/Cargo.lock b/Cargo.lock index 74cb54e..f4079f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "heck" version = "0.5.0" @@ -263,6 +269,7 @@ dependencies = [ "byteorder", "cbc", "clap", + "glob", "hex", "sha1", ] diff --git a/Cargo.toml b/Cargo.toml index 06908c5..a185eff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,4 +29,5 @@ cbc = "0" aes = "0" hex = "0" sha1 = "0" +glob = "0" clap = { version = "4", features = ["derive"] } diff --git a/src/bin/playground/main.rs b/src/bin/playground/main.rs index ee10f39..b853fa8 100644 --- a/src/bin/playground/main.rs +++ b/src/bin/playground/main.rs @@ -2,9 +2,13 @@ use std::fs; use rustii::title::{tmd, ticket, content, crypto, wad}; +use rustii::title; fn main() { let data = fs::read("sm.wad").unwrap(); + let title = title::Title::from_bytes(&data).unwrap(); + println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id)); + let wad = wad::WAD::from_bytes(&data).unwrap(); println!("size of tmd: {:?}", wad.tmd().len()); let tmd = tmd::TMD::from_bytes(&wad.tmd()).unwrap(); diff --git a/src/bin/rustii/title/wad.rs b/src/bin/rustii/title/wad.rs index 2863675..bafe27e 100644 --- a/src/bin/rustii/title/wad.rs +++ b/src/bin/rustii/title/wad.rs @@ -3,10 +3,12 @@ // // Code for WAD-related commands in the rustii CLI. -use clap::Subcommand; use std::{str, fs}; -use std::path::Path; -use rustii::title::{tmd, ticket, wad, content}; +use std::path::{Path, PathBuf}; +use clap::Subcommand; +use glob::glob; +use rustii::title::{tmd, ticket, content, wad}; +use rustii::title; #[derive(Subcommand)] #[command(arg_required_else_help = true)] @@ -24,35 +26,93 @@ pub enum Commands { } pub fn pack_wad(input: &str, output: &str) { - print!("packing"); + let in_path = Path::new(input); + if !in_path.exists() { + panic!("Error: Source directory does not exist."); + } + // Read TMD file (only accept one file). + let tmd_files: Vec = glob(&format!("{}/*.tmd", in_path.display())) + .expect("failed to read glob pattern") + .filter_map(|f| f.ok()).collect(); + if tmd_files.is_empty() { + panic!("Error: No TMD file found in the source directory."); + } else if tmd_files.len() > 1 { + panic!("Error: More than one TMD file found in the source directory.") + } + let tmd = tmd::TMD::from_bytes(&fs::read(&tmd_files[0]).expect("could not read TMD file")).unwrap(); + // Read Ticket file (only accept one file). + let ticket_files: Vec = glob(&format!("{}/*.tik", in_path.display())) + .expect("failed to read glob pattern") + .filter_map(|f| f.ok()).collect(); + if ticket_files.is_empty() { + panic!("Error: No Ticket file found in the source directory."); + } else if ticket_files.len() > 1 { + panic!("Error: More than one Ticket file found in the source directory.") + } + let tik = ticket::Ticket::from_bytes(&fs::read(&ticket_files[0]).expect("could not read Ticket file")).unwrap(); + // Read cert chain (only accept one file). + let cert_files: Vec = glob(&format!("{}/*.cert", in_path.display())) + .expect("failed to read glob pattern") + .filter_map(|f| f.ok()).collect(); + if cert_files.is_empty() { + panic!("Error: No cert file found in the source directory."); + } else if cert_files.len() > 1 { + panic!("Error: More than one Cert file found in the source directory.") + } + let cert_chain = fs::read(&cert_files[0]).expect("could not read cert chain file"); + // Read footer, if one exists (only accept one file). + let footer_files: Vec = glob(&format!("{}/*.footer", in_path.display())) + .expect("failed to read glob pattern") + .filter_map(|f| f.ok()).collect(); + let mut footer: Vec = Vec::new(); + if footer_files.len() == 1 { + footer = fs::read(&footer_files[0]).unwrap(); + } + // Iterate over expected content and read it into a content region. + let mut content_region = content::ContentRegion::new(tmd.content_records.clone()).expect("could not create content region"); + for content in tmd.content_records.clone() { + let data = fs::read(format!("{}/{:08X}.app", in_path.display(), content.index)).expect("could not read required content"); + content_region.load_content(&data, content.index as usize, tik.dec_title_key()).expect("failed to load content into ContentRegion"); + } + let wad = wad::WAD::from_parts(&cert_chain, &[], &tik, &tmd, &content_region, &footer).expect("failed to create WAD"); + // Write out WAD file. + let mut out_path = PathBuf::from(output); + match out_path.extension() { + Some(ext) => { + if ext != "wad" { + out_path.set_extension("wad"); + } + }, + None => { + out_path.set_extension("wad"); + } + } + fs::write(out_path, wad.to_bytes().unwrap()).expect("could not write to wad file"); + println!("WAD file packed!"); } pub fn unpack_wad(input: &str, output: &str) { let wad_file = fs::read(input).expect("could not read WAD"); - let wad = wad::WAD::from_bytes(&wad_file).expect("could not parse WAD"); - let tmd = tmd::TMD::from_bytes(&wad.tmd()).expect("could not parse TMD"); - let tik = ticket::Ticket::from_bytes(&wad.ticket()).expect("could not parse Ticket"); - let cert_data = &wad.cert_chain(); - let meta_data = &wad.meta(); + let title = title::Title::from_bytes(&wad_file).unwrap(); + let tid = hex::encode(title.tmd.title_id); // Create output directory if it doesn't exist. if !Path::new(output).exists() { fs::create_dir(output).expect("could not create output directory"); } let out_path = Path::new(output); // Write out all WAD components. - let tmd_file_name = format!("{}.tmd", hex::encode(tmd.title_id)); - fs::write(Path::join(out_path, tmd_file_name), tmd.to_bytes().unwrap()).expect("could not write TMD file"); - let ticket_file_name = format!("{}.tik", hex::encode(tmd.title_id)); - fs::write(Path::join(out_path, ticket_file_name), tik.to_bytes().unwrap()).expect("could not write Ticket file"); - let cert_file_name = format!("{}.cert", hex::encode(tmd.title_id)); - fs::write(Path::join(out_path, cert_file_name), cert_data).expect("could not write Cert file"); - let meta_file_name = format!("{}.footer", hex::encode(tmd.title_id)); - fs::write(Path::join(out_path, meta_file_name), meta_data).expect("could not write footer file"); + let tmd_file_name = format!("{}.tmd", tid); + fs::write(Path::join(out_path, tmd_file_name), title.tmd.to_bytes().unwrap()).expect("could not write TMD file"); + let ticket_file_name = format!("{}.tik", tid); + fs::write(Path::join(out_path, ticket_file_name), title.ticket.to_bytes().unwrap()).expect("could not write Ticket file"); + let cert_file_name = format!("{}.cert", tid); + fs::write(Path::join(out_path, cert_file_name), title.cert_chain()).expect("could not write Cert file"); + let meta_file_name = format!("{}.footer", tid); + fs::write(Path::join(out_path, meta_file_name), title.meta()).expect("could not write footer file"); // Iterate over contents, decrypt them, and write them out. - let content_region = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records).unwrap(); - for i in 0..tmd.num_contents { - let content_file_name = format!("{:08X}.app", content_region.content_records[i as usize].index); - let dec_content = content_region.get_content_by_index(i as usize, tik.dec_title_key()).unwrap(); + for i in 0..title.tmd.num_contents { + let content_file_name = format!("{:08X}.app", title.content.content_records[i as usize].index); + let dec_content = title.get_content_by_index(i as usize).unwrap(); fs::write(Path::join(out_path, content_file_name), dec_content).unwrap(); } println!("WAD file unpacked!"); diff --git a/src/title/content.rs b/src/title/content.rs index 27bd368..954f7f1 100644 --- a/src/title/content.rs +++ b/src/title/content.rs @@ -8,7 +8,7 @@ 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; +use crate::title::crypto; #[derive(Debug)] pub enum ContentError { @@ -40,6 +40,8 @@ pub struct ContentRegion { } 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: Vec) -> Result { let content_region_size = data.len() as u32; let num_contents = content_records.len() as u16; @@ -76,6 +78,24 @@ 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: Vec) -> Result { + 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.len() as u16; + let content_start_offsets: Vec = Vec::new(); + let mut contents: Vec> = Vec::new(); + contents.resize(num_contents as usize, Vec::new()); + Ok(ContentRegion { + content_records, + content_region_size, + num_contents, + content_start_offsets, + contents, + }) + } + pub fn to_bytes(&self) -> Result, std::io::Error> { let mut buf: Vec = Vec::new(); for i in 0..self.num_contents { @@ -95,7 +115,7 @@ impl ContentRegion { 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. - let mut content_dec = decrypt_content(&content, title_key, self.content_records[index].index); + 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()); @@ -125,4 +145,21 @@ impl ContentRegion { Err(ContentError::CIDNotFound) } } + + 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); + } + // 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[index].content_hash { + return Err(ContentError::BadHash); + } + let content_enc = crypto::encrypt_content(content, title_key, self.content_records[index].index, self.content_records[index].content_size); + self.contents[index] = content_enc; + Ok(()) + } } diff --git a/src/title/crypto.rs b/src/title/crypto.rs index da95843..7fee39c 100644 --- a/src/title/crypto.rs +++ b/src/title/crypto.rs @@ -52,6 +52,9 @@ pub fn encrypt_content(data: &[u8], title_key: [u8; 16], index: u16, size: u64) type Aes128CbcEnc = cbc::Encryptor; let encryptor = Aes128CbcEnc::new(&title_key.into(), iv.as_slice().into()); let mut buf = data.to_owned(); + let size = (size + 15) & !15; + buf.resize(size as usize, 0); encryptor.encrypt_padded_mut::(&mut buf, size as usize).unwrap(); + buf.resize(size as usize, 0); buf } diff --git a/src/title/mod.rs b/src/title/mod.rs index 9dbccc7..4b48402 100644 --- a/src/title/mod.rs +++ b/src/title/mod.rs @@ -1,5 +1,7 @@ // title/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustii +// +// Root for all title-related modules and implementation of the high-level Title object. pub mod commonkeys; pub mod content; @@ -7,3 +9,124 @@ pub mod crypto; pub mod ticket; pub mod tmd; pub mod wad; + +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub enum TitleError { + BadTicket, + BadTMD, + BadContent, + InvalidWAD, + WADError(wad::WADError), + IOError(std::io::Error), +} + +impl fmt::Display for TitleError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = match *self { + TitleError::BadTicket => "The provided Ticket data was invalid.", + TitleError::BadTMD => "The provided TMD data was invalid.", + TitleError::BadContent => "The provided content data was invalid.", + TitleError::InvalidWAD => "The provided WAD data was invalid.", + TitleError::WADError(_) => "A WAD could not be built from the provided data.", + TitleError::IOError(_) => "The provided Title data was invalid.", + }; + f.write_str(description) + } +} + +impl Error for TitleError {} + +#[derive(Debug)] +pub struct Title { + cert_chain: Vec, + crl: Vec, + pub ticket: ticket::Ticket, + pub tmd: tmd::TMD, + pub content: content::ContentRegion, + meta: Vec +} + +impl Title { + pub fn from_wad(wad: &wad::WAD) -> Result { + let ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(|_| TitleError::BadTicket)?; + let tmd = tmd::TMD::from_bytes(&wad.tmd()).map_err(|_| TitleError::BadTMD)?; + let content = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records.clone()).map_err(|_| TitleError::BadContent)?; + let title = Title { + cert_chain: wad.cert_chain(), + crl: wad.crl(), + ticket, + tmd, + content, + meta: wad.meta(), + }; + Ok(title) + } + + pub fn to_wad(&self) -> Result { + // Create a new WAD from the data in the Title. + let wad = wad::WAD::from_parts( + &self.cert_chain, + &self.crl, + &self.ticket, + &self.tmd, + &self.content, + &self.meta + ).map_err(TitleError::WADError)?; + Ok(wad) + } + + 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) + } + + 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) + } + + 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) + } + + pub fn cert_chain(&self) -> Vec { + self.cert_chain.clone() + } + + pub fn set_cert_chain(&mut self, cert_chain: &[u8]) { + self.cert_chain = cert_chain.to_vec(); + } + + pub fn crl(&self) -> Vec { + self.crl.clone() + } + + pub fn set_crl(&mut self, crl: &[u8]) { + self.crl = crl.to_vec(); + } + + pub fn set_ticket(&mut self, ticket: ticket::Ticket) { + self.ticket = ticket; + } + + pub fn set_tmd(&mut self, tmd: tmd::TMD) { + self.tmd = tmd; + } + + pub fn set_content(&mut self, content: content::ContentRegion) { + self.content = content; + } + + pub fn meta(&self) -> Vec { + self.meta.clone() + } + + pub fn set_meta(&mut self, meta: &[u8]) { + self.meta = meta.to_vec(); + } +} diff --git a/src/title/tmd.rs b/src/title/tmd.rs index 6500ed3..a2cbb94 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -7,6 +7,7 @@ use std::io::{Cursor, Read, Write}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; #[derive(Debug)] +#[derive(Clone)] pub struct ContentRecord { pub content_id: u32, pub index: u16, @@ -44,6 +45,7 @@ pub struct TMD { } impl TMD { + /// Creates a new TMD instance from the binary data of a TMD file. pub fn from_bytes(data: &[u8]) -> Result { let mut buf = Cursor::new(data); let signature_type = buf.read_u32::()?; @@ -129,6 +131,7 @@ impl TMD { }) } + /// Dumps the data in a TMD 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)?; diff --git a/src/title/wad.rs b/src/title/wad.rs index feca4e8..cf89b66 100644 --- a/src/title/wad.rs +++ b/src/title/wad.rs @@ -8,6 +8,7 @@ use std::fmt; use std::str; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use crate::title::{tmd, ticket, content}; #[derive(Debug)] pub enum WADError { @@ -28,7 +29,7 @@ impl fmt::Display for WADError { impl Error for WADError {} #[derive(Debug)] -pub enum WADTypes { +pub enum WADType { Installable, ImportBoot } @@ -42,7 +43,7 @@ pub struct WAD { #[derive(Debug)] pub struct WADHeader { pub header_size: u32, - pub wad_type: WADTypes, + pub wad_type: WADType, pub wad_version: u16, cert_chain_size: u32, crl_size: u32, @@ -63,6 +64,53 @@ pub struct WADBody { meta: Vec, } +impl WADHeader { + 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. + let tmd = tmd::TMD::from_bytes(&body.tmd).map_err(WADError::IOError)?; + let wad_type = match hex::encode(tmd.title_id).as_str() { + "0000000100000001" => WADType::ImportBoot, + _ => WADType::Installable, + }; + // Find the sizes of all components of the Title. + let cert_chain_size = body.cert_chain.len() as u32; + let crl_size = body.crl.len() as u32; + let ticket_size = body.ticket.len() as u32; + let tmd_size = body.tmd.len() as u32; + let content_size = body.content.len() as u32; + let meta_size = body.meta.len() as u32; + let header = WADHeader { + header_size: 32, + wad_type, + wad_version: 0, // This is always officially a zero. + cert_chain_size, + crl_size, + ticket_size, + tmd_size, + content_size, + meta_size, + padding: [0; 32], + }; + Ok(header) + } +} + +impl WADBody { + pub fn from_parts(cert_chain: &[u8], crl: &[u8], ticket: &ticket::Ticket, tmd: &tmd::TMD, + content: &content::ContentRegion, meta: &[u8]) -> Result { + let body = WADBody { + cert_chain: cert_chain.to_vec(), + crl: crl.to_vec(), + ticket: ticket.to_bytes().map_err(WADError::IOError)?, + tmd: tmd.to_bytes().map_err(WADError::IOError)?, + content: content.to_bytes().map_err(WADError::IOError)?, + meta: meta.to_vec(), + }; + Ok(body) + } +} + impl WAD { pub fn from_bytes(data: &[u8]) -> Result { let mut buf = Cursor::new(data); @@ -71,8 +119,8 @@ impl WAD { buf.read_exact(&mut wad_type).map_err(WADError::IOError)?; let wad_type = match str::from_utf8(&wad_type) { Ok(wad_type) => match wad_type { - "Is" => WADTypes::Installable, - "ib" => WADTypes::ImportBoot, + "Is" => WADType::Installable, + "ib" => WADType::ImportBoot, _ => return Err(WADError::BadType), }, Err(_) => return Err(WADError::BadType), @@ -141,13 +189,24 @@ impl WAD { }; Ok(wad) } + + pub fn from_parts(cert_chain: &[u8], 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)?; + let header = WADHeader::from_body(&body)?; + let wad = WAD { + header, + body, + }; + Ok(wad) + } pub fn to_bytes(&self) -> Result, WADError> { let mut buf = Vec::new(); buf.write_u32::(self.header.header_size).map_err(WADError::IOError)?; match self.header.wad_type { - WADTypes::Installable => { buf.write("Is".as_bytes()).map_err(WADError::IOError)?; }, - WADTypes::ImportBoot => { buf.write("ib".as_bytes()).map_err(WADError::IOError)?; }, + WADType::Installable => { buf.write("Is".as_bytes()).map_err(WADError::IOError)?; }, + WADType::ImportBoot => { buf.write("ib".as_bytes()).map_err(WADError::IOError)?; }, } buf.write_u16::(self.header.wad_version).map_err(WADError::IOError)?; buf.write_u32::(self.header.cert_chain_size).map_err(WADError::IOError)?; @@ -177,24 +236,54 @@ impl WAD { pub fn cert_chain(&self) -> Vec { self.body.cert_chain.clone() } + + pub fn set_cert_chain(&mut self, cert_chain: &[u8]) { + self.body.cert_chain = cert_chain.to_vec(); + self.header.cert_chain_size = cert_chain.len() as u32; + } pub fn crl(&self) -> Vec { self.body.crl.clone() } + + pub fn set_crl(&mut self, crl: &[u8]) { + self.body.crl = crl.to_vec(); + self.header.crl_size = crl.len() as u32; + } pub fn ticket(&self) -> Vec { self.body.ticket.clone() } + + pub fn set_ticket(&mut self, ticket: &[u8]) { + self.body.ticket = ticket.to_vec(); + self.header.ticket_size = ticket.len() as u32; + } pub fn tmd(&self) -> Vec { self.body.tmd.clone() } + + pub fn set_tmd(&mut self, tmd: &[u8]) { + self.body.tmd = tmd.to_vec(); + self.header.tmd_size = tmd.len() as u32; + } pub fn content(&self) -> Vec { self.body.content.clone() } + + pub fn set_content(&mut self, content: &[u8]) { + self.body.content = content.to_vec(); + self.header.content_size = content.len() as u32; + } pub fn meta(&self) -> Vec { self.body.meta.clone() } + + pub fn set_meta(&mut self, meta: &[u8]) { + self.body.meta = meta.to_vec(); + self.header.meta_size = meta.len() as u32; + } }