diff --git a/.gitignore b/.gitignore index 5a8bfa6..8814bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ target/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ + +# Wii Files +*.wad +*.tmd +*.tik +*.cert diff --git a/README.md b/README.md index c348f23..809d857 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ A very WIP and experimental port of [libWiiPy](https://github.com/NinjaCheetah/l ### What's Included - Structs for TMDs and Tickets that can be created from binary data - Simple Title Key encryption/decryption +- Content encryption/decryption +- WAD parsing (allowing for packing/unpacking) - A very basic test binary that makes sure these things work as expected ### What's Not Included diff --git a/src/bin/rustii/main.rs b/src/bin/rustii/main.rs index 6fe3748..45b3c96 100644 --- a/src/bin/rustii/main.rs +++ b/src/bin/rustii/main.rs @@ -1,28 +1,37 @@ +// Sample file for testing rustii library stuff. + use std::fs; -use rustii::title::{tmd, ticket, content}; +use rustii::title::{tmd, ticket, content, crypto, wad}; fn main() { - let data = fs::read("title.tmd").unwrap(); - let tmd = tmd::TMD::from_bytes(&data).unwrap(); + let data = fs::read("sm.wad").unwrap(); + let wad = wad::WAD::from_bytes(&data).unwrap(); + println!("size of tmd: {:?}", wad.tmd().len()); + let tmd = tmd::TMD::from_bytes(&wad.tmd()).unwrap(); println!("num content records: {:?}", tmd.content_records.len()); println!("first record data: {:?}", tmd.content_records.first().unwrap()); - assert_eq!(data, tmd.to_vec().unwrap()); + assert_eq!(wad.tmd(), tmd.to_bytes().unwrap()); - let data = fs::read("tik").unwrap(); - let tik = ticket::Ticket::from_bytes(&data).unwrap(); + let tik = ticket::Ticket::from_bytes(&wad.ticket()).unwrap(); println!("title version from ticket is: {:?}", tik.title_version); println!("title key (enc): {:?}", tik.title_key); println!("title key (dec): {:?}", tik.dec_title_key()); - assert_eq!(data, tik.to_vec().unwrap()); + assert_eq!(wad.ticket(), tik.to_bytes().unwrap()); - let data = fs::read("content-blob").unwrap(); - let content_region = content::ContentRegion::from_bytes(&data, tmd.content_records).unwrap(); - assert_eq!(data, content_region.to_bytes().unwrap()); + let content_region = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records).unwrap(); + assert_eq!(wad.content(), content_region.to_bytes().unwrap()); println!("content OK"); let content_dec = content_region.get_content_by_index(0, tik.dec_title_key()).unwrap(); println!("content dec from index: {:?}", content_dec); - let content = content_region.get_content_by_cid(150, tik.dec_title_key()).unwrap(); - println!("content dec from cid: {:?}", content); + let content = content_region.get_enc_content_by_index(0).unwrap(); + assert_eq!(content, crypto::encrypt_content(&content_dec, tik.dec_title_key(), 0, content_region.content_records[0].content_size)); + println!("content re-encrypted OK"); + + println!("wad header: {:?}", wad.header); + + let repacked = wad.to_bytes().unwrap(); + assert_eq!(repacked, data); + println!("wad packed OK"); } diff --git a/src/title/crypto.rs b/src/title/crypto.rs index 341320a..729a020 100644 --- a/src/title/crypto.rs +++ b/src/title/crypto.rs @@ -39,9 +39,19 @@ 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); type Aes128CbcDec = cbc::Decryptor; - println!("{:?}", iv); let decryptor = Aes128CbcDec::new(&title_key.into(), iv.as_slice().into()); let mut buf = data.to_owned(); decryptor.decrypt_padded_mut::(&mut buf).unwrap(); buf } + +// Encrypt content using a Title Key. +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); + type Aes128CbcEnc = cbc::Encryptor; + let encryptor = Aes128CbcEnc::new(&title_key.into(), iv.as_slice().into()); + let mut buf = data.to_owned(); + encryptor.encrypt_padded_mut::(&mut buf, size as usize).unwrap(); + buf +} diff --git a/src/title/mod.rs b/src/title/mod.rs index 46ac81d..81dbf4a 100644 --- a/src/title/mod.rs +++ b/src/title/mod.rs @@ -6,3 +6,4 @@ pub mod content; pub mod crypto; pub mod ticket; pub mod tmd; +pub mod wad; diff --git a/src/title/ticket.rs b/src/title/ticket.rs index d346729..f997f3e 100644 --- a/src/title/ticket.rs +++ b/src/title/ticket.rs @@ -118,7 +118,7 @@ impl Ticket { }) } - pub fn to_vec(&self) -> Result, std::io::Error> { + pub fn to_bytes(&self) -> Result, std::io::Error> { let mut buf: Vec = Vec::new(); buf.write_u32::(self.signature_type)?; buf.write_all(&self.signature)?; diff --git a/src/title/tmd.rs b/src/title/tmd.rs index 3829dfa..91cd4c0 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -129,7 +129,7 @@ impl TMD { }) } - pub fn to_vec(&self) -> Result, std::io::Error> { + pub fn to_bytes(&self) -> Result, std::io::Error> { let mut buf: Vec = Vec::new(); buf.write_u32::(self.signature_type)?; buf.write_all(&self.signature)?; diff --git a/src/title/wad.rs b/src/title/wad.rs new file mode 100644 index 0000000..6626a35 --- /dev/null +++ b/src/title/wad.rs @@ -0,0 +1,200 @@ +// title/wad.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors +// https://github.com/NinjaCheetah/rustii-lib +// +// Implements the structures and methods required for WAD parsing and editing. + +use std::error::Error; +use std::fmt; +use std::str; +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + +#[derive(Debug)] +pub enum WADError { + BadType, + IOError(std::io::Error), +} + +impl fmt::Display for WADError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = match *self { + WADError::BadType => "An invalid WAD type was specified.", + WADError::IOError(_) => "The provided WAD data was invalid.", + }; + f.write_str(description) + } +} + +impl Error for WADError {} + +#[derive(Debug)] +pub enum WADTypes { + Installable, + ImportBoot +} + +#[derive(Debug)] +pub struct WAD { + pub header: WADHeader, + pub body: WADBody, +} + +#[derive(Debug)] +pub struct WADHeader { + pub header_size: u32, + pub wad_type: WADTypes, + pub wad_version: u16, + cert_chain_size: u32, + crl_size: u32, + ticket_size: u32, + tmd_size: u32, + content_size: u32, + meta_size: u32, + padding: [u8; 32], +} + +#[derive(Debug)] +pub struct WADBody { + cert_chain: Vec, + crl: Vec, + ticket: Vec, + tmd: Vec, + content: Vec, + meta: Vec, +} + +impl WAD { + pub fn from_bytes(data: &[u8]) -> Result { + let mut buf = Cursor::new(data); + let header_size = buf.read_u32::().map_err(WADError::IOError)?; + let mut wad_type = [0u8; 2]; + 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, + _ => return Err(WADError::BadType), + }, + Err(_) => return Err(WADError::BadType), + }; + let wad_version = buf.read_u16::().map_err(WADError::IOError)?; + let cert_chain_size = buf.read_u32::().map_err(WADError::IOError)?; + let crl_size = buf.read_u32::().map_err(WADError::IOError)?; + let ticket_size = buf.read_u32::().map_err(WADError::IOError)?; + let tmd_size = buf.read_u32::().map_err(WADError::IOError)?; + // Round the content size to the nearest 16. + let content_size = (buf.read_u32::().map_err(WADError::IOError)? + 15) & !15; + let meta_size = buf.read_u32::().map_err(WADError::IOError)?; + let mut padding = [0u8; 32]; + buf.read_exact(&mut padding).map_err(WADError::IOError)?; + // Build header so we can use that data to read the WAD data. + let header = WADHeader { + header_size, + wad_type, + wad_version, + cert_chain_size, + crl_size, + ticket_size, + tmd_size, + content_size, + meta_size, + padding, + }; + // Find rounded offsets for each region. + let cert_chain_offset = (header.header_size + 63) & !63; + let crl_offset = (cert_chain_offset + header.cert_chain_size + 63) & !63; + let ticket_offset = (crl_offset + header.crl_size + 63) & !63; + let tmd_offset = (ticket_offset + header.ticket_size + 63) & !63; + let content_offset = (tmd_offset + header.tmd_size + 63) & !63; + let meta_offset = (content_offset + header.content_size + 63) & !63; + // Read cert chain data. + buf.seek(SeekFrom::Start(cert_chain_offset as u64)).map_err(WADError::IOError)?; + let mut cert_chain = vec![0u8; header.cert_chain_size as usize]; + buf.read_exact(&mut cert_chain).map_err(WADError::IOError)?; + buf.seek(SeekFrom::Start(crl_offset as u64)).map_err(WADError::IOError)?; + let mut crl = vec![0u8; header.crl_size as usize]; + buf.read_exact(&mut crl).map_err(WADError::IOError)?; + buf.seek(SeekFrom::Start(ticket_offset as u64)).map_err(WADError::IOError)?; + let mut ticket = vec![0u8; header.ticket_size as usize]; + buf.read_exact(&mut ticket).map_err(WADError::IOError)?; + buf.seek(SeekFrom::Start(tmd_offset as u64)).map_err(WADError::IOError)?; + let mut tmd = vec![0u8; header.tmd_size as usize]; + buf.read_exact(&mut tmd).map_err(WADError::IOError)?; + buf.seek(SeekFrom::Start(content_offset as u64)).map_err(WADError::IOError)?; + let mut content = vec![0u8; header.content_size as usize]; + buf.read_exact(&mut content).map_err(WADError::IOError)?; + buf.seek(SeekFrom::Start(meta_offset as u64)).map_err(WADError::IOError)?; + let mut meta = vec![0u8; header.meta_size as usize]; + buf.read_exact(&mut meta).map_err(WADError::IOError)?; + let body = WADBody { + cert_chain, + crl, + ticket, + tmd, + content, + meta, + }; + // Assemble full WAD object. + 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)?; }, + } + buf.write_u16::(self.header.wad_version).map_err(WADError::IOError)?; + buf.write_u32::(self.header.cert_chain_size).map_err(WADError::IOError)?; + buf.write_u32::(self.header.crl_size).map_err(WADError::IOError)?; + buf.write_u32::(self.header.ticket_size).map_err(WADError::IOError)?; + buf.write_u32::(self.header.tmd_size).map_err(WADError::IOError)?; + buf.write_u32::(self.header.content_size).map_err(WADError::IOError)?; + buf.write_u32::(self.header.meta_size).map_err(WADError::IOError)?; + buf.write_all(&self.header.padding).map_err(WADError::IOError)?; + // Pad up to nearest multiple of 64. This also needs to happen after each section of data. + buf.resize((buf.len() + 63) & !63, 0); + buf.write_all(&self.body.cert_chain).map_err(WADError::IOError)?; + buf.resize((buf.len() + 63) & !63, 0); + buf.write_all(&self.body.crl).map_err(WADError::IOError)?; + buf.resize((buf.len() + 63) & !63, 0); + buf.write_all(&self.body.ticket).map_err(WADError::IOError)?; + buf.resize((buf.len() + 63) & !63, 0); + buf.write_all(&self.body.tmd).map_err(WADError::IOError)?; + buf.resize((buf.len() + 63) & !63, 0); + buf.write_all(&self.body.content).map_err(WADError::IOError)?; + buf.resize((buf.len() + 63) & !63, 0); + buf.write_all(&self.body.meta).map_err(WADError::IOError)?; + buf.resize((buf.len() + 63) & !63, 0); + Ok(buf) + } + + pub fn cert_chain(&self) -> Vec { + self.body.cert_chain.clone() + } + + pub fn crl(&self) -> Vec { + self.body.crl.clone() + } + + pub fn ticket(&self) -> Vec { + self.body.ticket.clone() + } + + pub fn tmd(&self) -> Vec { + self.body.tmd.clone() + } + + pub fn content(&self) -> Vec { + self.body.content.clone() + } + + pub fn meta(&self) -> Vec { + self.body.meta.clone() + } +}