From 374c6950b2c3793d2b59ade1097c91d2a026669c Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 7 Nov 2024 23:56:43 -0700 Subject: [PATCH] Support decrypted discs & decrypt/encrypt conversion --- Cargo.lock | 4 +- Cargo.toml | 2 +- nod/src/disc/hashes.rs | 16 ++-- nod/src/disc/mod.rs | 37 ++++++++-- nod/src/disc/reader.rs | 114 ++++++++++++++++++++--------- nod/src/disc/wii.rs | 145 +++++++++++++++++++++++++------------ nod/src/io/block.rs | 36 +++++---- nod/src/io/ciso.rs | 8 +- nod/src/io/gcz.rs | 8 +- nod/src/io/iso.rs | 8 +- nod/src/io/mod.rs | 16 ++-- nod/src/io/nfs.rs | 22 +++--- nod/src/io/tgc.rs | 7 +- nod/src/io/wbfs.rs | 8 +- nod/src/io/wia.rs | 22 +++--- nod/src/lib.rs | 86 ++++++++++++++++++---- nodtool/Cargo.toml | 2 +- nodtool/src/cmd/convert.rs | 19 ++++- nodtool/src/cmd/dat.rs | 8 +- nodtool/src/cmd/extract.rs | 26 ++++--- nodtool/src/cmd/info.rs | 61 +++++++++------- nodtool/src/cmd/verify.rs | 4 +- nodtool/src/util/shared.rs | 16 ++-- 23 files changed, 453 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84d4d26..166eb18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,7 +441,7 @@ dependencies = [ [[package]] name = "nod" -version = "1.4.4" +version = "2.0.0-alpha.1" dependencies = [ "adler", "aes", @@ -464,7 +464,7 @@ dependencies = [ [[package]] name = "nodtool" -version = "1.4.4" +version = "2.0.0-alpha.1" dependencies = [ "argp", "base16ct", diff --git a/Cargo.toml b/Cargo.toml index e35c167..56c6bb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ strip = "debuginfo" codegen-units = 1 [workspace.package] -version = "1.4.4" +version = "2.0.0-alpha.1" edition = "2021" rust-version = "1.74" authors = ["Luke Street "] diff --git a/nod/src/disc/hashes.rs b/nod/src/disc/hashes.rs index a9e0392..5605623 100644 --- a/nod/src/disc/hashes.rs +++ b/nod/src/disc/hashes.rs @@ -16,7 +16,7 @@ use crate::{ }, io::HashBytes, util::read::read_box_slice, - OpenOptions, Result, ResultContext, SECTOR_SIZE, + PartitionOptions, Result, ResultContext, SECTOR_SIZE, }; /// In a sector, following the 0x400 byte block of hashes, each 0x400 bytes of decrypted data is @@ -81,7 +81,7 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> { // Precompute hashes for zeroed sectors. const ZERO_H0_BYTES: &[u8] = &[0u8; HASHES_SIZE]; - let zero_h0_hash = hash_bytes(ZERO_H0_BYTES); + let zero_h0_hash = sha1_hash(ZERO_H0_BYTES); let partitions = reader.partitions(); let mut hash_tables = Vec::with_capacity(partitions.len()); @@ -97,8 +97,9 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> { let group_count = hash_table.h3_hashes.len(); let mutex = Arc::new(Mutex::new(hash_table)); + let partition_options = PartitionOptions { validate_hashes: false }; (0..group_count).into_par_iter().try_for_each_with( - (reader.open_partition(part.index, &OpenOptions::default())?, mutex.clone()), + (reader.open_partition(part.index, &partition_options)?, mutex.clone()), |(stream, mutex), h3_index| -> Result<()> { let mut result = HashResult::new_box_zeroed()?; let mut data_buf = <[u8]>::new_box_zeroed_with_elems(SECTOR_DATA_SIZE)?; @@ -122,7 +123,7 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> { .read_exact(&mut data_buf) .with_context(|| format!("Reading sector {}", part_sector))?; for h0_index in 0..NUM_H0_HASHES { - let h0_hash = hash_bytes(array_ref![ + let h0_hash = sha1_hash(array_ref![ data_buf, h0_index * HASHES_SIZE, HASHES_SIZE @@ -196,9 +197,6 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> { Ok(()) } +/// Hashes a byte slice with SHA-1. #[inline] -pub fn hash_bytes(buf: &[u8]) -> HashBytes { - let mut hasher = Sha1::new(); - hasher.update(buf); - hasher.finalize().into() -} +pub fn sha1_hash(buf: &[u8]) -> HashBytes { HashBytes::from(Sha1::digest(buf)) } diff --git a/nod/src/disc/mod.rs b/nod/src/disc/mod.rs index 144f961..44ea033 100644 --- a/nod/src/disc/mod.rs +++ b/nod/src/disc/mod.rs @@ -24,7 +24,7 @@ pub(crate) mod wii; pub use fst::{Fst, Node, NodeKind}; pub use streams::{FileStream, OwnedFileStream, WindowedStream}; -pub use wii::{SignedHeader, Ticket, TicketLimit, TmdHeader, REGION_SIZE}; +pub use wii::{ContentMetadata, SignedHeader, Ticket, TicketLimit, TmdHeader, REGION_SIZE}; /// Size in bytes of a disc sector. (32 KiB) pub const SECTOR_SIZE: usize = 0x8000; @@ -90,6 +90,14 @@ impl DiscHeader { /// Whether this is a Wii disc. #[inline] pub fn is_wii(&self) -> bool { self.wii_magic == WII_MAGIC } + + /// Whether the disc has partition data hashes. + #[inline] + pub fn has_partition_hashes(&self) -> bool { self.no_partition_hashes == 0 } + + /// Whether the disc has partition data encryption. + #[inline] + pub fn has_partition_encryption(&self) -> bool { self.no_partition_encryption == 0 } } /// A header describing the contents of a disc partition. @@ -379,19 +387,23 @@ impl PartitionMeta { /// A view into the disc header. #[inline] pub fn header(&self) -> &DiscHeader { - DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::()]).unwrap() + DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::()]) + .expect("Invalid header alignment") } /// A view into the partition header. #[inline] pub fn partition_header(&self) -> &PartitionHeader { - PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::()..]).unwrap() + PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::()..]) + .expect("Invalid partition header alignment") } /// A view into the apploader header. #[inline] pub fn apploader_header(&self) -> &ApploaderHeader { - ApploaderHeader::ref_from_prefix(&self.raw_apploader).unwrap().0 + ApploaderHeader::ref_from_prefix(&self.raw_apploader) + .expect("Invalid apploader alignment") + .0 } /// A view into the file system table (FST). @@ -400,18 +412,29 @@ impl PartitionMeta { /// A view into the DOL header. #[inline] - pub fn dol_header(&self) -> &DolHeader { DolHeader::ref_from_prefix(&self.raw_dol).unwrap().0 } + pub fn dol_header(&self) -> &DolHeader { + DolHeader::ref_from_prefix(&self.raw_dol).expect("Invalid DOL alignment").0 + } /// A view into the ticket. (Wii only) #[inline] pub fn ticket(&self) -> Option<&Ticket> { - self.raw_ticket.as_ref().and_then(|v| Ticket::ref_from_bytes(v).ok()) + let raw_ticket = self.raw_ticket.as_deref()?; + Some(Ticket::ref_from_bytes(raw_ticket).expect("Invalid ticket alignment")) } /// A view into the TMD. (Wii only) #[inline] pub fn tmd_header(&self) -> Option<&TmdHeader> { - self.raw_tmd.as_ref().and_then(|v| TmdHeader::ref_from_prefix(v).ok().map(|(v, _)| v)) + let raw_tmd = self.raw_tmd.as_deref()?; + Some(TmdHeader::ref_from_prefix(raw_tmd).expect("Invalid TMD alignment").0) + } + + /// A view into the TMD content metadata. (Wii only) + #[inline] + pub fn content_metadata(&self) -> Option<&[ContentMetadata]> { + let raw_cmd = &self.raw_tmd.as_deref()?[size_of::()..]; + Some(<[ContentMetadata]>::ref_from_bytes(raw_cmd).expect("Invalid CMD alignment")) } } diff --git a/nod/src/disc/reader.rs b/nod/src/disc/reader.rs index 14cab0c..645ba55 100644 --- a/nod/src/disc/reader.rs +++ b/nod/src/disc/reader.rs @@ -3,7 +3,7 @@ use std::{ io::{BufRead, Read, Seek, SeekFrom}, }; -use zerocopy::FromZeros; +use zerocopy::{FromBytes, FromZeros}; use super::{ gcn::PartitionGC, @@ -16,15 +16,10 @@ use crate::{ disc::wii::REGION_OFFSET, io::block::{Block, BlockIO, PartitionInfo}, util::read::{read_box, read_from, read_vec}, - DiscMeta, Error, OpenOptions, Result, ResultContext, SECTOR_SIZE, + DiscMeta, Error, OpenOptions, PartitionEncryptionMode, PartitionOptions, Result, ResultContext, + SECTOR_SIZE, }; -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub enum EncryptionMode { - Encrypted, - Decrypted, -} - pub struct DiscReader { io: Box, block: Block, @@ -33,7 +28,7 @@ pub struct DiscReader { sector_buf: Box<[u8; SECTOR_SIZE]>, sector_idx: u32, pos: u64, - mode: EncryptionMode, + mode: PartitionEncryptionMode, disc_header: Box, pub(crate) partitions: Vec, hash_tables: Vec, @@ -71,11 +66,7 @@ impl DiscReader { sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?, sector_idx: u32::MAX, pos: 0, - mode: if options.rebuild_encryption { - EncryptionMode::Encrypted - } else { - EncryptionMode::Decrypted - }, + mode: options.partition_encryption, disc_header: DiscHeader::new_box_zeroed()?, partitions: vec![], hash_tables: vec![], @@ -84,11 +75,28 @@ impl DiscReader { let disc_header: Box = read_box(&mut reader).context("Reading disc header")?; reader.disc_header = disc_header; if reader.disc_header.is_wii() { + if reader.disc_header.has_partition_encryption() + && !reader.disc_header.has_partition_hashes() + { + return Err(Error::DiscFormat( + "Wii disc is encrypted but has no partition hashes".to_string(), + )); + } + if !reader.disc_header.has_partition_hashes() + && options.partition_encryption == PartitionEncryptionMode::ForceEncrypted + { + return Err(Error::Other( + "Unsupported: Rebuilding encryption for Wii disc without hashes".to_string(), + )); + } reader.seek(SeekFrom::Start(REGION_OFFSET)).context("Seeking to region info")?; reader.region = Some(read_from(&mut reader).context("Reading region info")?); reader.partitions = read_partition_info(&mut reader)?; // Rebuild hashes if the format requires it - if (options.rebuild_encryption || options.validate_hashes) && meta.needs_hash_recovery { + if options.partition_encryption != PartitionEncryptionMode::AsIs + && meta.needs_hash_recovery + && reader.disc_header.has_partition_hashes() + { rebuild_hashes(&mut reader)?; } } @@ -125,7 +133,7 @@ impl DiscReader { pub fn open_partition( &self, index: usize, - options: &OpenOptions, + options: &PartitionOptions, ) -> Result> { if self.disc_header.is_gamecube() { if index == 0 { @@ -145,7 +153,7 @@ impl DiscReader { pub fn open_partition_kind( &self, kind: PartitionKind, - options: &OpenOptions, + options: &PartitionOptions, ) -> Result> { if self.disc_header.is_gamecube() { if kind == PartitionKind::Data { @@ -182,30 +190,51 @@ impl BufRead for DiscReader { // Read new sector into buffer if abs_sector != self.sector_idx { - if let Some(partition) = partition { - match self.mode { - EncryptionMode::Decrypted => self.block.decrypt( + match (self.mode, partition, self.disc_header.has_partition_encryption()) { + (PartitionEncryptionMode::Original, Some(partition), true) + | (PartitionEncryptionMode::ForceEncrypted, Some(partition), _) => { + self.block.encrypt( self.sector_buf.as_mut(), self.block_buf.as_ref(), abs_sector, partition, - )?, - EncryptionMode::Encrypted => self.block.encrypt( - self.sector_buf.as_mut(), - self.block_buf.as_ref(), - abs_sector, - partition, - )?, + )?; + } + (PartitionEncryptionMode::ForceDecrypted, Some(partition), _) => { + self.block.decrypt( + self.sector_buf.as_mut(), + self.block_buf.as_ref(), + abs_sector, + partition, + )?; + } + (PartitionEncryptionMode::AsIs, _, _) | (_, None, _) | (_, _, false) => { + self.block.copy_raw( + self.sector_buf.as_mut(), + self.block_buf.as_ref(), + abs_sector, + &self.disc_header, + )?; } - } else { - self.block.copy_raw( - self.sector_buf.as_mut(), - self.block_buf.as_ref(), - abs_sector, - &self.disc_header, - )?; } self.sector_idx = abs_sector; + + if self.sector_idx == 0 + && self.disc_header.is_wii() + && matches!( + self.mode, + PartitionEncryptionMode::ForceDecrypted + | PartitionEncryptionMode::ForceEncrypted + ) + { + let (disc_header, _) = DiscHeader::mut_from_prefix(self.sector_buf.as_mut()) + .expect("Invalid disc header alignment"); + disc_header.no_partition_encryption = match self.mode { + PartitionEncryptionMode::ForceDecrypted => 1, + PartitionEncryptionMode::ForceEncrypted => 0, + _ => unreachable!(), + }; + } } // Read from sector buffer @@ -273,8 +302,19 @@ fn read_partition_info(reader: &mut DiscReader) -> Result> { "Partition {group_idx}:{part_idx} offset is not sector aligned", ))); } + + let disc_header = reader.header(); let data_start_offset = entry.offset() + header.data_off(); - let data_end_offset = data_start_offset + header.data_size(); + let mut data_size = header.data_size(); + if data_size == 0 { + // Read until next partition or end of disc + // TODO: handle multiple partition groups + data_size = entries + .get(part_idx + 1) + .map(|part| part.offset() - data_start_offset) + .unwrap_or(reader.disc_size() - data_start_offset); + } + let data_end_offset = data_start_offset + data_size; if data_start_offset % SECTOR_SIZE as u64 != 0 || data_end_offset % SECTOR_SIZE as u64 != 0 { @@ -293,13 +333,15 @@ fn read_partition_info(reader: &mut DiscReader) -> Result> { disc_header: DiscHeader::new_box_zeroed()?, partition_header: PartitionHeader::new_box_zeroed()?, hash_table: None, + has_encryption: disc_header.has_partition_encryption(), + has_hashes: disc_header.has_partition_hashes(), }; let mut partition_reader = PartitionWii::new( reader.io.clone(), reader.disc_header.clone(), &info, - &OpenOptions::default(), + &PartitionOptions { validate_hashes: false }, )?; info.disc_header = read_box(&mut partition_reader).context("Reading disc header")?; info.partition_header = diff --git a/nod/src/disc/wii.rs b/nod/src/disc/wii.rs index 6541d29..9aca343 100644 --- a/nod/src/disc/wii.rs +++ b/nod/src/disc/wii.rs @@ -16,13 +16,13 @@ use crate::{ array_ref, disc::streams::OwnedFileStream, io::{ - aes_decrypt, + aes_cbc_decrypt, block::{Block, BlockIO, PartitionInfo}, - KeyBytes, + HashBytes, KeyBytes, }, static_assert, util::{div_rem, read::read_box_slice}, - Error, OpenOptions, Result, ResultContext, + Error, PartitionOptions, Result, ResultContext, }; /// Size in bytes of the hashes block in a Wii disc sector @@ -179,7 +179,7 @@ impl Ticket { format!("unknown common key index {}", self.common_key_idx), ))?; let mut title_key = self.title_key; - aes_decrypt(common_key, iv, &mut title_key); + aes_cbc_decrypt(common_key, &iv, &mut title_key); Ok(title_key) } } @@ -231,6 +231,24 @@ pub struct TmdHeader { static_assert!(size_of::() == 0x1E4); +/// TMD content metadata +#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C, align(4))] +pub struct ContentMetadata { + /// Content ID + pub content_id: U32, + /// Content index + pub content_index: U16, + /// Content type + pub content_type: U16, + /// Content size + pub size: U64, + /// Content hash + pub hash: HashBytes, +} + +static_assert!(size_of::() == 0x24); + pub const H3_TABLE_SIZE: usize = 0x18000; #[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] @@ -275,10 +293,10 @@ pub struct PartitionWii { sector_buf: Box<[u8; SECTOR_SIZE]>, sector: u32, pos: u64, - verify: bool, - raw_tmd: Box<[u8]>, - raw_cert_chain: Box<[u8]>, - raw_h3_table: Box<[u8]>, + options: PartitionOptions, + raw_tmd: Option>, + raw_cert_chain: Option>, + raw_h3_table: Option>, } impl Clone for PartitionWii { @@ -292,7 +310,7 @@ impl Clone for PartitionWii { sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(), sector: u32::MAX, pos: 0, - verify: self.verify, + options: self.options.clone(), raw_tmd: self.raw_tmd.clone(), raw_cert_chain: self.raw_cert_chain.clone(), raw_h3_table: self.raw_h3_table.clone(), @@ -305,29 +323,43 @@ impl PartitionWii { inner: Box, disc_header: Box, partition: &PartitionInfo, - options: &OpenOptions, + options: &PartitionOptions, ) -> Result> { let block_size = inner.block_size(); let mut reader = PartitionGC::new(inner, disc_header)?; // Read TMD, cert chain, and H3 table let offset = partition.start_sector as u64 * SECTOR_SIZE as u64; - reader - .seek(SeekFrom::Start(offset + partition.header.tmd_off())) - .context("Seeking to TMD offset")?; - let raw_tmd: Box<[u8]> = read_box_slice(&mut reader, partition.header.tmd_size() as usize) - .context("Reading TMD")?; - reader - .seek(SeekFrom::Start(offset + partition.header.cert_chain_off())) - .context("Seeking to cert chain offset")?; - let raw_cert_chain: Box<[u8]> = - read_box_slice(&mut reader, partition.header.cert_chain_size() as usize) - .context("Reading cert chain")?; - reader - .seek(SeekFrom::Start(offset + partition.header.h3_table_off())) - .context("Seeking to H3 table offset")?; - let raw_h3_table: Box<[u8]> = - read_box_slice(&mut reader, H3_TABLE_SIZE).context("Reading H3 table")?; + let raw_tmd = if partition.header.tmd_size() != 0 { + reader + .seek(SeekFrom::Start(offset + partition.header.tmd_off())) + .context("Seeking to TMD offset")?; + Some( + read_box_slice::(&mut reader, partition.header.tmd_size() as usize) + .context("Reading TMD")?, + ) + } else { + None + }; + let raw_cert_chain = if partition.header.cert_chain_size() != 0 { + reader + .seek(SeekFrom::Start(offset + partition.header.cert_chain_off())) + .context("Seeking to cert chain offset")?; + Some( + read_box_slice::(&mut reader, partition.header.cert_chain_size() as usize) + .context("Reading cert chain")?, + ) + } else { + None + }; + let raw_h3_table = if partition.has_hashes { + reader + .seek(SeekFrom::Start(offset + partition.header.h3_table_off())) + .context("Seeking to H3 table offset")?; + Some(read_box_slice::(&mut reader, H3_TABLE_SIZE).context("Reading H3 table")?) + } else { + None + }; Ok(Box::new(Self { io: reader.into_inner(), @@ -338,7 +370,7 @@ impl PartitionWii { sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?, sector: u32::MAX, pos: 0, - verify: options.validate_hashes, + options: options.clone(), raw_tmd, raw_cert_chain, raw_h3_table, @@ -348,37 +380,60 @@ impl PartitionWii { impl BufRead for PartitionWii { fn fill_buf(&mut self) -> io::Result<&[u8]> { - let part_sector = (self.pos / SECTOR_DATA_SIZE as u64) as u32; + let part_sector = if self.partition.has_hashes { + (self.pos / SECTOR_DATA_SIZE as u64) as u32 + } else { + (self.pos / SECTOR_SIZE as u64) as u32 + }; let abs_sector = self.partition.data_start_sector + part_sector; if abs_sector >= self.partition.data_end_sector { return Ok(&[]); } - let block_idx = - (abs_sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32; // Read new block if necessary + let block_idx = + (abs_sector as u64 * SECTOR_SIZE as u64 / self.block_buf.len() as u64) as u32; if block_idx != self.block_idx { - self.block = - self.io.read_block(self.block_buf.as_mut(), block_idx, Some(&self.partition))?; + self.block = self.io.read_block( + self.block_buf.as_mut(), + block_idx, + self.partition.has_encryption.then_some(&self.partition), + )?; self.block_idx = block_idx; } // Decrypt sector if necessary if abs_sector != self.sector { - self.block.decrypt( - self.sector_buf.as_mut(), - self.block_buf.as_ref(), - abs_sector, - &self.partition, - )?; - if self.verify { - verify_hashes(self.sector_buf.as_ref(), part_sector, self.raw_h3_table.as_ref())?; + if self.partition.has_encryption { + self.block.decrypt( + self.sector_buf.as_mut(), + self.block_buf.as_ref(), + abs_sector, + &self.partition, + )?; + } else { + self.block.copy_raw( + self.sector_buf.as_mut(), + self.block_buf.as_ref(), + abs_sector, + &self.partition.disc_header, + )?; + } + if self.options.validate_hashes { + if let Some(h3_table) = self.raw_h3_table.as_deref() { + verify_hashes(self.sector_buf.as_ref(), part_sector, h3_table)?; + } } self.sector = abs_sector; } - let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize; - Ok(&self.sector_buf[HASHES_SIZE + offset..]) + if self.partition.has_hashes { + let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize; + Ok(&self.sector_buf[HASHES_SIZE + offset..]) + } else { + let offset = (self.pos % SECTOR_SIZE as u64) as usize; + Ok(&self.sector_buf[offset..]) + } } #[inline] @@ -489,9 +544,9 @@ impl PartitionBase for PartitionWii { self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?; let mut meta = read_part_meta(self, true)?; meta.raw_ticket = Some(Box::from(self.partition.header.ticket.as_bytes())); - meta.raw_tmd = Some(self.raw_tmd.clone()); - meta.raw_cert_chain = Some(self.raw_cert_chain.clone()); - meta.raw_h3_table = Some(self.raw_h3_table.clone()); + meta.raw_tmd = self.raw_tmd.clone(); + meta.raw_cert_chain = self.raw_cert_chain.clone(); + meta.raw_h3_table = self.raw_h3_table.clone(); Ok(meta) } diff --git a/nod/src/io/block.rs b/nod/src/io/block.rs index a322fb2..a56e699 100644 --- a/nod/src/io/block.rs +++ b/nod/src/io/block.rs @@ -15,7 +15,8 @@ use crate::{ DiscHeader, PartitionHeader, PartitionKind, GCN_MAGIC, SECTOR_SIZE, WII_MAGIC, }, io::{ - aes_decrypt, aes_encrypt, split::SplitFileReader, DiscMeta, Format, KeyBytes, MagicBytes, + aes_cbc_decrypt, aes_cbc_encrypt, split::SplitFileReader, DiscMeta, Format, KeyBytes, + MagicBytes, }, util::{lfg::LaggedFibonacci, read::read_from}, Error, Result, ResultContext, @@ -218,9 +219,9 @@ pub struct PartitionInfo { pub kind: PartitionKind, /// The start sector of the partition. pub start_sector: u32, - /// The start sector of the partition's (encrypted) data. + /// The start sector of the partition's data. pub data_start_sector: u32, - /// The end sector of the partition's (encrypted) data. + /// The end sector of the partition's data. pub data_end_sector: u32, /// The AES key for the partition, also known as the "title key". pub key: KeyBytes, @@ -232,6 +233,10 @@ pub struct PartitionInfo { pub partition_header: Box, /// The hash table for the partition, if rebuilt. pub hash_table: Option, + /// Whether the partition data is encrypted + pub has_encryption: bool, + /// Whether the partition data hashes are present + pub has_hashes: bool, } /// The block kind returned by [`BlockIO::read_block`]. @@ -239,6 +244,8 @@ pub struct PartitionInfo { pub enum Block { /// Raw data or encrypted Wii partition data Raw, + /// Encrypted Wii partition data + PartEncrypted, /// Decrypted Wii partition data PartDecrypted { /// Whether the sector has its hash block intact @@ -264,6 +271,9 @@ impl Block { match self { Block::Raw => { out.copy_from_slice(block_sector::(data, abs_sector)?); + } + Block::PartEncrypted => { + out.copy_from_slice(block_sector::(data, abs_sector)?); decrypt_sector(out, partition); } Block::PartDecrypted { has_hashes } => { @@ -296,6 +306,10 @@ impl Block { match self { Block::Raw => { out.copy_from_slice(block_sector::(data, abs_sector)?); + encrypt_sector(out, partition); + } + Block::PartEncrypted => { + out.copy_from_slice(block_sector::(data, abs_sector)?); } Block::PartDecrypted { has_hashes } => { out.copy_from_slice(block_sector::(data, abs_sector)?); @@ -327,15 +341,9 @@ impl Block { disc_header: &DiscHeader, ) -> io::Result<()> { match self { - Block::Raw => { + Block::Raw | Block::PartEncrypted | Block::PartDecrypted { .. } => { out.copy_from_slice(block_sector::(data, abs_sector)?); } - Block::PartDecrypted { .. } => { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Cannot copy decrypted data as raw", - )); - } Block::Junk => generate_junk(out, abs_sector, None, disc_header), Block::Zero => out.fill(0), } @@ -406,15 +414,15 @@ fn rebuild_hash_block(out: &mut [u8; SECTOR_SIZE], part_sector: u32, partition: } fn encrypt_sector(out: &mut [u8; SECTOR_SIZE], partition: &PartitionInfo) { - aes_encrypt(&partition.key, [0u8; 16], &mut out[..HASHES_SIZE]); + aes_cbc_encrypt(&partition.key, &[0u8; 16], &mut out[..HASHES_SIZE]); // Data IV from encrypted hash block let iv = *array_ref![out, 0x3D0, 16]; - aes_encrypt(&partition.key, iv, &mut out[HASHES_SIZE..]); + aes_cbc_encrypt(&partition.key, &iv, &mut out[HASHES_SIZE..]); } fn decrypt_sector(out: &mut [u8; SECTOR_SIZE], partition: &PartitionInfo) { // Data IV from encrypted hash block let iv = *array_ref![out, 0x3D0, 16]; - aes_decrypt(&partition.key, [0u8; 16], &mut out[..HASHES_SIZE]); - aes_decrypt(&partition.key, iv, &mut out[HASHES_SIZE..]); + aes_cbc_decrypt(&partition.key, &[0u8; 16], &mut out[..HASHES_SIZE]); + aes_cbc_decrypt(&partition.key, &iv, &mut out[HASHES_SIZE..]); } diff --git a/nod/src/io/ciso.rs b/nod/src/io/ciso.rs index 89b81f2..2f6466e 100644 --- a/nod/src/io/ciso.rs +++ b/nod/src/io/ciso.rs @@ -85,7 +85,7 @@ impl BlockIO for DiscIOCISO { &mut self, out: &mut [u8], block: u32, - _partition: Option<&PartitionInfo>, + partition: Option<&PartitionInfo>, ) -> io::Result { if block >= CISO_MAP_SIZE as u32 { // Out of bounds @@ -109,7 +109,11 @@ impl BlockIO for DiscIOCISO { + phys_block as u64 * self.header.block_size.get() as u64; self.inner.seek(SeekFrom::Start(file_offset))?; self.inner.read_exact(out)?; - Ok(Block::Raw) + + match partition { + Some(partition) if partition.has_encryption => Ok(Block::PartEncrypted), + _ => Ok(Block::Raw), + } } fn block_size_internal(&self) -> u32 { self.header.block_size.get() } diff --git a/nod/src/io/gcz.rs b/nod/src/io/gcz.rs index 94ae326..c12028f 100644 --- a/nod/src/io/gcz.rs +++ b/nod/src/io/gcz.rs @@ -83,7 +83,7 @@ impl BlockIO for DiscIOGCZ { &mut self, out: &mut [u8], block: u32, - _partition: Option<&PartitionInfo>, + partition: Option<&PartitionInfo>, ) -> io::Result { if block >= self.header.block_count.get() { // Out of bounds @@ -166,7 +166,11 @@ impl BlockIO for DiscIOGCZ { // Copy uncompressed block out.copy_from_slice(self.block_buf.as_slice()); } - Ok(Block::Raw) + + match partition { + Some(partition) if partition.has_encryption => Ok(Block::PartEncrypted), + _ => Ok(Block::Raw), + } } fn block_size_internal(&self) -> u32 { self.header.block_size.get() } diff --git a/nod/src/io/iso.rs b/nod/src/io/iso.rs index 484b85b..aea206f 100644 --- a/nod/src/io/iso.rs +++ b/nod/src/io/iso.rs @@ -31,7 +31,7 @@ impl BlockIO for DiscIOISO { &mut self, out: &mut [u8], block: u32, - _partition: Option<&PartitionInfo>, + partition: Option<&PartitionInfo>, ) -> io::Result { let offset = block as u64 * SECTOR_SIZE as u64; if offset >= self.stream_len { @@ -48,7 +48,11 @@ impl BlockIO for DiscIOISO { } else { self.inner.read_exact(out)?; } - Ok(Block::Raw) + + match partition { + Some(partition) if partition.has_encryption => Ok(Block::PartEncrypted), + _ => Ok(Block::Raw), + } } fn block_size_internal(&self) -> u32 { SECTOR_SIZE as u32 } diff --git a/nod/src/io/mod.rs b/nod/src/io/mod.rs index a7cd586..a8e1e8b 100644 --- a/nod/src/io/mod.rs +++ b/nod/src/io/mod.rs @@ -124,19 +124,19 @@ pub struct DiscMeta { } /// Encrypts data in-place using AES-128-CBC with the given key and IV. -#[inline(always)] -pub(crate) fn aes_encrypt(key: &KeyBytes, iv: KeyBytes, data: &mut [u8]) { +/// Requires the data length to be a multiple of the AES block size (16 bytes). +pub fn aes_cbc_encrypt(key: &KeyBytes, iv: &KeyBytes, data: &mut [u8]) { use aes::cipher::{block_padding::NoPadding, BlockEncryptMut, KeyIvInit}; - >::new(key.into(), &aes::Block::from(iv)) + >::new(key.into(), iv.into()) .encrypt_padded_mut::(data, data.len()) - .unwrap(); // Safe: using NoPadding + .unwrap(); } /// Decrypts data in-place using AES-128-CBC with the given key and IV. -#[inline(always)] -pub(crate) fn aes_decrypt(key: &KeyBytes, iv: KeyBytes, data: &mut [u8]) { +/// Requires the data length to be a multiple of the AES block size (16 bytes). +pub fn aes_cbc_decrypt(key: &KeyBytes, iv: &KeyBytes, data: &mut [u8]) { use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit}; - >::new(key.into(), &aes::Block::from(iv)) + >::new(key.into(), iv.into()) .decrypt_padded_mut::(data) - .unwrap(); // Safe: using NoPadding + .unwrap(); } diff --git a/nod/src/io/nfs.rs b/nod/src/io/nfs.rs index 776ecb7..799ee47 100644 --- a/nod/src/io/nfs.rs +++ b/nod/src/io/nfs.rs @@ -9,9 +9,10 @@ use std::{ use zerocopy::{big_endian::U32, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; use crate::{ + array_ref_mut, disc::SECTOR_SIZE, io::{ - aes_decrypt, + aes_cbc_decrypt, block::{Block, BlockIO, PartitionInfo, NFS_MAGIC}, split::SplitFileReader, Format, KeyBytes, MagicBytes, @@ -125,18 +126,15 @@ impl BlockIO for DiscIONFS { self.inner.read_exact(out)?; // Decrypt - let iv_bytes = sector.to_be_bytes(); - #[rustfmt::skip] - let iv: KeyBytes = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - iv_bytes[0], iv_bytes[1], iv_bytes[2], iv_bytes[3], - ]; - aes_decrypt(&self.key, iv, out); + let mut iv = [0u8; 0x10]; + *array_ref_mut!(iv, 12, 4) = sector.to_be_bytes(); + aes_cbc_decrypt(&self.key, &iv, out); - if partition.is_some() { - Ok(Block::PartDecrypted { has_hashes: true }) - } else { - Ok(Block::Raw) + match partition { + Some(partition) if partition.has_encryption => { + Ok(Block::PartDecrypted { has_hashes: true }) + } + _ => Ok(Block::Raw), } } diff --git a/nod/src/io/tgc.rs b/nod/src/io/tgc.rs index 1da150a..0508c9e 100644 --- a/nod/src/io/tgc.rs +++ b/nod/src/io/tgc.rs @@ -96,7 +96,7 @@ impl BlockIO for DiscIOTGC { &mut self, out: &mut [u8], block: u32, - _partition: Option<&PartitionInfo>, + partition: Option<&PartitionInfo>, ) -> io::Result { let offset = self.header.header_offset.get() as u64 + block as u64 * SECTOR_SIZE as u64; if offset >= self.stream_len { @@ -137,7 +137,10 @@ impl BlockIO for DiscIOTGC { .copy_from_slice(&self.fst[fst_offset..fst_offset + copy_len]); } - Ok(Block::Raw) + match partition { + Some(partition) if partition.has_encryption => Ok(Block::PartEncrypted), + _ => Ok(Block::Raw), + } } fn block_size_internal(&self) -> u32 { SECTOR_SIZE as u32 } diff --git a/nod/src/io/wbfs.rs b/nod/src/io/wbfs.rs index d5c7327..a9c31ac 100644 --- a/nod/src/io/wbfs.rs +++ b/nod/src/io/wbfs.rs @@ -97,7 +97,7 @@ impl BlockIO for DiscIOWBFS { &mut self, out: &mut [u8], block: u32, - _partition: Option<&PartitionInfo>, + partition: Option<&PartitionInfo>, ) -> io::Result { let block_size = self.header.block_size(); if block >= self.header.max_blocks() { @@ -120,7 +120,11 @@ impl BlockIO for DiscIOWBFS { let block_start = block_size as u64 * phys_block as u64; self.inner.seek(SeekFrom::Start(block_start))?; self.inner.read_exact(out)?; - Ok(Block::Raw) + + match partition { + Some(partition) if partition.has_encryption => Ok(Block::PartEncrypted), + _ => Ok(Block::Raw), + } } fn block_size_internal(&self) -> u32 { self.header.block_size() } diff --git a/nod/src/io/wia.rs b/nod/src/io/wia.rs index f33eea0..0020afb 100644 --- a/nod/src/io/wia.rs +++ b/nod/src/io/wia.rs @@ -8,7 +8,7 @@ use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout}; use crate::{ disc::{ - hashes::hash_bytes, + hashes::sha1_hash, wii::{HASHES_SIZE, SECTOR_DATA_SIZE}, SECTOR_SIZE, }, @@ -530,7 +530,7 @@ impl Clone for DiscIOWIA { } fn verify_hash(buf: &[u8], expected: &HashBytes) -> Result<()> { - let out = hash_bytes(buf); + let out = sha1_hash(buf); if out != *expected { let mut got_bytes = [0u8; 40]; let got = base16ct::lower::encode_str(&out, &mut got_bytes).unwrap(); // Safe: fixed buffer size @@ -684,7 +684,10 @@ impl BlockIO for DiscIOWIA { let chunk_size = self.disc.chunk_size.get(); let sectors_per_chunk = chunk_size / SECTOR_SIZE as u32; - let (group_index, group_sector, partition_offset) = if let Some(partition) = partition { + let in_partition = partition.is_some_and(|info| info.has_encryption); + let (group_index, group_sector, partition_offset) = if in_partition { + let partition = partition.unwrap(); + // Find the partition let Some(wia_part) = self.partitions.get(partition.index) else { return Err(io::Error::new( @@ -783,7 +786,7 @@ impl BlockIO for DiscIOWIA { // Read group data if necessary if group_index != self.group { - let group_data_size = if partition.is_some() { + let group_data_size = if in_partition { // Within a partition, hashes are excluded from the data size (sectors_per_chunk * SECTOR_DATA_SIZE as u32) as usize } else { @@ -798,11 +801,8 @@ impl BlockIO for DiscIOWIA { matches!(self.disc.compression(), WIACompression::None | WIACompression::Purge) || !group.is_compressed(); if uncompressed_exception_lists { - self.exception_lists = read_exception_lists( - &mut reader, - partition.is_some(), - self.disc.chunk_size.get(), - )?; + self.exception_lists = + read_exception_lists(&mut reader, in_partition, self.disc.chunk_size.get())?; // Align to 4 let rem = reader.stream_position()? % 4; if rem != 0 { @@ -817,7 +817,7 @@ impl BlockIO for DiscIOWIA { if !uncompressed_exception_lists { self.exception_lists = read_exception_lists( reader.as_mut(), - partition.is_some(), + in_partition, self.disc.chunk_size.get(), )?; } @@ -861,7 +861,7 @@ impl BlockIO for DiscIOWIA { } // Read sector from cached group data - if partition.is_some() { + if in_partition { let sector_data_start = group_sector as usize * SECTOR_DATA_SIZE; out[..HASHES_SIZE].fill(0); out[HASHES_SIZE..SECTOR_SIZE].copy_from_slice( diff --git a/nod/src/lib.rs b/nod/src/lib.rs index f895071..0a8f67f 100644 --- a/nod/src/lib.rs +++ b/nod/src/lib.rs @@ -46,8 +46,8 @@ //! Converting a disc image to raw ISO: //! //! ```no_run -//! // Enable `rebuild_encryption` to ensure the output is a valid ISO. -//! let options = nod::OpenOptions { rebuild_encryption: true, ..Default::default() }; +//! // Enable `PartitionEncryptionMode::Original` to ensure the output is a valid ISO. +//! let options = nod::OpenOptions { partition_encryption: nod::PartitionEncryptionMode::Original }; //! let mut disc = nod::Disc::new_with_options("path/to/file.rvz", &options) //! .expect("Failed to open disc"); //! @@ -64,9 +64,9 @@ use std::{ }; pub use disc::{ - ApploaderHeader, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind, OwnedFileStream, - PartitionBase, PartitionHeader, PartitionKind, PartitionMeta, SignedHeader, Ticket, - TicketLimit, TmdHeader, WindowedStream, BI2_SIZE, BOOT_SIZE, DL_DVD_SIZE, GCN_MAGIC, + ApploaderHeader, ContentMetadata, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind, + OwnedFileStream, PartitionBase, PartitionHeader, PartitionKind, PartitionMeta, SignedHeader, + Ticket, TicketLimit, TmdHeader, WindowedStream, BI2_SIZE, BOOT_SIZE, DL_DVD_SIZE, GCN_MAGIC, MINI_DVD_SIZE, REGION_SIZE, SECTOR_SIZE, SL_DVD_SIZE, WII_MAGIC, }; pub use io::{ @@ -150,13 +150,46 @@ where E: ErrorContext } } +/// Wii partition encryption mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum PartitionEncryptionMode { + /// Partition data is read as it's stored in the underlying disc format. + /// For example, WIA/RVZ partitions are stored decrypted, so this avoids + /// rebuilding the partition encryption and hash data if it will only be + /// read via [`PartitionBase`]. If it's desired to read a full disc image + /// via [`Disc`], use [`PartitionEncryptionMode::Original`] instead. + #[default] + AsIs, + /// Partition encryption and hashes are rebuilt to match its original state, + /// if necessary. This is used for converting or verifying a disc image. + Original, + /// Partition data will be encrypted if reading a decrypted disc image. + /// Modifies the disc header to mark partition data as encrypted. + ForceEncrypted, + /// Partition data will be decrypted if reading an encrypted disc image. + /// Modifies the disc header to mark partition data as decrypted. + ForceDecrypted, +} + /// Options for opening a disc image. #[derive(Default, Debug, Clone)] pub struct OpenOptions { - /// Wii: Rebuild partition data encryption and hashes if the underlying format stores data - /// decrypted or with hashes removed. (e.g. WIA/RVZ, NFS) - pub rebuild_encryption: bool, - /// Wii: Validate partition data hashes while reading the disc image. + /// Wii: Partition encryption mode. By default, partitions are read as they + /// are stored in the underlying disc format, avoiding extra work when the + /// underlying format stores them decrypted (e.g. WIA/RVZ). + /// + /// This can be changed to [`PartitionEncryptionMode::Original`] to rebuild + /// partition encryption and hashes to match its original state for conversion + /// or verification. + pub partition_encryption: PartitionEncryptionMode, +} + +/// Options for opening a partition. +#[derive(Default, Debug, Clone)] +pub struct PartitionOptions { + /// Wii: Validate data hashes while reading the partition, if available. + /// To ensure hashes are present, regardless of the underlying disc format, + /// set [`OpenOptions::partition_encryption`] to [`PartitionEncryptionMode::Original`]. pub validate_hashes: bool, } @@ -165,7 +198,6 @@ pub struct OpenOptions { /// This is the primary entry point for reading disc images. pub struct Disc { reader: disc::reader::DiscReader, - options: OpenOptions, } impl Disc { @@ -180,7 +212,7 @@ impl Disc { pub fn new_with_options>(path: P, options: &OpenOptions) -> Result { let io = io::block::open(path.as_ref())?; let reader = disc::reader::DiscReader::new(io, options)?; - Ok(Disc { reader, options: options.clone() }) + Ok(Disc { reader }) } /// Opens a disc image from a read stream. @@ -197,7 +229,7 @@ impl Disc { ) -> Result { let io = io::block::new(stream)?; let reader = disc::reader::DiscReader::new(io, options)?; - Ok(Disc { reader, options: options.clone() }) + Ok(Disc { reader }) } /// Detects the format of a disc image from a read stream. @@ -236,7 +268,20 @@ impl Disc { /// **GameCube**: `index` must always be 0. #[inline] pub fn open_partition(&self, index: usize) -> Result> { - self.reader.open_partition(index, &self.options) + self.open_partition_with_options(index, &PartitionOptions::default()) + } + + /// Opens a decrypted partition read stream for the specified partition index + /// with custom options. + /// + /// **GameCube**: `index` must always be 0. + #[inline] + pub fn open_partition_with_options( + &self, + index: usize, + options: &PartitionOptions, + ) -> Result> { + self.reader.open_partition(index, options) } /// Opens a decrypted partition read stream for the first partition matching @@ -245,7 +290,20 @@ impl Disc { /// **GameCube**: `kind` must always be [`PartitionKind::Data`]. #[inline] pub fn open_partition_kind(&self, kind: PartitionKind) -> Result> { - self.reader.open_partition_kind(kind, &self.options) + self.reader.open_partition_kind(kind, &PartitionOptions::default()) + } + + /// Opens a decrypted partition read stream for the first partition matching + /// the specified kind with custom options. + /// + /// **GameCube**: `kind` must always be [`PartitionKind::Data`]. + #[inline] + pub fn open_partition_kind_with_options( + &self, + kind: PartitionKind, + options: &PartitionOptions, + ) -> Result> { + self.reader.open_partition_kind(kind, options) } } diff --git a/nodtool/Cargo.toml b/nodtool/Cargo.toml index bae1f61..6764958 100644 --- a/nodtool/Cargo.toml +++ b/nodtool/Cargo.toml @@ -30,7 +30,7 @@ indicatif = "0.17" itertools = "0.13" log = "0.4" md-5 = "0.10" -nod = { version = "1.2", path = "../nod" } +nod = { version = "2.0.0-alpha", path = "../nod" } quick-xml = { version = "0.36", features = ["serialize"] } serde = { version = "1.0", features = ["derive"] } sha1 = "0.10" diff --git a/nodtool/src/cmd/convert.rs b/nodtool/src/cmd/convert.rs index 991a03a..8af1e42 100644 --- a/nodtool/src/cmd/convert.rs +++ b/nodtool/src/cmd/convert.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use argp::FromArgs; +use nod::OpenOptions; use crate::util::{redump, shared::convert_and_verify}; @@ -20,6 +21,12 @@ pub struct Args { #[argp(option, short = 'd')] /// path to DAT file(s) for verification (optional) dat: Vec, + #[argp(switch)] + /// decrypt Wii partition data + decrypt: bool, + #[argp(switch)] + /// encrypt Wii partition data + encrypt: bool, } pub fn run(args: Args) -> nod::Result<()> { @@ -27,5 +34,15 @@ pub fn run(args: Args) -> nod::Result<()> { println!("Loading dat files..."); redump::load_dats(args.dat.iter().map(PathBuf::as_ref))?; } - convert_and_verify(&args.file, Some(&args.out), args.md5) + let options = OpenOptions { + partition_encryption: match (args.decrypt, args.encrypt) { + (true, false) => nod::PartitionEncryptionMode::ForceDecrypted, + (false, true) => nod::PartitionEncryptionMode::ForceEncrypted, + (false, false) => nod::PartitionEncryptionMode::Original, + (true, true) => { + return Err(nod::Error::Other("Both --decrypt and --encrypt specified".to_string())) + } + }, + }; + convert_and_verify(&args.file, Some(&args.out), args.md5, &options) } diff --git a/nodtool/src/cmd/dat.rs b/nodtool/src/cmd/dat.rs index fae8ef4..c306050 100644 --- a/nodtool/src/cmd/dat.rs +++ b/nodtool/src/cmd/dat.rs @@ -10,7 +10,7 @@ use std::{ use argp::FromArgs; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; -use nod::{Disc, OpenOptions, Result, ResultContext}; +use nod::{Disc, OpenOptions, PartitionEncryptionMode, Result, ResultContext}; use zerocopy::FromZeros; use crate::util::{ @@ -165,10 +165,8 @@ struct DiscHashes { } fn load_disc(path: &Path, name: &str, full_verify: bool) -> Result { - let mut disc = Disc::new_with_options(path, &OpenOptions { - rebuild_encryption: true, - validate_hashes: false, - })?; + let options = OpenOptions { partition_encryption: PartitionEncryptionMode::Original }; + let mut disc = Disc::new_with_options(path, &options)?; let disc_size = disc.disc_size(); if !full_verify { let meta = disc.meta(); diff --git a/nodtool/src/cmd/extract.rs b/nodtool/src/cmd/extract.rs index fdeb131..02be1a6 100644 --- a/nodtool/src/cmd/extract.rs +++ b/nodtool/src/cmd/extract.rs @@ -9,7 +9,8 @@ use std::{ use argp::FromArgs; use itertools::Itertools; use nod::{ - Disc, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta, ResultContext, + Disc, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta, PartitionOptions, + ResultContext, }; use size::{Base, Size}; use zerocopy::IntoBytes; @@ -52,36 +53,39 @@ pub fn run(args: Args) -> nod::Result<()> { } else { output_dir = args.file.with_extension(""); } - let disc = Disc::new_with_options(&args.file, &OpenOptions { - rebuild_encryption: false, - validate_hashes: args.validate, - })?; + let disc = Disc::new_with_options(&args.file, &OpenOptions::default())?; let header = disc.header(); let is_wii = header.is_wii(); + let partition_options = PartitionOptions { validate_hashes: args.validate }; if let Some(partition) = args.partition { if partition.eq_ignore_ascii_case("all") { for info in disc.partitions() { let mut out_dir = output_dir.clone(); out_dir.push(info.kind.dir_name().as_ref()); - let mut partition = disc.open_partition(info.index)?; + let mut partition = + disc.open_partition_with_options(info.index, &partition_options)?; extract_partition(&disc, partition.as_mut(), &out_dir, is_wii, args.quiet)?; } } else if partition.eq_ignore_ascii_case("data") { - let mut partition = disc.open_partition_kind(PartitionKind::Data)?; + let mut partition = + disc.open_partition_kind_with_options(PartitionKind::Data, &partition_options)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?; } else if partition.eq_ignore_ascii_case("update") { - let mut partition = disc.open_partition_kind(PartitionKind::Update)?; + let mut partition = + disc.open_partition_kind_with_options(PartitionKind::Update, &partition_options)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?; } else if partition.eq_ignore_ascii_case("channel") { - let mut partition = disc.open_partition_kind(PartitionKind::Channel)?; + let mut partition = + disc.open_partition_kind_with_options(PartitionKind::Channel, &partition_options)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?; } else { let idx = partition.parse::().map_err(|_| "Invalid partition index")?; - let mut partition = disc.open_partition(idx)?; + let mut partition = disc.open_partition_with_options(idx, &partition_options)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?; } } else { - let mut partition = disc.open_partition_kind(PartitionKind::Data)?; + let mut partition = + disc.open_partition_kind_with_options(PartitionKind::Data, &partition_options)?; extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?; } Ok(()) diff --git a/nodtool/src/cmd/info.rs b/nodtool/src/cmd/info.rs index ee08784..16c2d95 100644 --- a/nodtool/src/cmd/info.rs +++ b/nodtool/src/cmd/info.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use argp::FromArgs; -use nod::{Disc, OpenOptions, SECTOR_SIZE}; +use nod::{Disc, SECTOR_SIZE}; use size::Size; use crate::util::{display, shared::print_header}; @@ -24,16 +24,16 @@ pub fn run(args: Args) -> nod::Result<()> { fn info_file(path: &Path) -> nod::Result<()> { log::info!("Loading {}", display(path)); - let disc = Disc::new_with_options(path, &OpenOptions { - rebuild_encryption: false, - validate_hashes: false, - })?; + let disc = Disc::new(path)?; let header = disc.header(); let meta = disc.meta(); print_header(header, &meta); if header.is_wii() { for (idx, info) in disc.partitions().iter().enumerate() { + let mut partition = disc.open_partition(idx)?; + let meta = partition.meta()?; + println!(); println!("Partition {}", idx); println!("\tType: {}", info.kind); @@ -41,43 +41,50 @@ fn info_file(path: &Path) -> nod::Result<()> { println!("\tStart sector: {} (offset {:#X})", info.start_sector, offset); let data_size = (info.data_end_sector - info.data_start_sector) as u64 * SECTOR_SIZE as u64; + if info.has_encryption { + println!( + "\tEncrypted data offset / size: {:#X} / {:#X} ({})", + info.data_start_sector as u64 * SECTOR_SIZE as u64, + data_size, + Size::from_bytes(data_size) + ); + } else { + println!( + "\tDecrypted data offset / size: {:#X} / {:#X} ({})", + offset, + data_size, + Size::from_bytes(data_size) + ); + } println!( - "\tData offset / size: {:#X} / {:#X} ({})", - info.data_start_sector as u64 * SECTOR_SIZE as u64, - data_size, - Size::from_bytes(data_size) - ); - println!( - "\tTMD offset / size: {:#X} / {:#X}", + "\tTMD offset / size: {:#X} / {:#X}", offset + info.header.tmd_off(), info.header.tmd_size() ); + if let Some(content_metadata) = meta.content_metadata() { + for content in content_metadata { + println!( + "\t-> Content {:08X} size: {:#X} ({})", + content.content_index.get(), + content.size.get(), + Size::from_bytes(content.size.get()), + ); + } + } println!( - "\tCert offset / size: {:#X} / {:#X}", + "\tCert chain offset / size: {:#X} / {:#X}", offset + info.header.cert_chain_off(), info.header.cert_chain_size() ); println!( - "\tH3 offset / size: {:#X} / {:#X}", + "\tH3 table offset / size: {:#X} / {:#X}", offset + info.header.h3_table_off(), info.header.h3_table_size() ); - let mut partition = disc.open_partition(idx)?; - let meta = partition.meta()?; let tmd = meta.tmd_header(); let title_id_str = if let Some(tmd) = tmd { - format!( - "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", - tmd.title_id[0], - tmd.title_id[1], - tmd.title_id[2], - tmd.title_id[3], - tmd.title_id[4], - tmd.title_id[5], - tmd.title_id[6], - tmd.title_id[7] - ) + hex::encode_upper(tmd.title_id) } else { "N/A".to_string() }; diff --git a/nodtool/src/cmd/verify.rs b/nodtool/src/cmd/verify.rs index c1e35bc..c70086d 100644 --- a/nodtool/src/cmd/verify.rs +++ b/nodtool/src/cmd/verify.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use argp::FromArgs; +use nod::{OpenOptions, PartitionEncryptionMode}; use crate::util::{redump, shared::convert_and_verify}; @@ -24,8 +25,9 @@ pub fn run(args: Args) -> nod::Result<()> { println!("Loading dat files..."); redump::load_dats(args.dat.iter().map(PathBuf::as_ref))?; } + let options = OpenOptions { partition_encryption: PartitionEncryptionMode::Original }; for file in &args.file { - convert_and_verify(file, None, args.md5)?; + convert_and_verify(file, None, args.md5, &options)?; println!(); } Ok(()) diff --git a/nodtool/src/util/shared.rs b/nodtool/src/util/shared.rs index 52fde42..8758763 100644 --- a/nodtool/src/util/shared.rs +++ b/nodtool/src/util/shared.rs @@ -38,20 +38,22 @@ pub fn print_header(header: &DiscHeader, meta: &DiscMeta) { println!("Title: {}", header.game_title_str()); println!("Game ID: {}", header.game_id_str()); println!("Disc {}, Revision {}", header.disc_num + 1, header.disc_version); - if header.no_partition_hashes != 0 { + if !header.has_partition_hashes() { println!("[!] Disc has no hashes"); } - if header.no_partition_encryption != 0 { + if !header.has_partition_encryption() { println!("[!] Disc is not encrypted"); } } -pub fn convert_and_verify(in_file: &Path, out_file: Option<&Path>, md5: bool) -> Result<()> { +pub fn convert_and_verify( + in_file: &Path, + out_file: Option<&Path>, + md5: bool, + options: &OpenOptions, +) -> Result<()> { println!("Loading {}", display(in_file)); - let mut disc = Disc::new_with_options(in_file, &OpenOptions { - rebuild_encryption: true, - validate_hashes: false, - })?; + let mut disc = Disc::new_with_options(in_file, options)?; let header = disc.header(); let meta = disc.meta(); print_header(header, &meta);