mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-05 23:11:02 -04:00
Added WAD parsing, allows for WAD packing/unpacking
This commit is contained in:
parent
93f2103763
commit
83dc83d2d6
6
.gitignore
vendored
6
.gitignore
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -39,9 +39,19 @@ 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);
|
||||
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||
println!("{:?}", iv);
|
||||
let decryptor = Aes128CbcDec::new(&title_key.into(), iv.as_slice().into());
|
||||
let mut buf = data.to_owned();
|
||||
decryptor.decrypt_padded_mut::<ZeroPadding>(&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<u8> {
|
||||
let mut iv = Vec::from(index.to_be_bytes());
|
||||
iv.resize(16, 0);
|
||||
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
|
||||
let encryptor = Aes128CbcEnc::new(&title_key.into(), iv.as_slice().into());
|
||||
let mut buf = data.to_owned();
|
||||
encryptor.encrypt_padded_mut::<ZeroPadding>(&mut buf, size as usize).unwrap();
|
||||
buf
|
||||
}
|
||||
|
@ -6,3 +6,4 @@ pub mod content;
|
||||
pub mod crypto;
|
||||
pub mod ticket;
|
||||
pub mod tmd;
|
||||
pub mod wad;
|
||||
|
@ -118,7 +118,7 @@ impl Ticket {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> {
|
||||
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)?;
|
||||
buf.write_all(&self.signature)?;
|
||||
|
@ -129,7 +129,7 @@ impl TMD {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> {
|
||||
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)?;
|
||||
buf.write_all(&self.signature)?;
|
||||
|
200
src/title/wad.rs
Normal file
200
src/title/wad.rs
Normal file
@ -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<u8>,
|
||||
crl: Vec<u8>,
|
||||
ticket: Vec<u8>,
|
||||
tmd: Vec<u8>,
|
||||
content: Vec<u8>,
|
||||
meta: Vec<u8>,
|
||||
}
|
||||
|
||||
impl WAD {
|
||||
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)?;
|
||||
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::<BigEndian>().map_err(WADError::IOError)?;
|
||||
let cert_chain_size = buf.read_u32::<BigEndian>().map_err(WADError::IOError)?;
|
||||
let crl_size = buf.read_u32::<BigEndian>().map_err(WADError::IOError)?;
|
||||
let ticket_size = buf.read_u32::<BigEndian>().map_err(WADError::IOError)?;
|
||||
let tmd_size = buf.read_u32::<BigEndian>().map_err(WADError::IOError)?;
|
||||
// Round the content size to the nearest 16.
|
||||
let content_size = (buf.read_u32::<BigEndian>().map_err(WADError::IOError)? + 15) & !15;
|
||||
let meta_size = buf.read_u32::<BigEndian>().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<Vec<u8>, WADError> {
|
||||
let mut buf = Vec::new();
|
||||
buf.write_u32::<BigEndian>(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::<BigEndian>(self.header.wad_version).map_err(WADError::IOError)?;
|
||||
buf.write_u32::<BigEndian>(self.header.cert_chain_size).map_err(WADError::IOError)?;
|
||||
buf.write_u32::<BigEndian>(self.header.crl_size).map_err(WADError::IOError)?;
|
||||
buf.write_u32::<BigEndian>(self.header.ticket_size).map_err(WADError::IOError)?;
|
||||
buf.write_u32::<BigEndian>(self.header.tmd_size).map_err(WADError::IOError)?;
|
||||
buf.write_u32::<BigEndian>(self.header.content_size).map_err(WADError::IOError)?;
|
||||
buf.write_u32::<BigEndian>(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<u8> {
|
||||
self.body.cert_chain.clone()
|
||||
}
|
||||
|
||||
pub fn crl(&self) -> Vec<u8> {
|
||||
self.body.crl.clone()
|
||||
}
|
||||
|
||||
pub fn ticket(&self) -> Vec<u8> {
|
||||
self.body.ticket.clone()
|
||||
}
|
||||
|
||||
pub fn tmd(&self) -> Vec<u8> {
|
||||
self.body.tmd.clone()
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Vec<u8> {
|
||||
self.body.content.clone()
|
||||
}
|
||||
|
||||
pub fn meta(&self) -> Vec<u8> {
|
||||
self.body.meta.clone()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user