Skip to content

Commit 374c695

Browse files
committed
Support decrypted discs & decrypt/encrypt conversion
1 parent df8ab22 commit 374c695

File tree

23 files changed

+450
-219
lines changed

23 files changed

+450
-219
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ strip = "debuginfo"
99
codegen-units = 1
1010

1111
[workspace.package]
12-
version = "1.4.4"
12+
version = "2.0.0-alpha.1"
1313
edition = "2021"
1414
rust-version = "1.74"
1515
authors = ["Luke Street <luke@street.dev>"]

nod/src/disc/hashes.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::{
1616
},
1717
io::HashBytes,
1818
util::read::read_box_slice,
19-
OpenOptions, Result, ResultContext, SECTOR_SIZE,
19+
PartitionOptions, Result, ResultContext, SECTOR_SIZE,
2020
};
2121

2222
/// 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<()> {
8181

8282
// Precompute hashes for zeroed sectors.
8383
const ZERO_H0_BYTES: &[u8] = &[0u8; HASHES_SIZE];
84-
let zero_h0_hash = hash_bytes(ZERO_H0_BYTES);
84+
let zero_h0_hash = sha1_hash(ZERO_H0_BYTES);
8585

8686
let partitions = reader.partitions();
8787
let mut hash_tables = Vec::with_capacity(partitions.len());
@@ -97,8 +97,9 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
9797

9898
let group_count = hash_table.h3_hashes.len();
9999
let mutex = Arc::new(Mutex::new(hash_table));
100+
let partition_options = PartitionOptions { validate_hashes: false };
100101
(0..group_count).into_par_iter().try_for_each_with(
101-
(reader.open_partition(part.index, &OpenOptions::default())?, mutex.clone()),
102+
(reader.open_partition(part.index, &partition_options)?, mutex.clone()),
102103
|(stream, mutex), h3_index| -> Result<()> {
103104
let mut result = HashResult::new_box_zeroed()?;
104105
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<()> {
122123
.read_exact(&mut data_buf)
123124
.with_context(|| format!("Reading sector {}", part_sector))?;
124125
for h0_index in 0..NUM_H0_HASHES {
125-
let h0_hash = hash_bytes(array_ref![
126+
let h0_hash = sha1_hash(array_ref![
126127
data_buf,
127128
h0_index * HASHES_SIZE,
128129
HASHES_SIZE
@@ -196,9 +197,6 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
196197
Ok(())
197198
}
198199

200+
/// Hashes a byte slice with SHA-1.
199201
#[inline]
200-
pub fn hash_bytes(buf: &[u8]) -> HashBytes {
201-
let mut hasher = Sha1::new();
202-
hasher.update(buf);
203-
hasher.finalize().into()
204-
}
202+
pub fn sha1_hash(buf: &[u8]) -> HashBytes { HashBytes::from(Sha1::digest(buf)) }

nod/src/disc/mod.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub(crate) mod wii;
2424

2525
pub use fst::{Fst, Node, NodeKind};
2626
pub use streams::{FileStream, OwnedFileStream, WindowedStream};
27-
pub use wii::{SignedHeader, Ticket, TicketLimit, TmdHeader, REGION_SIZE};
27+
pub use wii::{ContentMetadata, SignedHeader, Ticket, TicketLimit, TmdHeader, REGION_SIZE};
2828

2929
/// Size in bytes of a disc sector. (32 KiB)
3030
pub const SECTOR_SIZE: usize = 0x8000;
@@ -90,6 +90,14 @@ impl DiscHeader {
9090
/// Whether this is a Wii disc.
9191
#[inline]
9292
pub fn is_wii(&self) -> bool { self.wii_magic == WII_MAGIC }
93+
94+
/// Whether the disc has partition data hashes.
95+
#[inline]
96+
pub fn has_partition_hashes(&self) -> bool { self.no_partition_hashes == 0 }
97+
98+
/// Whether the disc has partition data encryption.
99+
#[inline]
100+
pub fn has_partition_encryption(&self) -> bool { self.no_partition_encryption == 0 }
93101
}
94102

95103
/// A header describing the contents of a disc partition.
@@ -379,19 +387,23 @@ impl PartitionMeta {
379387
/// A view into the disc header.
380388
#[inline]
381389
pub fn header(&self) -> &DiscHeader {
382-
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()]).unwrap()
390+
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()])
391+
.expect("Invalid header alignment")
383392
}
384393

385394
/// A view into the partition header.
386395
#[inline]
387396
pub fn partition_header(&self) -> &PartitionHeader {
388-
PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::<DiscHeader>()..]).unwrap()
397+
PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::<DiscHeader>()..])
398+
.expect("Invalid partition header alignment")
389399
}
390400

391401
/// A view into the apploader header.
392402
#[inline]
393403
pub fn apploader_header(&self) -> &ApploaderHeader {
394-
ApploaderHeader::ref_from_prefix(&self.raw_apploader).unwrap().0
404+
ApploaderHeader::ref_from_prefix(&self.raw_apploader)
405+
.expect("Invalid apploader alignment")
406+
.0
395407
}
396408

397409
/// A view into the file system table (FST).
@@ -400,18 +412,29 @@ impl PartitionMeta {
400412

401413
/// A view into the DOL header.
402414
#[inline]
403-
pub fn dol_header(&self) -> &DolHeader { DolHeader::ref_from_prefix(&self.raw_dol).unwrap().0 }
415+
pub fn dol_header(&self) -> &DolHeader {
416+
DolHeader::ref_from_prefix(&self.raw_dol).expect("Invalid DOL alignment").0
417+
}
404418

405419
/// A view into the ticket. (Wii only)
406420
#[inline]
407421
pub fn ticket(&self) -> Option<&Ticket> {
408-
self.raw_ticket.as_ref().and_then(|v| Ticket::ref_from_bytes(v).ok())
422+
let raw_ticket = self.raw_ticket.as_deref()?;
423+
Some(Ticket::ref_from_bytes(raw_ticket).expect("Invalid ticket alignment"))
409424
}
410425

411426
/// A view into the TMD. (Wii only)
412427
#[inline]
413428
pub fn tmd_header(&self) -> Option<&TmdHeader> {
414-
self.raw_tmd.as_ref().and_then(|v| TmdHeader::ref_from_prefix(v).ok().map(|(v, _)| v))
429+
let raw_tmd = self.raw_tmd.as_deref()?;
430+
Some(TmdHeader::ref_from_prefix(raw_tmd).expect("Invalid TMD alignment").0)
431+
}
432+
433+
/// A view into the TMD content metadata. (Wii only)
434+
#[inline]
435+
pub fn content_metadata(&self) -> Option<&[ContentMetadata]> {
436+
let raw_cmd = &self.raw_tmd.as_deref()?[size_of::<TmdHeader>()..];
437+
Some(<[ContentMetadata]>::ref_from_bytes(raw_cmd).expect("Invalid CMD alignment"))
415438
}
416439
}
417440

nod/src/disc/reader.rs

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
io::{BufRead, Read, Seek, SeekFrom},
44
};
55

6-
use zerocopy::FromZeros;
6+
use zerocopy::{FromBytes, FromZeros};
77

88
use super::{
99
gcn::PartitionGC,
@@ -16,15 +16,10 @@ use crate::{
1616
disc::wii::REGION_OFFSET,
1717
io::block::{Block, BlockIO, PartitionInfo},
1818
util::read::{read_box, read_from, read_vec},
19-
DiscMeta, Error, OpenOptions, Result, ResultContext, SECTOR_SIZE,
19+
DiscMeta, Error, OpenOptions, PartitionEncryptionMode, PartitionOptions, Result, ResultContext,
20+
SECTOR_SIZE,
2021
};
2122

22-
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
23-
pub enum EncryptionMode {
24-
Encrypted,
25-
Decrypted,
26-
}
27-
2823
pub struct DiscReader {
2924
io: Box<dyn BlockIO>,
3025
block: Block,
@@ -33,7 +28,7 @@ pub struct DiscReader {
3328
sector_buf: Box<[u8; SECTOR_SIZE]>,
3429
sector_idx: u32,
3530
pos: u64,
36-
mode: EncryptionMode,
31+
mode: PartitionEncryptionMode,
3732
disc_header: Box<DiscHeader>,
3833
pub(crate) partitions: Vec<PartitionInfo>,
3934
hash_tables: Vec<HashTable>,
@@ -71,11 +66,7 @@ impl DiscReader {
7166
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
7267
sector_idx: u32::MAX,
7368
pos: 0,
74-
mode: if options.rebuild_encryption {
75-
EncryptionMode::Encrypted
76-
} else {
77-
EncryptionMode::Decrypted
78-
},
69+
mode: options.partition_encryption,
7970
disc_header: DiscHeader::new_box_zeroed()?,
8071
partitions: vec![],
8172
hash_tables: vec![],
@@ -84,11 +75,28 @@ impl DiscReader {
8475
let disc_header: Box<DiscHeader> = read_box(&mut reader).context("Reading disc header")?;
8576
reader.disc_header = disc_header;
8677
if reader.disc_header.is_wii() {
78+
if reader.disc_header.has_partition_encryption()
79+
&& !reader.disc_header.has_partition_hashes()
80+
{
81+
return Err(Error::DiscFormat(
82+
"Wii disc is encrypted but has no partition hashes".to_string(),
83+
));
84+
}
85+
if !reader.disc_header.has_partition_hashes()
86+
&& options.partition_encryption == PartitionEncryptionMode::ForceEncrypted
87+
{
88+
return Err(Error::Other(
89+
"Unsupported: Rebuilding encryption for Wii disc without hashes".to_string(),
90+
));
91+
}
8792
reader.seek(SeekFrom::Start(REGION_OFFSET)).context("Seeking to region info")?;
8893
reader.region = Some(read_from(&mut reader).context("Reading region info")?);
8994
reader.partitions = read_partition_info(&mut reader)?;
9095
// Rebuild hashes if the format requires it
91-
if (options.rebuild_encryption || options.validate_hashes) && meta.needs_hash_recovery {
96+
if options.partition_encryption != PartitionEncryptionMode::AsIs
97+
&& meta.needs_hash_recovery
98+
&& reader.disc_header.has_partition_hashes()
99+
{
92100
rebuild_hashes(&mut reader)?;
93101
}
94102
}
@@ -125,7 +133,7 @@ impl DiscReader {
125133
pub fn open_partition(
126134
&self,
127135
index: usize,
128-
options: &OpenOptions,
136+
options: &PartitionOptions,
129137
) -> Result<Box<dyn PartitionBase>> {
130138
if self.disc_header.is_gamecube() {
131139
if index == 0 {
@@ -145,7 +153,7 @@ impl DiscReader {
145153
pub fn open_partition_kind(
146154
&self,
147155
kind: PartitionKind,
148-
options: &OpenOptions,
156+
options: &PartitionOptions,
149157
) -> Result<Box<dyn PartitionBase>> {
150158
if self.disc_header.is_gamecube() {
151159
if kind == PartitionKind::Data {
@@ -182,30 +190,51 @@ impl BufRead for DiscReader {
182190

183191
// Read new sector into buffer
184192
if abs_sector != self.sector_idx {
185-
if let Some(partition) = partition {
186-
match self.mode {
187-
EncryptionMode::Decrypted => self.block.decrypt(
193+
match (self.mode, partition, self.disc_header.has_partition_encryption()) {
194+
(PartitionEncryptionMode::Original, Some(partition), true)
195+
| (PartitionEncryptionMode::ForceEncrypted, Some(partition), _) => {
196+
self.block.encrypt(
188197
self.sector_buf.as_mut(),
189198
self.block_buf.as_ref(),
190199
abs_sector,
191200
partition,
192-
)?,
193-
EncryptionMode::Encrypted => self.block.encrypt(
201+
)?;
202+
}
203+
(PartitionEncryptionMode::ForceDecrypted, Some(partition), _) => {
204+
self.block.decrypt(
194205
self.sector_buf.as_mut(),
195206
self.block_buf.as_ref(),
196207
abs_sector,
197208
partition,
198-
)?,
209+
)?;
210+
}
211+
(PartitionEncryptionMode::AsIs, _, _) | (_, None, _) | (_, _, false) => {
212+
self.block.copy_raw(
213+
self.sector_buf.as_mut(),
214+
self.block_buf.as_ref(),
215+
abs_sector,
216+
&self.disc_header,
217+
)?;
199218
}
200-
} else {
201-
self.block.copy_raw(
202-
self.sector_buf.as_mut(),
203-
self.block_buf.as_ref(),
204-
abs_sector,
205-
&self.disc_header,
206-
)?;
207219
}
208220
self.sector_idx = abs_sector;
221+
222+
if self.sector_idx == 0
223+
&& self.disc_header.is_wii()
224+
&& matches!(
225+
self.mode,
226+
PartitionEncryptionMode::ForceDecrypted
227+
| PartitionEncryptionMode::ForceEncrypted
228+
)
229+
{
230+
let (disc_header, _) = DiscHeader::mut_from_prefix(self.sector_buf.as_mut())
231+
.expect("Invalid disc header alignment");
232+
disc_header.no_partition_encryption = match self.mode {
233+
PartitionEncryptionMode::ForceDecrypted => 1,
234+
PartitionEncryptionMode::ForceEncrypted => 0,
235+
_ => unreachable!(),
236+
};
237+
}
209238
}
210239

211240
// Read from sector buffer
@@ -273,8 +302,19 @@ fn read_partition_info(reader: &mut DiscReader) -> Result<Vec<PartitionInfo>> {
273302
"Partition {group_idx}:{part_idx} offset is not sector aligned",
274303
)));
275304
}
305+
306+
let disc_header = reader.header();
276307
let data_start_offset = entry.offset() + header.data_off();
277-
let data_end_offset = data_start_offset + header.data_size();
308+
let mut data_size = header.data_size();
309+
if data_size == 0 {
310+
// Read until next partition or end of disc
311+
// TODO: handle multiple partition groups
312+
data_size = entries
313+
.get(part_idx + 1)
314+
.map(|part| part.offset() - data_start_offset)
315+
.unwrap_or(reader.disc_size() - data_start_offset);
316+
}
317+
let data_end_offset = data_start_offset + data_size;
278318
if data_start_offset % SECTOR_SIZE as u64 != 0
279319
|| data_end_offset % SECTOR_SIZE as u64 != 0
280320
{
@@ -293,13 +333,15 @@ fn read_partition_info(reader: &mut DiscReader) -> Result<Vec<PartitionInfo>> {
293333
disc_header: DiscHeader::new_box_zeroed()?,
294334
partition_header: PartitionHeader::new_box_zeroed()?,
295335
hash_table: None,
336+
has_encryption: disc_header.has_partition_encryption(),
337+
has_hashes: disc_header.has_partition_hashes(),
296338
};
297339

298340
let mut partition_reader = PartitionWii::new(
299341
reader.io.clone(),
300342
reader.disc_header.clone(),
301343
&info,
302-
&OpenOptions::default(),
344+
&PartitionOptions { validate_hashes: false },
303345
)?;
304346
info.disc_header = read_box(&mut partition_reader).context("Reading disc header")?;
305347
info.partition_header =

0 commit comments

Comments
 (0)