nod-rs/src/disc/wii.rs

420 lines
12 KiB
Rust

use std::{
io,
io::{Read, Seek, SeekFrom},
};
use aes::{Aes128, Block, NewBlockCipher};
use binread::prelude::*;
use block_modes::{block_padding::NoPadding, BlockMode, Cbc};
use sha1::{digest, Digest, Sha1};
use crate::{
array_ref,
disc::{BI2Header, DiscBase, DiscIO, Header, PartHeader, PartReadStream, BUFFER_SIZE},
div_rem,
fst::{find_node, node_parser, Node, NodeKind, NodeType},
streams::{wrap_windowed, OwningWindowedReadStream, ReadStream, SharedWindowedReadStream},
Error, Result,
};
type Aes128Cbc = Cbc<Aes128, NoPadding>;
const BLOCK_SIZE: usize = 0x7c00;
const BUFFER_OFFSET: usize = BUFFER_SIZE - BLOCK_SIZE;
#[rustfmt::skip]
const COMMON_KEYS: [[u8; 16]; 2] = [
/* Normal */
[0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7],
/* Korean */
[0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e],
];
#[derive(Debug, PartialEq, BinRead)]
#[br(repr = u32)]
enum WiiPartType {
Data,
Update,
Channel,
}
#[derive(Debug, PartialEq, BinRead)]
#[br(repr = u32)]
enum SigType {
Rsa4096 = 0x00010000,
Rsa2048 = 0x00010001,
EllipticalCurve = 0x00010002,
}
#[derive(Debug, PartialEq, BinRead)]
#[br(repr = u32)]
enum KeyType {
Rsa4096 = 0x00000000,
Rsa2048 = 0x00000001,
}
#[derive(Debug, PartialEq, BinRead)]
struct WiiPart {
#[br(map = | x: u32 | (x as u64) << 2)]
part_data_off: u64,
part_type: WiiPartType,
#[br(restore_position, args(part_data_off))]
part_header: WiiPartitionHeader,
}
#[derive(Debug, PartialEq, BinRead)]
struct WiiPartInfo {
#[br(seek_before = SeekFrom::Start(0x40000))]
part_count: u32,
#[br(map = | x: u32 | (x as u64) << 2)]
part_info_off: u64,
#[br(seek_before = SeekFrom::Start(part_info_off), count = part_count)]
parts: Vec<WiiPart>,
}
#[derive(Debug, PartialEq, BinRead)]
struct TicketTimeLimit {
enable_time_limit: u32,
time_limit: u32,
}
#[derive(Debug, PartialEq, BinRead)]
struct Ticket {
sig_type: SigType,
#[br(count = 256)]
sig: Vec<u8>,
#[br(pad_before = 60, count = 64)]
sig_issuer: Vec<u8>,
#[br(count = 60)]
ecdh: Vec<u8>,
#[br(pad_before = 3)]
enc_key: [u8; 16],
#[br(pad_before = 1)]
ticket_id: [u8; 8],
console_id: [u8; 4],
title_id: [u8; 8],
#[br(pad_before = 2)]
ticket_version: u16,
permitted_titles_mask: u32,
permit_mask: u32,
title_export_allowed: u8,
common_key_idx: u8,
#[br(pad_before = 48, count = 64)]
content_access_permissions: Vec<u8>,
#[br(pad_before = 2, count = 8)]
time_limits: Vec<TicketTimeLimit>,
}
#[derive(Debug, PartialEq, BinRead)]
struct TMDContent {
id: u32,
index: u16,
content_type: u16,
size: u64,
hash: [u8; 20],
}
#[derive(Debug, PartialEq, BinRead)]
struct TMD {
sig_type: SigType,
#[br(count = 256)]
sig: Vec<u8>,
#[br(pad_before = 60, count = 64)]
sig_issuer: Vec<u8>,
version: u8,
ca_crl_version: u8,
signer_crl_version: u8,
#[br(pad_before = 1)]
ios_id_major: u32,
ios_id_minor: u32,
title_id_major: u32,
title_id_minor: [char; 4],
title_type: u32,
group_id: u16,
#[br(pad_before = 62)]
access_flags: u32,
title_version: u16,
num_contents: u16,
#[br(pad_after = 2)]
boot_idx: u16,
#[br(count = num_contents)]
contents: Vec<TMDContent>,
}
#[derive(Debug, PartialEq, BinRead)]
struct Certificate {
sig_type: SigType,
#[br(count = if sig_type == SigType::Rsa4096 { 512 }
else if sig_type == SigType::Rsa2048 { 256 }
else if sig_type == SigType::EllipticalCurve { 64 } else { 0 })]
sig: Vec<u8>,
#[br(pad_before = 60, count = 64)]
issuer: Vec<u8>,
key_type: KeyType,
#[br(count = 64)]
subject: Vec<u8>,
#[br(count = if key_type == KeyType::Rsa4096 { 512 } else if key_type == KeyType::Rsa2048 { 256 } else { 0 })]
key: Vec<u8>,
modulus: u32,
#[br(pad_after = 52)]
pub_exp: u32,
}
#[derive(Debug, PartialEq, BinRead)]
#[br(import(partition_off: u64))]
struct WiiPartitionHeader {
#[br(seek_before = SeekFrom::Start(partition_off))]
ticket: Ticket,
tmd_size: u32,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
tmd_off: u64,
cert_chain_size: u32,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
cert_chain_off: u64,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
global_hash_table_off: u64,
#[br(map = | x: u32 | ((x as u64) << 2) + partition_off)]
data_off: u64,
#[br(map = | x: u32 | (x as u64) << 2)]
data_size: u64,
#[br(seek_before = SeekFrom::Start(tmd_off))]
tmd: TMD,
#[br(seek_before = SeekFrom::Start(cert_chain_off))]
ca_cert: Certificate,
tmd_cert: Certificate,
ticket_cert: Certificate,
#[br(seek_before = SeekFrom::Start(global_hash_table_off), count = 0x18000)]
h3_data: Vec<u8>,
}
pub(crate) struct DiscWii {
header: Header,
part_info: WiiPartInfo,
}
pub(crate) fn new_disc_wii(mut stream: &mut dyn ReadStream, header: Header) -> Result<DiscWii> {
let mut disc = DiscWii { header, part_info: stream.read_be()? };
disc.decrypt_partition_keys()?;
Result::Ok(disc)
}
impl DiscWii {
pub(crate) fn decrypt_partition_keys(&mut self) -> Result<()> {
for part in self.part_info.parts.as_mut_slice() {
let ticket = &mut part.part_header.ticket;
let mut iv: [u8; 16] = [0; 16];
iv[..8].copy_from_slice(&ticket.title_id);
Aes128Cbc::new(
Aes128::new(&COMMON_KEYS[ticket.common_key_idx as usize].into()),
&iv.into(),
)
.decrypt(&mut ticket.enc_key)?;
}
Result::Ok(())
}
}
impl DiscBase for DiscWii {
fn get_header(&self) -> &Header { &self.header }
fn get_data_partition<'a>(
&self,
disc_io: &'a mut dyn DiscIO,
) -> Result<Box<dyn PartReadStream + 'a>> {
let part = self
.part_info
.parts
.iter()
.find(|v| v.part_type == WiiPartType::Data)
.ok_or(Error::DiscFormat("Failed to locate data partition".to_string()))?;
let data_off = part.part_header.data_off;
let result = Box::new(WiiPartReadStream {
stream: wrap_windowed(
disc_io.begin_read_stream(data_off)?,
data_off,
part.part_header.data_size,
)?,
crypto: if disc_io.has_wii_crypto() {
Aes128::new(&part.part_header.ticket.enc_key.into()).into()
} else {
Option::None
},
offset: 0,
cur_block: u64::MAX,
buf: [0; 0x8000],
validate_hashes: false,
});
Result::Ok(result)
}
}
struct WiiPartReadStream<'a> {
stream: OwningWindowedReadStream<'a>,
crypto: Option<Aes128>,
offset: u64,
cur_block: u64,
buf: [u8; BUFFER_SIZE],
validate_hashes: bool,
}
impl<'a> PartReadStream for WiiPartReadStream<'a> {
fn begin_file_stream(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
assert_eq!(node.kind, NodeKind::File);
io::Result::Ok(self.new_window((node.offset as u64) << 2, node.length as u64)?)
}
fn read_header(&mut self) -> Result<Box<dyn PartHeader>> {
self.seek(SeekFrom::Start(0))?;
Result::Ok(Box::from(self.read_be::<WiiPartition>()?))
}
fn ideal_buffer_size(&self) -> usize { BLOCK_SIZE }
}
#[inline(always)]
fn as_digest(slice: &[u8; 20]) -> digest::Output<Sha1> { (*slice).into() }
fn decrypt_block(part: &mut WiiPartReadStream, cluster: usize) -> io::Result<()> {
part.stream.read(&mut part.buf)?;
if part.crypto.is_some() {
// Fetch IV before decrypting header
let iv = Block::from(*array_ref![part.buf, 0x3d0, 16]);
// Don't need to decrypt header if we're not validating hashes
if part.validate_hashes {
Aes128Cbc::new(part.crypto.as_ref().unwrap().clone(), &Block::from([0; 16]))
.decrypt(&mut part.buf[..BUFFER_OFFSET])
.expect("Failed to decrypt header");
}
Aes128Cbc::new(part.crypto.as_ref().unwrap().clone(), &iv)
.decrypt(&mut part.buf[BUFFER_OFFSET..])
.expect("Failed to decrypt block");
}
if part.validate_hashes && part.crypto.is_some()
/* FIXME NFS validation? */
{
let (mut group, sub_group) = div_rem(cluster, 8);
group %= 8;
// H0 hashes
for i in 0..31 {
let mut hash = Sha1::new();
hash.update(array_ref![part.buf, (i + 1) * 0x400, 0x400]);
let expected = as_digest(array_ref![part.buf, i * 20, 20]);
let output = hash.finalize();
if output != expected {
panic!(
"Invalid hash! (block {:?}) {:?}\n\texpected {:?}",
i,
output.as_slice(),
expected
);
}
}
// H1 hash
{
let mut hash = Sha1::new();
hash.update(array_ref![part.buf, 0, 0x26C]);
let expected = as_digest(array_ref![part.buf, 0x280 + sub_group * 20, 20]);
let output = hash.finalize();
if output != expected {
panic!(
"Invalid hash! (subgroup {:?}) {:?}\n\texpected {:?}",
sub_group,
output.as_slice(),
expected
);
}
}
// H2 hash
{
let mut hash = Sha1::new();
hash.update(array_ref![part.buf, 0x280, 0xA0]);
let expected = as_digest(array_ref![part.buf, 0x340 + group * 20, 20]);
let output = hash.finalize();
if output != expected {
panic!(
"Invalid hash! (group {:?}) {:?}\n\texpected {:?}",
group,
output.as_slice(),
expected
);
}
}
}
io::Result::Ok(())
}
impl<'a> Read for WiiPartReadStream<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let (mut block, mut block_offset) = div_rem(self.offset as usize, BLOCK_SIZE);
let mut rem = buf.len();
let mut read: usize = 0;
while rem > 0 {
if block != self.cur_block as usize {
decrypt_block(self, block)?;
self.cur_block = block as u64;
}
let mut cache_size = rem;
if cache_size + block_offset > BLOCK_SIZE {
cache_size = BLOCK_SIZE - block_offset;
}
buf[read..read + cache_size].copy_from_slice(
&self.buf[BUFFER_OFFSET + block_offset..BUFFER_OFFSET + block_offset + cache_size],
);
read += cache_size;
rem -= cache_size;
block_offset = 0;
block += 1;
}
self.offset += buf.len() as u64;
io::Result::Ok(buf.len())
}
}
#[inline(always)]
fn to_block_size(v: u64) -> u64 {
(v / BUFFER_SIZE as u64) * BLOCK_SIZE as u64 + (v % BUFFER_SIZE as u64)
}
impl<'a> Seek for WiiPartReadStream<'a> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.offset = match pos {
SeekFrom::Start(v) => v,
SeekFrom::End(v) => (self.stable_stream_len()? as i64 + v) as u64,
SeekFrom::Current(v) => (self.offset as i64 + v) as u64,
};
let block = self.offset / BLOCK_SIZE as u64;
if block != self.cur_block {
self.stream.seek(SeekFrom::Start(block * BUFFER_SIZE as u64))?;
self.cur_block = u64::MAX;
}
io::Result::Ok(self.offset)
}
fn stream_position(&mut self) -> io::Result<u64> { io::Result::Ok(self.offset) }
}
impl<'a> ReadStream for WiiPartReadStream<'a> {
fn stable_stream_len(&mut self) -> io::Result<u64> {
io::Result::Ok(to_block_size(self.stream.stable_stream_len()?))
}
}
#[derive(Clone, Debug, PartialEq, BinRead)]
pub(crate) struct WiiPartition {
header: Header,
bi2_header: BI2Header,
#[br(seek_before = SeekFrom::Start((header.fst_off as u64) << 2))]
#[br(parse_with = node_parser)]
root_node: NodeType,
}
impl PartHeader for WiiPartition {
fn root_node(&self) -> &NodeType { &self.root_node }
fn find_node(&self, path: &str) -> Option<&NodeType> { find_node(&self.root_node, path) }
}