mirror of
https://github.com/encounter/decomp-toolkit.git
synced 2025-07-19 11:36:09 +00:00
221 lines
7.0 KiB
Rust
221 lines
7.0 KiB
Rust
use std::{
|
|
io,
|
|
io::{BufRead, Read, Seek},
|
|
};
|
|
|
|
use aes::cipher::{BlockDecryptMut, KeyIvInit};
|
|
use anyhow::{bail, Result};
|
|
use nodtool::nod::{Ticket, TmdHeader};
|
|
use sha1::{Digest, Sha1};
|
|
use size::Size;
|
|
use zerocopy::{big_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
|
|
|
use crate::{
|
|
array_ref_mut, static_assert,
|
|
util::read::{read_box_slice, read_from},
|
|
};
|
|
|
|
// TODO: other WAD types?
|
|
pub const WAD_MAGIC: [u8; 8] = [0x00, 0x00, 0x00, 0x20, 0x49, 0x73, 0x00, 0x00];
|
|
|
|
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
|
#[repr(C, align(4))]
|
|
pub struct WadHeader {
|
|
pub header_size: U32,
|
|
pub wad_type: [u8; 0x2],
|
|
pub wad_version: U16,
|
|
pub cert_chain_size: U32,
|
|
pub _reserved1: [u8; 0x4],
|
|
pub ticket_size: U32,
|
|
pub tmd_size: U32,
|
|
pub data_size: U32,
|
|
pub footer_size: U32,
|
|
}
|
|
|
|
static_assert!(size_of::<WadHeader>() == 0x20);
|
|
|
|
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
|
#[repr(C, align(4))]
|
|
pub struct ContentMetadata {
|
|
pub content_id: U32,
|
|
pub content_index: U16,
|
|
pub content_type: U16,
|
|
pub size: U64,
|
|
pub hash: HashBytes,
|
|
}
|
|
|
|
static_assert!(size_of::<ContentMetadata>() == 0x24);
|
|
|
|
impl ContentMetadata {
|
|
#[inline]
|
|
pub fn iv(&self) -> [u8; 0x10] {
|
|
let mut iv = [0u8; 0x10];
|
|
*array_ref_mut!(iv, 0, 2) = self.content_index.get().to_be_bytes();
|
|
iv
|
|
}
|
|
}
|
|
|
|
const ALIGNMENT: usize = 0x40;
|
|
|
|
#[inline(always)]
|
|
pub fn align_up(value: u64, alignment: u64) -> u64 { (value + alignment - 1) & !(alignment - 1) }
|
|
|
|
pub type HashBytes = [u8; 20];
|
|
pub type KeyBytes = [u8; 16];
|
|
|
|
type Aes128Cbc = cbc::Decryptor<aes::Aes128>;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct WadFile {
|
|
pub header: WadHeader,
|
|
pub title_key: KeyBytes,
|
|
pub fake_signed: bool,
|
|
pub raw_cert_chain: Box<[u8]>,
|
|
pub raw_ticket: Box<[u8]>,
|
|
pub raw_tmd: Box<[u8]>,
|
|
pub content_offset: u64,
|
|
}
|
|
|
|
impl WadFile {
|
|
pub fn ticket(&self) -> &Ticket {
|
|
Ticket::ref_from_bytes(&self.raw_ticket).expect("Invalid ticket alignment")
|
|
}
|
|
|
|
pub fn tmd(&self) -> &TmdHeader {
|
|
TmdHeader::ref_from_prefix(&self.raw_tmd).expect("Invalid TMD alignment").0
|
|
}
|
|
|
|
pub fn contents(&self) -> &[ContentMetadata] {
|
|
let (_, cmd_data) =
|
|
TmdHeader::ref_from_prefix(&self.raw_tmd).expect("Invalid TMD alignment");
|
|
<[ContentMetadata]>::ref_from_bytes(cmd_data).expect("Invalid CMD alignment")
|
|
}
|
|
|
|
pub fn content_offset(&self, content_index: u16) -> u64 {
|
|
let contents = self.contents();
|
|
let mut offset = self.content_offset;
|
|
for content in contents.iter().take(content_index as usize) {
|
|
offset = align_up(offset + content.size.get(), ALIGNMENT as u64);
|
|
}
|
|
offset
|
|
}
|
|
|
|
pub fn trailer_offset(&self) -> u64 {
|
|
let contents = self.contents();
|
|
let mut offset = self.content_offset;
|
|
for content in contents.iter() {
|
|
offset = align_up(offset + content.size.get(), ALIGNMENT as u64);
|
|
}
|
|
offset
|
|
}
|
|
}
|
|
|
|
pub fn process_wad<R>(reader: &mut R) -> Result<WadFile>
|
|
where R: BufRead + Seek + ?Sized {
|
|
let header: WadHeader = read_from(reader)?;
|
|
let mut offset = align_up(header.header_size.get() as u64, ALIGNMENT as u64);
|
|
|
|
reader.seek(io::SeekFrom::Start(offset))?;
|
|
let raw_cert_chain: Box<[u8]> = read_box_slice(reader, header.cert_chain_size.get() as usize)?;
|
|
offset = align_up(offset + header.cert_chain_size.get() as u64, ALIGNMENT as u64);
|
|
|
|
reader.seek(io::SeekFrom::Start(offset))?;
|
|
let raw_ticket: Box<[u8]> = read_box_slice(reader, header.ticket_size.get() as usize)?;
|
|
offset = align_up(offset + header.ticket_size.get() as u64, ALIGNMENT as u64);
|
|
|
|
reader.seek(io::SeekFrom::Start(offset))?;
|
|
let raw_tmd: Box<[u8]> = read_box_slice(reader, header.tmd_size.get() as usize)?;
|
|
offset = align_up(offset + header.tmd_size.get() as u64, ALIGNMENT as u64);
|
|
|
|
let content_offset = offset;
|
|
let mut file = WadFile {
|
|
header,
|
|
title_key: [0; 16],
|
|
fake_signed: false,
|
|
raw_cert_chain,
|
|
raw_ticket,
|
|
raw_tmd,
|
|
content_offset,
|
|
};
|
|
|
|
let mut title_key_found = false;
|
|
if file.ticket().header.sig.iter().all(|&x| x == 0) {
|
|
// Fake signed, try to determine common key index
|
|
file.fake_signed = true;
|
|
let contents = file.contents();
|
|
if let Some(smallest_content) = contents.iter().min_by_key(|x| x.size.get()) {
|
|
let mut ticket = file.ticket().clone();
|
|
for i in 0..2 {
|
|
ticket.common_key_idx = i;
|
|
let title_key = ticket.decrypt_title_key()?;
|
|
let offset = file.content_offset(smallest_content.content_index.get());
|
|
reader.seek(io::SeekFrom::Start(offset))?;
|
|
if verify_content(reader, smallest_content, &title_key)? {
|
|
file.title_key = title_key;
|
|
title_key_found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if !title_key_found {
|
|
bail!("Failed to determine title key for fake signed WAD");
|
|
}
|
|
}
|
|
if !title_key_found {
|
|
let title_key = file.ticket().decrypt_title_key()?;
|
|
file.title_key = title_key;
|
|
}
|
|
|
|
Ok(file)
|
|
}
|
|
|
|
pub fn verify_wad<R>(file: &WadFile, reader: &mut R) -> Result<()>
|
|
where R: Read + Seek + ?Sized {
|
|
for content in file.contents() {
|
|
let content_index = content.content_index.get();
|
|
println!(
|
|
"Verifying content {:08x} (size {})",
|
|
content_index,
|
|
Size::from_bytes(content.size.get())
|
|
);
|
|
let offset = file.content_offset(content_index);
|
|
reader.seek(io::SeekFrom::Start(offset))?;
|
|
if !verify_content(reader, content, &file.title_key)? {
|
|
bail!("Content {:08x} hash mismatch", content_index);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_content<R>(
|
|
reader: &mut R,
|
|
content: &ContentMetadata,
|
|
title_key: &KeyBytes,
|
|
) -> Result<bool>
|
|
where
|
|
R: Read + ?Sized,
|
|
{
|
|
let mut buf = <[[u8; 0x10]]>::new_box_zeroed_with_elems(0x200)
|
|
.map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
|
// Read full padded size for decryption
|
|
let read_size = align_up(content.size.get(), 0x40);
|
|
let mut decryptor = Aes128Cbc::new(title_key.into(), (&content.iv()).into());
|
|
let mut digest = Sha1::default();
|
|
let mut read = 0;
|
|
while read < read_size {
|
|
let len = buf.len().min(usize::try_from(read_size - read).unwrap_or(usize::MAX));
|
|
debug_assert_eq!(len % 0x10, 0);
|
|
reader.read_exact(&mut buf.as_mut_bytes()[..len])?;
|
|
for block in buf.iter_mut().take(len / 0x10) {
|
|
decryptor.decrypt_block_mut(block.into());
|
|
}
|
|
// Only hash up to content size
|
|
let hash_len = (read + len as u64).min(content.size.get()).saturating_sub(read) as usize;
|
|
if hash_len > 0 {
|
|
digest.update(&buf.as_bytes()[..hash_len]);
|
|
}
|
|
read += len as u64;
|
|
}
|
|
Ok(HashBytes::from(digest.finalize()) == content.hash)
|
|
}
|