use crate::error::Error;
use crate::utils::from_latin1_or_shift_jis;
use crate::utils::trim;
use std::io;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use byteorder::BigEndian;
use byteorder::ReadBytesExt;
use aes::Aes128;
use aes::cipher::Block;
use aes::cipher::BlockDecryptMut;
use aes::cipher::KeyIvInit;
use cbc::Decryptor as CbcDecryptor;
#[derive(Debug)]
pub struct Header {
pub console_id: String,
pub game_code: String,
pub country_code: String,
pub maker_code: String,
pub disk_id: u8,
pub version: u8,
pub audio_streaming: u8,
pub stream_buffer_size: u8,
pub magic: u32,
pub game_name: String,
pub disable_hash_verification: u8,
pub disable_disc_encryption: u8,
}
pub struct RegionSettings {
pub region: u32,
pub japan_taiwan: u8,
pub usa: u8,
pub germany: u8,
pub pegi: u8,
pub finland: u8,
pub portugal: u8,
pub britain: u8,
pub australia: u8,
pub korea: u8,
}
pub struct PartitionTables {
pub partitions: Vec<Partition>,
}
pub struct Partition {
pub offset: u64,
pub type_: u32,
pub ticket: Ticket,
pub tmd_size: u32,
pub tmd_offset: u64,
pub cert_chain_size: u32,
pub cert_chain_offset: u64,
pub h3_table_offset: u64,
pub data_offset: u64,
pub data_size: u64,
}
#[derive(Default, Copy, Debug, Clone)]
pub struct CcLimit {
pub limit_type: u32,
pub maximum_usage: u32,
}
#[derive(Debug, Clone)]
pub struct Ticket {
pub signature_type: u32,
pub signature: [u8; 0x100],
pub signature_issuer: String, pub ecdh_data: [u8; 0x3C],
pub format_version: u8,
pub title_key: [u8; 0x10],
pub id: [u8; 0x08],
pub console_id: [u8; 4],
pub title_id: [u8; 0x08],
pub title_version: u16,
pub permitted_titles_mask: u32,
pub permit_mask: u32,
pub title_export_allowed: u8,
pub common_key_index: u8,
pub content_access_permissions: [u8; 0x40],
pub cc_limits: [CcLimit; 8],
}
pub trait TicketReader {
fn read_wii_ticket(&mut self) -> Result<Ticket, Error>;
}
impl<T: Seek + Read> TicketReader for T {
fn read_wii_ticket(&mut self) -> Result<Ticket, Error> {
let signature_type = self.read_u32::<BigEndian>()?;
let mut signature = [0u8; 0x100];
self.read_exact(&mut signature)?;
self.seek(SeekFrom::Current(0x3C))?;
let mut signature_issuer = [0u8; 0x40];
self.read_exact(&mut signature_issuer)?;
let signature_issuer = trim(str::from_utf8(&signature_issuer)?).to_string();
let mut ecdh_data = [0u8; 0x3C];
self.read_exact(&mut ecdh_data)?;
let format_version = self.read_u8()?;
self.seek(SeekFrom::Current(2))?;
let mut title_key = [0u8; 0x10];
self.read_exact(&mut title_key)?;
self.seek(SeekFrom::Current(1))?;
let mut id = [0u8; 8];
self.read_exact(&mut id)?;
let mut console_id = [0u8; 4];
self.read_exact(&mut console_id)?;
let mut title_id = [0u8; 0x08];
self.read_exact(&mut title_id)?;
self.seek(SeekFrom::Current(2))?;
let title_version = self.read_u16::<BigEndian>()?;
let permitted_titles_mask = self.read_u32::<BigEndian>()?;
let permit_mask = self.read_u32::<BigEndian>()?;
let title_export_allowed = self.read_u8()?;
let common_key_index = self.read_u8()?;
self.seek(SeekFrom::Current(0x30))?;
let mut content_access_permissions = [0u8; 0x40];
self.read_exact(&mut content_access_permissions)?;
self.seek(SeekFrom::Current(0x2))?;
let mut cc_limits = [CcLimit::default(); 8];
for limit in &mut cc_limits {
*limit = CcLimit {
limit_type: self.read_u32::<BigEndian>()?,
maximum_usage: self.read_u32::<BigEndian>()?,
};
}
Ok(Ticket {
signature_type,
signature,
signature_issuer,
ecdh_data,
format_version,
title_key,
id,
console_id,
title_id,
title_version,
permitted_titles_mask,
permit_mask,
title_export_allowed,
common_key_index,
content_access_permissions,
cc_limits,
})
}
}
pub struct Metadata {
pub header: Header,
pub partition_tables: PartitionTables,
}
pub trait MetadataRead {
fn read_wii_header(&mut self) -> Result<Header, Error>;
fn read_wii_partitions(&mut self) -> Result<PartitionTables, Error>;
}
impl Metadata {
pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
let header = io.read_wii_header()?;
let partition_tables = io.read_wii_partitions()?;
Ok(Self {
header,
partition_tables,
})
}
}
impl<T: Read + Seek> MetadataRead for T {
fn read_wii_header(&mut self) -> Result<Header, Error> {
self.seek(SeekFrom::Start(0))?;
let mut game_code = [0u8; 4];
self.read_exact(&mut game_code)?;
let game_code = from_latin1_or_shift_jis(&game_code)?;
let maker_code = self.read_u16::<BigEndian>()?.to_be_bytes();
let maker_code = from_latin1_or_shift_jis(&maker_code)?;
let disk_id = self.read_u8()?;
let version = self.read_u8()?;
let audio_streaming = self.read_u8()?;
let stream_buffer_size = self.read_u8()?;
self.seek(SeekFrom::Current(14))?;
let magic = self.read_u32::<BigEndian>()?;
let mut game_name = [0u8; 0x40];
self.read_exact(&mut game_name)?;
let game_name = trim(&from_latin1_or_shift_jis(&game_name)?).to_string();
if game_code.len() != 4 {
return Err(Error::Parse("bad game code string, wrong size".into()));
}
if !game_code.is_ascii() {
return Err(Error::Parse("bad game code string, not ASCII".into()));
}
let disable_hash_verification = self.read_u8()?;
let disable_disc_encryption = self.read_u8()?;
Ok(Header {
console_id: game_code[0..1].to_string(),
game_code: game_code[1..3].to_string(),
country_code: game_code[3..4].to_string(),
maker_code,
disk_id,
version,
audio_streaming,
stream_buffer_size,
magic,
game_name,
disable_hash_verification,
disable_disc_encryption,
})
}
fn read_wii_partitions(&mut self) -> Result<PartitionTables, Error> {
let mut partitions = Vec::<Partition>::default();
for i in 0..4 {
self.seek(SeekFrom::Start(0x40000 + 8 * i))?;
let total_partitions = self.read_u32::<BigEndian>()?;
let partition_table_offset = u64::from(self.read_u32::<BigEndian>()?) << 2;
if total_partitions == 0 {
continue;
}
for j in 0..total_partitions {
self.seek(SeekFrom::Start(partition_table_offset + u64::from(j) * 8))?;
let offset = u64::from(self.read_u32::<BigEndian>()?) << 2;
let type_ = self.read_u32::<BigEndian>()?;
self.seek(SeekFrom::Start(offset))?;
let ticket = self.read_wii_ticket()?;
let tmd_size = self.read_u32::<BigEndian>()?;
let tmd_offset = u64::from(self.read_u32::<BigEndian>()?) << 2;
let cert_chain_size = self.read_u32::<BigEndian>()?;
let cert_chain_offset = u64::from(self.read_u32::<BigEndian>()?) << 2;
let h3_table_offset = u64::from(self.read_u32::<BigEndian>()?) << 2;
let data_offset = u64::from(self.read_u32::<BigEndian>()?) << 2;
let data_size = u64::from(self.read_u32::<BigEndian>()?) << 2;
let partition = Partition {
offset,
type_,
ticket,
tmd_size,
tmd_offset,
cert_chain_size,
cert_chain_offset,
h3_table_offset,
data_offset,
data_size,
};
partitions.push(partition);
}
}
Ok(PartitionTables { partitions })
}
}
fn print_key(key: &[u8]) {
for byte in key {
print!("{byte:02X}");
}
println!();
}
const WII_COMMON_KEY: [u8; 16] = [
0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7,
];
impl Partition {
pub fn read_sector<T: Read + Seek>(&self, io: &mut T, sector: u32) -> io::Result<Vec<u8>> {
let position = u64::from(sector) * 0x8000 + self.offset + self.data_offset;
let max = self.offset + self.data_offset + self.data_size;
if position >= max {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"sector out of partition range",
));
}
io.seek(SeekFrom::Start(position))?;
let mut data = vec![0u8; 0x8000];
io.read_exact(&mut data)?;
#[allow(clippy::missing_panics_doc)]
let data_iv: [u8; 16] = data[0x3D0..0x3E0].try_into().unwrap();
let mut title_iv = [0u8; 16];
title_iv[0..8].copy_from_slice(&self.ticket.title_id);
let mut title_key = self.ticket.title_key;
let mut title_aes = CbcDecryptor::<Aes128>::new(&WII_COMMON_KEY.into(), &title_iv.into());
title_aes.decrypt_block_mut((&mut title_key).into());
let title_key = title_key;
let mut aes_hash = CbcDecryptor::<Aes128>::new(&title_key.into(), &[0u8; 16].into());
for block in data[0..0x400].chunks_exact_mut(16) {
let block = Block::<Aes128>::from_mut_slice(block);
aes_hash.decrypt_block_mut(block);
}
let mut data_aes = CbcDecryptor::<Aes128>::new(&title_key.into(), &data_iv.into());
for block in data[0x400..0x8000].chunks_exact_mut(16) {
let block = Block::<Aes128>::from_mut_slice(block);
data_aes.decrypt_block_mut(block);
}
Ok(data)
}
}