mirror of https://github.com/encounter/nod-rs.git
Support decrypted discs & decrypt/encrypt conversion
This commit is contained in:
parent
df8ab228c8
commit
374c6950b2
|
@ -441,7 +441,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nod"
|
name = "nod"
|
||||||
version = "1.4.4"
|
version = "2.0.0-alpha.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler",
|
"adler",
|
||||||
"aes",
|
"aes",
|
||||||
|
@ -464,7 +464,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodtool"
|
name = "nodtool"
|
||||||
version = "1.4.4"
|
version = "2.0.0-alpha.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argp",
|
"argp",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
|
|
@ -9,7 +9,7 @@ strip = "debuginfo"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.4.4"
|
version = "2.0.0-alpha.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.74"
|
rust-version = "1.74"
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
io::HashBytes,
|
io::HashBytes,
|
||||||
util::read::read_box_slice,
|
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
|
/// 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.
|
// Precompute hashes for zeroed sectors.
|
||||||
const ZERO_H0_BYTES: &[u8] = &[0u8; HASHES_SIZE];
|
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 partitions = reader.partitions();
|
||||||
let mut hash_tables = Vec::with_capacity(partitions.len());
|
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 group_count = hash_table.h3_hashes.len();
|
||||||
let mutex = Arc::new(Mutex::new(hash_table));
|
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(
|
(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<()> {
|
|(stream, mutex), h3_index| -> Result<()> {
|
||||||
let mut result = HashResult::new_box_zeroed()?;
|
let mut result = HashResult::new_box_zeroed()?;
|
||||||
let mut data_buf = <[u8]>::new_box_zeroed_with_elems(SECTOR_DATA_SIZE)?;
|
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)
|
.read_exact(&mut data_buf)
|
||||||
.with_context(|| format!("Reading sector {}", part_sector))?;
|
.with_context(|| format!("Reading sector {}", part_sector))?;
|
||||||
for h0_index in 0..NUM_H0_HASHES {
|
for h0_index in 0..NUM_H0_HASHES {
|
||||||
let h0_hash = hash_bytes(array_ref![
|
let h0_hash = sha1_hash(array_ref![
|
||||||
data_buf,
|
data_buf,
|
||||||
h0_index * HASHES_SIZE,
|
h0_index * HASHES_SIZE,
|
||||||
HASHES_SIZE
|
HASHES_SIZE
|
||||||
|
@ -196,9 +197,6 @@ pub fn rebuild_hashes(reader: &mut DiscReader) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hashes a byte slice with SHA-1.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hash_bytes(buf: &[u8]) -> HashBytes {
|
pub fn sha1_hash(buf: &[u8]) -> HashBytes { HashBytes::from(Sha1::digest(buf)) }
|
||||||
let mut hasher = Sha1::new();
|
|
||||||
hasher.update(buf);
|
|
||||||
hasher.finalize().into()
|
|
||||||
}
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub(crate) mod wii;
|
||||||
|
|
||||||
pub use fst::{Fst, Node, NodeKind};
|
pub use fst::{Fst, Node, NodeKind};
|
||||||
pub use streams::{FileStream, OwnedFileStream, WindowedStream};
|
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)
|
/// Size in bytes of a disc sector. (32 KiB)
|
||||||
pub const SECTOR_SIZE: usize = 0x8000;
|
pub const SECTOR_SIZE: usize = 0x8000;
|
||||||
|
@ -90,6 +90,14 @@ impl DiscHeader {
|
||||||
/// Whether this is a Wii disc.
|
/// Whether this is a Wii disc.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_wii(&self) -> bool { self.wii_magic == WII_MAGIC }
|
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.
|
/// A header describing the contents of a disc partition.
|
||||||
|
@ -379,19 +387,23 @@ impl PartitionMeta {
|
||||||
/// A view into the disc header.
|
/// A view into the disc header.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn header(&self) -> &DiscHeader {
|
pub fn header(&self) -> &DiscHeader {
|
||||||
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()]).unwrap()
|
DiscHeader::ref_from_bytes(&self.raw_boot[..size_of::<DiscHeader>()])
|
||||||
|
.expect("Invalid header alignment")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A view into the partition header.
|
/// A view into the partition header.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn partition_header(&self) -> &PartitionHeader {
|
pub fn partition_header(&self) -> &PartitionHeader {
|
||||||
PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::<DiscHeader>()..]).unwrap()
|
PartitionHeader::ref_from_bytes(&self.raw_boot[size_of::<DiscHeader>()..])
|
||||||
|
.expect("Invalid partition header alignment")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A view into the apploader header.
|
/// A view into the apploader header.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn apploader_header(&self) -> &ApploaderHeader {
|
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).
|
/// A view into the file system table (FST).
|
||||||
|
@ -400,18 +412,29 @@ impl PartitionMeta {
|
||||||
|
|
||||||
/// A view into the DOL header.
|
/// A view into the DOL header.
|
||||||
#[inline]
|
#[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)
|
/// A view into the ticket. (Wii only)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn ticket(&self) -> Option<&Ticket> {
|
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)
|
/// A view into the TMD. (Wii only)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn tmd_header(&self) -> Option<&TmdHeader> {
|
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::<TmdHeader>()..];
|
||||||
|
Some(<[ContentMetadata]>::ref_from_bytes(raw_cmd).expect("Invalid CMD alignment"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
||||||
io::{BufRead, Read, Seek, SeekFrom},
|
io::{BufRead, Read, Seek, SeekFrom},
|
||||||
};
|
};
|
||||||
|
|
||||||
use zerocopy::FromZeros;
|
use zerocopy::{FromBytes, FromZeros};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
gcn::PartitionGC,
|
gcn::PartitionGC,
|
||||||
|
@ -16,15 +16,10 @@ use crate::{
|
||||||
disc::wii::REGION_OFFSET,
|
disc::wii::REGION_OFFSET,
|
||||||
io::block::{Block, BlockIO, PartitionInfo},
|
io::block::{Block, BlockIO, PartitionInfo},
|
||||||
util::read::{read_box, read_from, read_vec},
|
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 {
|
pub struct DiscReader {
|
||||||
io: Box<dyn BlockIO>,
|
io: Box<dyn BlockIO>,
|
||||||
block: Block,
|
block: Block,
|
||||||
|
@ -33,7 +28,7 @@ pub struct DiscReader {
|
||||||
sector_buf: Box<[u8; SECTOR_SIZE]>,
|
sector_buf: Box<[u8; SECTOR_SIZE]>,
|
||||||
sector_idx: u32,
|
sector_idx: u32,
|
||||||
pos: u64,
|
pos: u64,
|
||||||
mode: EncryptionMode,
|
mode: PartitionEncryptionMode,
|
||||||
disc_header: Box<DiscHeader>,
|
disc_header: Box<DiscHeader>,
|
||||||
pub(crate) partitions: Vec<PartitionInfo>,
|
pub(crate) partitions: Vec<PartitionInfo>,
|
||||||
hash_tables: Vec<HashTable>,
|
hash_tables: Vec<HashTable>,
|
||||||
|
@ -71,11 +66,7 @@ impl DiscReader {
|
||||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
|
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
|
||||||
sector_idx: u32::MAX,
|
sector_idx: u32::MAX,
|
||||||
pos: 0,
|
pos: 0,
|
||||||
mode: if options.rebuild_encryption {
|
mode: options.partition_encryption,
|
||||||
EncryptionMode::Encrypted
|
|
||||||
} else {
|
|
||||||
EncryptionMode::Decrypted
|
|
||||||
},
|
|
||||||
disc_header: DiscHeader::new_box_zeroed()?,
|
disc_header: DiscHeader::new_box_zeroed()?,
|
||||||
partitions: vec![],
|
partitions: vec![],
|
||||||
hash_tables: vec![],
|
hash_tables: vec![],
|
||||||
|
@ -84,11 +75,28 @@ impl DiscReader {
|
||||||
let disc_header: Box<DiscHeader> = read_box(&mut reader).context("Reading disc header")?;
|
let disc_header: Box<DiscHeader> = read_box(&mut reader).context("Reading disc header")?;
|
||||||
reader.disc_header = disc_header;
|
reader.disc_header = disc_header;
|
||||||
if reader.disc_header.is_wii() {
|
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.seek(SeekFrom::Start(REGION_OFFSET)).context("Seeking to region info")?;
|
||||||
reader.region = Some(read_from(&mut reader).context("Reading region info")?);
|
reader.region = Some(read_from(&mut reader).context("Reading region info")?);
|
||||||
reader.partitions = read_partition_info(&mut reader)?;
|
reader.partitions = read_partition_info(&mut reader)?;
|
||||||
// Rebuild hashes if the format requires it
|
// 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)?;
|
rebuild_hashes(&mut reader)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +133,7 @@ impl DiscReader {
|
||||||
pub fn open_partition(
|
pub fn open_partition(
|
||||||
&self,
|
&self,
|
||||||
index: usize,
|
index: usize,
|
||||||
options: &OpenOptions,
|
options: &PartitionOptions,
|
||||||
) -> Result<Box<dyn PartitionBase>> {
|
) -> Result<Box<dyn PartitionBase>> {
|
||||||
if self.disc_header.is_gamecube() {
|
if self.disc_header.is_gamecube() {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
|
@ -145,7 +153,7 @@ impl DiscReader {
|
||||||
pub fn open_partition_kind(
|
pub fn open_partition_kind(
|
||||||
&self,
|
&self,
|
||||||
kind: PartitionKind,
|
kind: PartitionKind,
|
||||||
options: &OpenOptions,
|
options: &PartitionOptions,
|
||||||
) -> Result<Box<dyn PartitionBase>> {
|
) -> Result<Box<dyn PartitionBase>> {
|
||||||
if self.disc_header.is_gamecube() {
|
if self.disc_header.is_gamecube() {
|
||||||
if kind == PartitionKind::Data {
|
if kind == PartitionKind::Data {
|
||||||
|
@ -182,22 +190,25 @@ impl BufRead for DiscReader {
|
||||||
|
|
||||||
// Read new sector into buffer
|
// Read new sector into buffer
|
||||||
if abs_sector != self.sector_idx {
|
if abs_sector != self.sector_idx {
|
||||||
if let Some(partition) = partition {
|
match (self.mode, partition, self.disc_header.has_partition_encryption()) {
|
||||||
match self.mode {
|
(PartitionEncryptionMode::Original, Some(partition), true)
|
||||||
EncryptionMode::Decrypted => self.block.decrypt(
|
| (PartitionEncryptionMode::ForceEncrypted, Some(partition), _) => {
|
||||||
|
self.block.encrypt(
|
||||||
self.sector_buf.as_mut(),
|
self.sector_buf.as_mut(),
|
||||||
self.block_buf.as_ref(),
|
self.block_buf.as_ref(),
|
||||||
abs_sector,
|
abs_sector,
|
||||||
partition,
|
partition,
|
||||||
)?,
|
)?;
|
||||||
EncryptionMode::Encrypted => self.block.encrypt(
|
|
||||||
self.sector_buf.as_mut(),
|
|
||||||
self.block_buf.as_ref(),
|
|
||||||
abs_sector,
|
|
||||||
partition,
|
|
||||||
)?,
|
|
||||||
}
|
}
|
||||||
} else {
|
(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.block.copy_raw(
|
||||||
self.sector_buf.as_mut(),
|
self.sector_buf.as_mut(),
|
||||||
self.block_buf.as_ref(),
|
self.block_buf.as_ref(),
|
||||||
|
@ -205,7 +216,25 @@ impl BufRead for DiscReader {
|
||||||
&self.disc_header,
|
&self.disc_header,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.sector_idx = abs_sector;
|
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
|
// Read from sector buffer
|
||||||
|
@ -273,8 +302,19 @@ fn read_partition_info(reader: &mut DiscReader) -> Result<Vec<PartitionInfo>> {
|
||||||
"Partition {group_idx}:{part_idx} offset is not sector aligned",
|
"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_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
|
if data_start_offset % SECTOR_SIZE as u64 != 0
|
||||||
|| data_end_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<Vec<PartitionInfo>> {
|
||||||
disc_header: DiscHeader::new_box_zeroed()?,
|
disc_header: DiscHeader::new_box_zeroed()?,
|
||||||
partition_header: PartitionHeader::new_box_zeroed()?,
|
partition_header: PartitionHeader::new_box_zeroed()?,
|
||||||
hash_table: None,
|
hash_table: None,
|
||||||
|
has_encryption: disc_header.has_partition_encryption(),
|
||||||
|
has_hashes: disc_header.has_partition_hashes(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut partition_reader = PartitionWii::new(
|
let mut partition_reader = PartitionWii::new(
|
||||||
reader.io.clone(),
|
reader.io.clone(),
|
||||||
reader.disc_header.clone(),
|
reader.disc_header.clone(),
|
||||||
&info,
|
&info,
|
||||||
&OpenOptions::default(),
|
&PartitionOptions { validate_hashes: false },
|
||||||
)?;
|
)?;
|
||||||
info.disc_header = read_box(&mut partition_reader).context("Reading disc header")?;
|
info.disc_header = read_box(&mut partition_reader).context("Reading disc header")?;
|
||||||
info.partition_header =
|
info.partition_header =
|
||||||
|
|
|
@ -16,13 +16,13 @@ use crate::{
|
||||||
array_ref,
|
array_ref,
|
||||||
disc::streams::OwnedFileStream,
|
disc::streams::OwnedFileStream,
|
||||||
io::{
|
io::{
|
||||||
aes_decrypt,
|
aes_cbc_decrypt,
|
||||||
block::{Block, BlockIO, PartitionInfo},
|
block::{Block, BlockIO, PartitionInfo},
|
||||||
KeyBytes,
|
HashBytes, KeyBytes,
|
||||||
},
|
},
|
||||||
static_assert,
|
static_assert,
|
||||||
util::{div_rem, read::read_box_slice},
|
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
|
/// 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),
|
format!("unknown common key index {}", self.common_key_idx),
|
||||||
))?;
|
))?;
|
||||||
let mut title_key = self.title_key;
|
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)
|
Ok(title_key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,6 +231,24 @@ pub struct TmdHeader {
|
||||||
|
|
||||||
static_assert!(size_of::<TmdHeader>() == 0x1E4);
|
static_assert!(size_of::<TmdHeader>() == 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::<ContentMetadata>() == 0x24);
|
||||||
|
|
||||||
pub const H3_TABLE_SIZE: usize = 0x18000;
|
pub const H3_TABLE_SIZE: usize = 0x18000;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
#[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||||
|
@ -275,10 +293,10 @@ pub struct PartitionWii {
|
||||||
sector_buf: Box<[u8; SECTOR_SIZE]>,
|
sector_buf: Box<[u8; SECTOR_SIZE]>,
|
||||||
sector: u32,
|
sector: u32,
|
||||||
pos: u64,
|
pos: u64,
|
||||||
verify: bool,
|
options: PartitionOptions,
|
||||||
raw_tmd: Box<[u8]>,
|
raw_tmd: Option<Box<[u8]>>,
|
||||||
raw_cert_chain: Box<[u8]>,
|
raw_cert_chain: Option<Box<[u8]>>,
|
||||||
raw_h3_table: Box<[u8]>,
|
raw_h3_table: Option<Box<[u8]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for PartitionWii {
|
impl Clone for PartitionWii {
|
||||||
|
@ -292,7 +310,7 @@ impl Clone for PartitionWii {
|
||||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
|
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed().unwrap(),
|
||||||
sector: u32::MAX,
|
sector: u32::MAX,
|
||||||
pos: 0,
|
pos: 0,
|
||||||
verify: self.verify,
|
options: self.options.clone(),
|
||||||
raw_tmd: self.raw_tmd.clone(),
|
raw_tmd: self.raw_tmd.clone(),
|
||||||
raw_cert_chain: self.raw_cert_chain.clone(),
|
raw_cert_chain: self.raw_cert_chain.clone(),
|
||||||
raw_h3_table: self.raw_h3_table.clone(),
|
raw_h3_table: self.raw_h3_table.clone(),
|
||||||
|
@ -305,29 +323,43 @@ impl PartitionWii {
|
||||||
inner: Box<dyn BlockIO>,
|
inner: Box<dyn BlockIO>,
|
||||||
disc_header: Box<DiscHeader>,
|
disc_header: Box<DiscHeader>,
|
||||||
partition: &PartitionInfo,
|
partition: &PartitionInfo,
|
||||||
options: &OpenOptions,
|
options: &PartitionOptions,
|
||||||
) -> Result<Box<Self>> {
|
) -> Result<Box<Self>> {
|
||||||
let block_size = inner.block_size();
|
let block_size = inner.block_size();
|
||||||
let mut reader = PartitionGC::new(inner, disc_header)?;
|
let mut reader = PartitionGC::new(inner, disc_header)?;
|
||||||
|
|
||||||
// Read TMD, cert chain, and H3 table
|
// Read TMD, cert chain, and H3 table
|
||||||
let offset = partition.start_sector as u64 * SECTOR_SIZE as u64;
|
let offset = partition.start_sector as u64 * SECTOR_SIZE as u64;
|
||||||
|
let raw_tmd = if partition.header.tmd_size() != 0 {
|
||||||
reader
|
reader
|
||||||
.seek(SeekFrom::Start(offset + partition.header.tmd_off()))
|
.seek(SeekFrom::Start(offset + partition.header.tmd_off()))
|
||||||
.context("Seeking to TMD offset")?;
|
.context("Seeking to TMD offset")?;
|
||||||
let raw_tmd: Box<[u8]> = read_box_slice(&mut reader, partition.header.tmd_size() as usize)
|
Some(
|
||||||
.context("Reading TMD")?;
|
read_box_slice::<u8, _>(&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
|
reader
|
||||||
.seek(SeekFrom::Start(offset + partition.header.cert_chain_off()))
|
.seek(SeekFrom::Start(offset + partition.header.cert_chain_off()))
|
||||||
.context("Seeking to cert chain offset")?;
|
.context("Seeking to cert chain offset")?;
|
||||||
let raw_cert_chain: Box<[u8]> =
|
Some(
|
||||||
read_box_slice(&mut reader, partition.header.cert_chain_size() as usize)
|
read_box_slice::<u8, _>(&mut reader, partition.header.cert_chain_size() as usize)
|
||||||
.context("Reading cert chain")?;
|
.context("Reading cert chain")?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let raw_h3_table = if partition.has_hashes {
|
||||||
reader
|
reader
|
||||||
.seek(SeekFrom::Start(offset + partition.header.h3_table_off()))
|
.seek(SeekFrom::Start(offset + partition.header.h3_table_off()))
|
||||||
.context("Seeking to H3 table offset")?;
|
.context("Seeking to H3 table offset")?;
|
||||||
let raw_h3_table: Box<[u8]> =
|
Some(read_box_slice::<u8, _>(&mut reader, H3_TABLE_SIZE).context("Reading H3 table")?)
|
||||||
read_box_slice(&mut reader, H3_TABLE_SIZE).context("Reading H3 table")?;
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Box::new(Self {
|
Ok(Box::new(Self {
|
||||||
io: reader.into_inner(),
|
io: reader.into_inner(),
|
||||||
|
@ -338,7 +370,7 @@ impl PartitionWii {
|
||||||
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
|
sector_buf: <[u8; SECTOR_SIZE]>::new_box_zeroed()?,
|
||||||
sector: u32::MAX,
|
sector: u32::MAX,
|
||||||
pos: 0,
|
pos: 0,
|
||||||
verify: options.validate_hashes,
|
options: options.clone(),
|
||||||
raw_tmd,
|
raw_tmd,
|
||||||
raw_cert_chain,
|
raw_cert_chain,
|
||||||
raw_h3_table,
|
raw_h3_table,
|
||||||
|
@ -348,37 +380,60 @@ impl PartitionWii {
|
||||||
|
|
||||||
impl BufRead for PartitionWii {
|
impl BufRead for PartitionWii {
|
||||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
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;
|
let abs_sector = self.partition.data_start_sector + part_sector;
|
||||||
if abs_sector >= self.partition.data_end_sector {
|
if abs_sector >= self.partition.data_end_sector {
|
||||||
return Ok(&[]);
|
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
|
// 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 {
|
if block_idx != self.block_idx {
|
||||||
self.block =
|
self.block = self.io.read_block(
|
||||||
self.io.read_block(self.block_buf.as_mut(), block_idx, Some(&self.partition))?;
|
self.block_buf.as_mut(),
|
||||||
|
block_idx,
|
||||||
|
self.partition.has_encryption.then_some(&self.partition),
|
||||||
|
)?;
|
||||||
self.block_idx = block_idx;
|
self.block_idx = block_idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt sector if necessary
|
// Decrypt sector if necessary
|
||||||
if abs_sector != self.sector {
|
if abs_sector != self.sector {
|
||||||
|
if self.partition.has_encryption {
|
||||||
self.block.decrypt(
|
self.block.decrypt(
|
||||||
self.sector_buf.as_mut(),
|
self.sector_buf.as_mut(),
|
||||||
self.block_buf.as_ref(),
|
self.block_buf.as_ref(),
|
||||||
abs_sector,
|
abs_sector,
|
||||||
&self.partition,
|
&self.partition,
|
||||||
)?;
|
)?;
|
||||||
if self.verify {
|
} else {
|
||||||
verify_hashes(self.sector_buf.as_ref(), part_sector, self.raw_h3_table.as_ref())?;
|
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;
|
self.sector = abs_sector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.partition.has_hashes {
|
||||||
let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize;
|
let offset = (self.pos % SECTOR_DATA_SIZE as u64) as usize;
|
||||||
Ok(&self.sector_buf[HASHES_SIZE + offset..])
|
Ok(&self.sector_buf[HASHES_SIZE + offset..])
|
||||||
|
} else {
|
||||||
|
let offset = (self.pos % SECTOR_SIZE as u64) as usize;
|
||||||
|
Ok(&self.sector_buf[offset..])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -489,9 +544,9 @@ impl PartitionBase for PartitionWii {
|
||||||
self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?;
|
self.seek(SeekFrom::Start(0)).context("Seeking to partition header")?;
|
||||||
let mut meta = read_part_meta(self, true)?;
|
let mut meta = read_part_meta(self, true)?;
|
||||||
meta.raw_ticket = Some(Box::from(self.partition.header.ticket.as_bytes()));
|
meta.raw_ticket = Some(Box::from(self.partition.header.ticket.as_bytes()));
|
||||||
meta.raw_tmd = Some(self.raw_tmd.clone());
|
meta.raw_tmd = self.raw_tmd.clone();
|
||||||
meta.raw_cert_chain = Some(self.raw_cert_chain.clone());
|
meta.raw_cert_chain = self.raw_cert_chain.clone();
|
||||||
meta.raw_h3_table = Some(self.raw_h3_table.clone());
|
meta.raw_h3_table = self.raw_h3_table.clone();
|
||||||
Ok(meta)
|
Ok(meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@ use crate::{
|
||||||
DiscHeader, PartitionHeader, PartitionKind, GCN_MAGIC, SECTOR_SIZE, WII_MAGIC,
|
DiscHeader, PartitionHeader, PartitionKind, GCN_MAGIC, SECTOR_SIZE, WII_MAGIC,
|
||||||
},
|
},
|
||||||
io::{
|
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},
|
util::{lfg::LaggedFibonacci, read::read_from},
|
||||||
Error, Result, ResultContext,
|
Error, Result, ResultContext,
|
||||||
|
@ -218,9 +219,9 @@ pub struct PartitionInfo {
|
||||||
pub kind: PartitionKind,
|
pub kind: PartitionKind,
|
||||||
/// The start sector of the partition.
|
/// The start sector of the partition.
|
||||||
pub start_sector: u32,
|
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,
|
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,
|
pub data_end_sector: u32,
|
||||||
/// The AES key for the partition, also known as the "title key".
|
/// The AES key for the partition, also known as the "title key".
|
||||||
pub key: KeyBytes,
|
pub key: KeyBytes,
|
||||||
|
@ -232,6 +233,10 @@ pub struct PartitionInfo {
|
||||||
pub partition_header: Box<PartitionHeader>,
|
pub partition_header: Box<PartitionHeader>,
|
||||||
/// The hash table for the partition, if rebuilt.
|
/// The hash table for the partition, if rebuilt.
|
||||||
pub hash_table: Option<HashTable>,
|
pub hash_table: Option<HashTable>,
|
||||||
|
/// 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`].
|
/// The block kind returned by [`BlockIO::read_block`].
|
||||||
|
@ -239,6 +244,8 @@ pub struct PartitionInfo {
|
||||||
pub enum Block {
|
pub enum Block {
|
||||||
/// Raw data or encrypted Wii partition data
|
/// Raw data or encrypted Wii partition data
|
||||||
Raw,
|
Raw,
|
||||||
|
/// Encrypted Wii partition data
|
||||||
|
PartEncrypted,
|
||||||
/// Decrypted Wii partition data
|
/// Decrypted Wii partition data
|
||||||
PartDecrypted {
|
PartDecrypted {
|
||||||
/// Whether the sector has its hash block intact
|
/// Whether the sector has its hash block intact
|
||||||
|
@ -264,6 +271,9 @@ impl Block {
|
||||||
match self {
|
match self {
|
||||||
Block::Raw => {
|
Block::Raw => {
|
||||||
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
||||||
|
}
|
||||||
|
Block::PartEncrypted => {
|
||||||
|
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
||||||
decrypt_sector(out, partition);
|
decrypt_sector(out, partition);
|
||||||
}
|
}
|
||||||
Block::PartDecrypted { has_hashes } => {
|
Block::PartDecrypted { has_hashes } => {
|
||||||
|
@ -296,6 +306,10 @@ impl Block {
|
||||||
match self {
|
match self {
|
||||||
Block::Raw => {
|
Block::Raw => {
|
||||||
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
||||||
|
encrypt_sector(out, partition);
|
||||||
|
}
|
||||||
|
Block::PartEncrypted => {
|
||||||
|
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
||||||
}
|
}
|
||||||
Block::PartDecrypted { has_hashes } => {
|
Block::PartDecrypted { has_hashes } => {
|
||||||
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
||||||
|
@ -327,15 +341,9 @@ impl Block {
|
||||||
disc_header: &DiscHeader,
|
disc_header: &DiscHeader,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Block::Raw => {
|
Block::Raw | Block::PartEncrypted | Block::PartDecrypted { .. } => {
|
||||||
out.copy_from_slice(block_sector::<SECTOR_SIZE>(data, abs_sector)?);
|
out.copy_from_slice(block_sector::<SECTOR_SIZE>(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::Junk => generate_junk(out, abs_sector, None, disc_header),
|
||||||
Block::Zero => out.fill(0),
|
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) {
|
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
|
// Data IV from encrypted hash block
|
||||||
let iv = *array_ref![out, 0x3D0, 16];
|
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) {
|
fn decrypt_sector(out: &mut [u8; SECTOR_SIZE], partition: &PartitionInfo) {
|
||||||
// Data IV from encrypted hash block
|
// Data IV from encrypted hash block
|
||||||
let iv = *array_ref![out, 0x3D0, 16];
|
let iv = *array_ref![out, 0x3D0, 16];
|
||||||
aes_decrypt(&partition.key, [0u8; 16], &mut out[..HASHES_SIZE]);
|
aes_cbc_decrypt(&partition.key, &[0u8; 16], &mut out[..HASHES_SIZE]);
|
||||||
aes_decrypt(&partition.key, iv, &mut out[HASHES_SIZE..]);
|
aes_cbc_decrypt(&partition.key, &iv, &mut out[HASHES_SIZE..]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ impl BlockIO for DiscIOCISO {
|
||||||
&mut self,
|
&mut self,
|
||||||
out: &mut [u8],
|
out: &mut [u8],
|
||||||
block: u32,
|
block: u32,
|
||||||
_partition: Option<&PartitionInfo>,
|
partition: Option<&PartitionInfo>,
|
||||||
) -> io::Result<Block> {
|
) -> io::Result<Block> {
|
||||||
if block >= CISO_MAP_SIZE as u32 {
|
if block >= CISO_MAP_SIZE as u32 {
|
||||||
// Out of bounds
|
// Out of bounds
|
||||||
|
@ -109,7 +109,11 @@ impl BlockIO for DiscIOCISO {
|
||||||
+ phys_block as u64 * self.header.block_size.get() as u64;
|
+ phys_block as u64 * self.header.block_size.get() as u64;
|
||||||
self.inner.seek(SeekFrom::Start(file_offset))?;
|
self.inner.seek(SeekFrom::Start(file_offset))?;
|
||||||
self.inner.read_exact(out)?;
|
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() }
|
fn block_size_internal(&self) -> u32 { self.header.block_size.get() }
|
||||||
|
|
|
@ -83,7 +83,7 @@ impl BlockIO for DiscIOGCZ {
|
||||||
&mut self,
|
&mut self,
|
||||||
out: &mut [u8],
|
out: &mut [u8],
|
||||||
block: u32,
|
block: u32,
|
||||||
_partition: Option<&PartitionInfo>,
|
partition: Option<&PartitionInfo>,
|
||||||
) -> io::Result<Block> {
|
) -> io::Result<Block> {
|
||||||
if block >= self.header.block_count.get() {
|
if block >= self.header.block_count.get() {
|
||||||
// Out of bounds
|
// Out of bounds
|
||||||
|
@ -166,7 +166,11 @@ impl BlockIO for DiscIOGCZ {
|
||||||
// Copy uncompressed block
|
// Copy uncompressed block
|
||||||
out.copy_from_slice(self.block_buf.as_slice());
|
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() }
|
fn block_size_internal(&self) -> u32 { self.header.block_size.get() }
|
||||||
|
|
|
@ -31,7 +31,7 @@ impl BlockIO for DiscIOISO {
|
||||||
&mut self,
|
&mut self,
|
||||||
out: &mut [u8],
|
out: &mut [u8],
|
||||||
block: u32,
|
block: u32,
|
||||||
_partition: Option<&PartitionInfo>,
|
partition: Option<&PartitionInfo>,
|
||||||
) -> io::Result<Block> {
|
) -> io::Result<Block> {
|
||||||
let offset = block as u64 * SECTOR_SIZE as u64;
|
let offset = block as u64 * SECTOR_SIZE as u64;
|
||||||
if offset >= self.stream_len {
|
if offset >= self.stream_len {
|
||||||
|
@ -48,7 +48,11 @@ impl BlockIO for DiscIOISO {
|
||||||
} else {
|
} else {
|
||||||
self.inner.read_exact(out)?;
|
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 }
|
fn block_size_internal(&self) -> u32 { SECTOR_SIZE as u32 }
|
||||||
|
|
|
@ -124,19 +124,19 @@ pub struct DiscMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypts data in-place using AES-128-CBC with the given key and IV.
|
/// Encrypts data in-place using AES-128-CBC with the given key and IV.
|
||||||
#[inline(always)]
|
/// Requires the data length to be a multiple of the AES block size (16 bytes).
|
||||||
pub(crate) fn aes_encrypt(key: &KeyBytes, iv: KeyBytes, data: &mut [u8]) {
|
pub fn aes_cbc_encrypt(key: &KeyBytes, iv: &KeyBytes, data: &mut [u8]) {
|
||||||
use aes::cipher::{block_padding::NoPadding, BlockEncryptMut, KeyIvInit};
|
use aes::cipher::{block_padding::NoPadding, BlockEncryptMut, KeyIvInit};
|
||||||
<cbc::Encryptor<aes::Aes128>>::new(key.into(), &aes::Block::from(iv))
|
<cbc::Encryptor<aes::Aes128>>::new(key.into(), iv.into())
|
||||||
.encrypt_padded_mut::<NoPadding>(data, data.len())
|
.encrypt_padded_mut::<NoPadding>(data, data.len())
|
||||||
.unwrap(); // Safe: using NoPadding
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypts data in-place using AES-128-CBC with the given key and IV.
|
/// Decrypts data in-place using AES-128-CBC with the given key and IV.
|
||||||
#[inline(always)]
|
/// Requires the data length to be a multiple of the AES block size (16 bytes).
|
||||||
pub(crate) fn aes_decrypt(key: &KeyBytes, iv: KeyBytes, data: &mut [u8]) {
|
pub fn aes_cbc_decrypt(key: &KeyBytes, iv: &KeyBytes, data: &mut [u8]) {
|
||||||
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit};
|
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit};
|
||||||
<cbc::Decryptor<aes::Aes128>>::new(key.into(), &aes::Block::from(iv))
|
<cbc::Decryptor<aes::Aes128>>::new(key.into(), iv.into())
|
||||||
.decrypt_padded_mut::<NoPadding>(data)
|
.decrypt_padded_mut::<NoPadding>(data)
|
||||||
.unwrap(); // Safe: using NoPadding
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,10 @@ use std::{
|
||||||
use zerocopy::{big_endian::U32, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
use zerocopy::{big_endian::U32, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
array_ref_mut,
|
||||||
disc::SECTOR_SIZE,
|
disc::SECTOR_SIZE,
|
||||||
io::{
|
io::{
|
||||||
aes_decrypt,
|
aes_cbc_decrypt,
|
||||||
block::{Block, BlockIO, PartitionInfo, NFS_MAGIC},
|
block::{Block, BlockIO, PartitionInfo, NFS_MAGIC},
|
||||||
split::SplitFileReader,
|
split::SplitFileReader,
|
||||||
Format, KeyBytes, MagicBytes,
|
Format, KeyBytes, MagicBytes,
|
||||||
|
@ -125,18 +126,15 @@ impl BlockIO for DiscIONFS {
|
||||||
self.inner.read_exact(out)?;
|
self.inner.read_exact(out)?;
|
||||||
|
|
||||||
// Decrypt
|
// Decrypt
|
||||||
let iv_bytes = sector.to_be_bytes();
|
let mut iv = [0u8; 0x10];
|
||||||
#[rustfmt::skip]
|
*array_ref_mut!(iv, 12, 4) = sector.to_be_bytes();
|
||||||
let iv: KeyBytes = [
|
aes_cbc_decrypt(&self.key, &iv, out);
|
||||||
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);
|
|
||||||
|
|
||||||
if partition.is_some() {
|
match partition {
|
||||||
|
Some(partition) if partition.has_encryption => {
|
||||||
Ok(Block::PartDecrypted { has_hashes: true })
|
Ok(Block::PartDecrypted { has_hashes: true })
|
||||||
} else {
|
}
|
||||||
Ok(Block::Raw)
|
_ => Ok(Block::Raw),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ impl BlockIO for DiscIOTGC {
|
||||||
&mut self,
|
&mut self,
|
||||||
out: &mut [u8],
|
out: &mut [u8],
|
||||||
block: u32,
|
block: u32,
|
||||||
_partition: Option<&PartitionInfo>,
|
partition: Option<&PartitionInfo>,
|
||||||
) -> io::Result<Block> {
|
) -> io::Result<Block> {
|
||||||
let offset = self.header.header_offset.get() as u64 + block as u64 * SECTOR_SIZE as u64;
|
let offset = self.header.header_offset.get() as u64 + block as u64 * SECTOR_SIZE as u64;
|
||||||
if offset >= self.stream_len {
|
if offset >= self.stream_len {
|
||||||
|
@ -137,7 +137,10 @@ impl BlockIO for DiscIOTGC {
|
||||||
.copy_from_slice(&self.fst[fst_offset..fst_offset + copy_len]);
|
.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 }
|
fn block_size_internal(&self) -> u32 { SECTOR_SIZE as u32 }
|
||||||
|
|
|
@ -97,7 +97,7 @@ impl BlockIO for DiscIOWBFS {
|
||||||
&mut self,
|
&mut self,
|
||||||
out: &mut [u8],
|
out: &mut [u8],
|
||||||
block: u32,
|
block: u32,
|
||||||
_partition: Option<&PartitionInfo>,
|
partition: Option<&PartitionInfo>,
|
||||||
) -> io::Result<Block> {
|
) -> io::Result<Block> {
|
||||||
let block_size = self.header.block_size();
|
let block_size = self.header.block_size();
|
||||||
if block >= self.header.max_blocks() {
|
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;
|
let block_start = block_size as u64 * phys_block as u64;
|
||||||
self.inner.seek(SeekFrom::Start(block_start))?;
|
self.inner.seek(SeekFrom::Start(block_start))?;
|
||||||
self.inner.read_exact(out)?;
|
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() }
|
fn block_size_internal(&self) -> u32 { self.header.block_size() }
|
||||||
|
|
|
@ -8,7 +8,7 @@ use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
disc::{
|
disc::{
|
||||||
hashes::hash_bytes,
|
hashes::sha1_hash,
|
||||||
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
|
wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
|
||||||
SECTOR_SIZE,
|
SECTOR_SIZE,
|
||||||
},
|
},
|
||||||
|
@ -530,7 +530,7 @@ impl Clone for DiscIOWIA {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_hash(buf: &[u8], expected: &HashBytes) -> Result<()> {
|
fn verify_hash(buf: &[u8], expected: &HashBytes) -> Result<()> {
|
||||||
let out = hash_bytes(buf);
|
let out = sha1_hash(buf);
|
||||||
if out != *expected {
|
if out != *expected {
|
||||||
let mut got_bytes = [0u8; 40];
|
let mut got_bytes = [0u8; 40];
|
||||||
let got = base16ct::lower::encode_str(&out, &mut got_bytes).unwrap(); // Safe: fixed buffer size
|
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 chunk_size = self.disc.chunk_size.get();
|
||||||
let sectors_per_chunk = chunk_size / SECTOR_SIZE as u32;
|
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
|
// Find the partition
|
||||||
let Some(wia_part) = self.partitions.get(partition.index) else {
|
let Some(wia_part) = self.partitions.get(partition.index) else {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
|
@ -783,7 +786,7 @@ impl BlockIO for DiscIOWIA {
|
||||||
|
|
||||||
// Read group data if necessary
|
// Read group data if necessary
|
||||||
if group_index != self.group {
|
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
|
// Within a partition, hashes are excluded from the data size
|
||||||
(sectors_per_chunk * SECTOR_DATA_SIZE as u32) as usize
|
(sectors_per_chunk * SECTOR_DATA_SIZE as u32) as usize
|
||||||
} else {
|
} else {
|
||||||
|
@ -798,11 +801,8 @@ impl BlockIO for DiscIOWIA {
|
||||||
matches!(self.disc.compression(), WIACompression::None | WIACompression::Purge)
|
matches!(self.disc.compression(), WIACompression::None | WIACompression::Purge)
|
||||||
|| !group.is_compressed();
|
|| !group.is_compressed();
|
||||||
if uncompressed_exception_lists {
|
if uncompressed_exception_lists {
|
||||||
self.exception_lists = read_exception_lists(
|
self.exception_lists =
|
||||||
&mut reader,
|
read_exception_lists(&mut reader, in_partition, self.disc.chunk_size.get())?;
|
||||||
partition.is_some(),
|
|
||||||
self.disc.chunk_size.get(),
|
|
||||||
)?;
|
|
||||||
// Align to 4
|
// Align to 4
|
||||||
let rem = reader.stream_position()? % 4;
|
let rem = reader.stream_position()? % 4;
|
||||||
if rem != 0 {
|
if rem != 0 {
|
||||||
|
@ -817,7 +817,7 @@ impl BlockIO for DiscIOWIA {
|
||||||
if !uncompressed_exception_lists {
|
if !uncompressed_exception_lists {
|
||||||
self.exception_lists = read_exception_lists(
|
self.exception_lists = read_exception_lists(
|
||||||
reader.as_mut(),
|
reader.as_mut(),
|
||||||
partition.is_some(),
|
in_partition,
|
||||||
self.disc.chunk_size.get(),
|
self.disc.chunk_size.get(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
@ -861,7 +861,7 @@ impl BlockIO for DiscIOWIA {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read sector from cached group data
|
// Read sector from cached group data
|
||||||
if partition.is_some() {
|
if in_partition {
|
||||||
let sector_data_start = group_sector as usize * SECTOR_DATA_SIZE;
|
let sector_data_start = group_sector as usize * SECTOR_DATA_SIZE;
|
||||||
out[..HASHES_SIZE].fill(0);
|
out[..HASHES_SIZE].fill(0);
|
||||||
out[HASHES_SIZE..SECTOR_SIZE].copy_from_slice(
|
out[HASHES_SIZE..SECTOR_SIZE].copy_from_slice(
|
||||||
|
|
|
@ -46,8 +46,8 @@
|
||||||
//! Converting a disc image to raw ISO:
|
//! Converting a disc image to raw ISO:
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! // Enable `rebuild_encryption` to ensure the output is a valid ISO.
|
//! // Enable `PartitionEncryptionMode::Original` to ensure the output is a valid ISO.
|
||||||
//! let options = nod::OpenOptions { rebuild_encryption: true, ..Default::default() };
|
//! let options = nod::OpenOptions { partition_encryption: nod::PartitionEncryptionMode::Original };
|
||||||
//! let mut disc = nod::Disc::new_with_options("path/to/file.rvz", &options)
|
//! let mut disc = nod::Disc::new_with_options("path/to/file.rvz", &options)
|
||||||
//! .expect("Failed to open disc");
|
//! .expect("Failed to open disc");
|
||||||
//!
|
//!
|
||||||
|
@ -64,9 +64,9 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use disc::{
|
pub use disc::{
|
||||||
ApploaderHeader, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind, OwnedFileStream,
|
ApploaderHeader, ContentMetadata, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind,
|
||||||
PartitionBase, PartitionHeader, PartitionKind, PartitionMeta, SignedHeader, Ticket,
|
OwnedFileStream, PartitionBase, PartitionHeader, PartitionKind, PartitionMeta, SignedHeader,
|
||||||
TicketLimit, TmdHeader, WindowedStream, BI2_SIZE, BOOT_SIZE, DL_DVD_SIZE, GCN_MAGIC,
|
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,
|
MINI_DVD_SIZE, REGION_SIZE, SECTOR_SIZE, SL_DVD_SIZE, WII_MAGIC,
|
||||||
};
|
};
|
||||||
pub use io::{
|
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.
|
/// Options for opening a disc image.
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct OpenOptions {
|
pub struct OpenOptions {
|
||||||
/// Wii: Rebuild partition data encryption and hashes if the underlying format stores data
|
/// Wii: Partition encryption mode. By default, partitions are read as they
|
||||||
/// decrypted or with hashes removed. (e.g. WIA/RVZ, NFS)
|
/// are stored in the underlying disc format, avoiding extra work when the
|
||||||
pub rebuild_encryption: bool,
|
/// underlying format stores them decrypted (e.g. WIA/RVZ).
|
||||||
/// Wii: Validate partition data hashes while reading the disc image.
|
///
|
||||||
|
/// 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,
|
pub validate_hashes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +198,6 @@ pub struct OpenOptions {
|
||||||
/// This is the primary entry point for reading disc images.
|
/// This is the primary entry point for reading disc images.
|
||||||
pub struct Disc {
|
pub struct Disc {
|
||||||
reader: disc::reader::DiscReader,
|
reader: disc::reader::DiscReader,
|
||||||
options: OpenOptions,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Disc {
|
impl Disc {
|
||||||
|
@ -180,7 +212,7 @@ impl Disc {
|
||||||
pub fn new_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Disc> {
|
pub fn new_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Disc> {
|
||||||
let io = io::block::open(path.as_ref())?;
|
let io = io::block::open(path.as_ref())?;
|
||||||
let reader = disc::reader::DiscReader::new(io, options)?;
|
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.
|
/// Opens a disc image from a read stream.
|
||||||
|
@ -197,7 +229,7 @@ impl Disc {
|
||||||
) -> Result<Disc> {
|
) -> Result<Disc> {
|
||||||
let io = io::block::new(stream)?;
|
let io = io::block::new(stream)?;
|
||||||
let reader = disc::reader::DiscReader::new(io, options)?;
|
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.
|
/// Detects the format of a disc image from a read stream.
|
||||||
|
@ -236,7 +268,20 @@ impl Disc {
|
||||||
/// **GameCube**: `index` must always be 0.
|
/// **GameCube**: `index` must always be 0.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase>> {
|
pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase>> {
|
||||||
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<Box<dyn PartitionBase>> {
|
||||||
|
self.reader.open_partition(index, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a decrypted partition read stream for the first partition matching
|
/// Opens a decrypted partition read stream for the first partition matching
|
||||||
|
@ -245,7 +290,20 @@ impl Disc {
|
||||||
/// **GameCube**: `kind` must always be [`PartitionKind::Data`].
|
/// **GameCube**: `kind` must always be [`PartitionKind::Data`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase>> {
|
pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase>> {
|
||||||
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<Box<dyn PartitionBase>> {
|
||||||
|
self.reader.open_partition_kind(kind, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ indicatif = "0.17"
|
||||||
itertools = "0.13"
|
itertools = "0.13"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
md-5 = "0.10"
|
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"] }
|
quick-xml = { version = "0.36", features = ["serialize"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha1 = "0.10"
|
sha1 = "0.10"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
|
use nod::OpenOptions;
|
||||||
|
|
||||||
use crate::util::{redump, shared::convert_and_verify};
|
use crate::util::{redump, shared::convert_and_verify};
|
||||||
|
|
||||||
|
@ -20,6 +21,12 @@ pub struct Args {
|
||||||
#[argp(option, short = 'd')]
|
#[argp(option, short = 'd')]
|
||||||
/// path to DAT file(s) for verification (optional)
|
/// path to DAT file(s) for verification (optional)
|
||||||
dat: Vec<PathBuf>,
|
dat: Vec<PathBuf>,
|
||||||
|
#[argp(switch)]
|
||||||
|
/// decrypt Wii partition data
|
||||||
|
decrypt: bool,
|
||||||
|
#[argp(switch)]
|
||||||
|
/// encrypt Wii partition data
|
||||||
|
encrypt: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(args: Args) -> nod::Result<()> {
|
pub fn run(args: Args) -> nod::Result<()> {
|
||||||
|
@ -27,5 +34,15 @@ pub fn run(args: Args) -> nod::Result<()> {
|
||||||
println!("Loading dat files...");
|
println!("Loading dat files...");
|
||||||
redump::load_dats(args.dat.iter().map(PathBuf::as_ref))?;
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use std::{
|
||||||
|
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
|
||||||
use nod::{Disc, OpenOptions, Result, ResultContext};
|
use nod::{Disc, OpenOptions, PartitionEncryptionMode, Result, ResultContext};
|
||||||
use zerocopy::FromZeros;
|
use zerocopy::FromZeros;
|
||||||
|
|
||||||
use crate::util::{
|
use crate::util::{
|
||||||
|
@ -165,10 +165,8 @@ struct DiscHashes {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_disc(path: &Path, name: &str, full_verify: bool) -> Result<DiscHashes> {
|
fn load_disc(path: &Path, name: &str, full_verify: bool) -> Result<DiscHashes> {
|
||||||
let mut disc = Disc::new_with_options(path, &OpenOptions {
|
let options = OpenOptions { partition_encryption: PartitionEncryptionMode::Original };
|
||||||
rebuild_encryption: true,
|
let mut disc = Disc::new_with_options(path, &options)?;
|
||||||
validate_hashes: false,
|
|
||||||
})?;
|
|
||||||
let disc_size = disc.disc_size();
|
let disc_size = disc.disc_size();
|
||||||
if !full_verify {
|
if !full_verify {
|
||||||
let meta = disc.meta();
|
let meta = disc.meta();
|
||||||
|
|
|
@ -9,7 +9,8 @@ use std::{
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nod::{
|
use nod::{
|
||||||
Disc, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta, ResultContext,
|
Disc, Fst, Node, OpenOptions, PartitionBase, PartitionKind, PartitionMeta, PartitionOptions,
|
||||||
|
ResultContext,
|
||||||
};
|
};
|
||||||
use size::{Base, Size};
|
use size::{Base, Size};
|
||||||
use zerocopy::IntoBytes;
|
use zerocopy::IntoBytes;
|
||||||
|
@ -52,36 +53,39 @@ pub fn run(args: Args) -> nod::Result<()> {
|
||||||
} else {
|
} else {
|
||||||
output_dir = args.file.with_extension("");
|
output_dir = args.file.with_extension("");
|
||||||
}
|
}
|
||||||
let disc = Disc::new_with_options(&args.file, &OpenOptions {
|
let disc = Disc::new_with_options(&args.file, &OpenOptions::default())?;
|
||||||
rebuild_encryption: false,
|
|
||||||
validate_hashes: args.validate,
|
|
||||||
})?;
|
|
||||||
let header = disc.header();
|
let header = disc.header();
|
||||||
let is_wii = header.is_wii();
|
let is_wii = header.is_wii();
|
||||||
|
let partition_options = PartitionOptions { validate_hashes: args.validate };
|
||||||
if let Some(partition) = args.partition {
|
if let Some(partition) = args.partition {
|
||||||
if partition.eq_ignore_ascii_case("all") {
|
if partition.eq_ignore_ascii_case("all") {
|
||||||
for info in disc.partitions() {
|
for info in disc.partitions() {
|
||||||
let mut out_dir = output_dir.clone();
|
let mut out_dir = output_dir.clone();
|
||||||
out_dir.push(info.kind.dir_name().as_ref());
|
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)?;
|
extract_partition(&disc, partition.as_mut(), &out_dir, is_wii, args.quiet)?;
|
||||||
}
|
}
|
||||||
} else if partition.eq_ignore_ascii_case("data") {
|
} 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)?;
|
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||||
} else if partition.eq_ignore_ascii_case("update") {
|
} 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)?;
|
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||||
} else if partition.eq_ignore_ascii_case("channel") {
|
} 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)?;
|
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||||
} else {
|
} else {
|
||||||
let idx = partition.parse::<usize>().map_err(|_| "Invalid partition index")?;
|
let idx = partition.parse::<usize>().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)?;
|
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||||
}
|
}
|
||||||
} else {
|
} 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)?;
|
extract_partition(&disc, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
use nod::{Disc, OpenOptions, SECTOR_SIZE};
|
use nod::{Disc, SECTOR_SIZE};
|
||||||
use size::Size;
|
use size::Size;
|
||||||
|
|
||||||
use crate::util::{display, shared::print_header};
|
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<()> {
|
fn info_file(path: &Path) -> nod::Result<()> {
|
||||||
log::info!("Loading {}", display(path));
|
log::info!("Loading {}", display(path));
|
||||||
let disc = Disc::new_with_options(path, &OpenOptions {
|
let disc = Disc::new(path)?;
|
||||||
rebuild_encryption: false,
|
|
||||||
validate_hashes: false,
|
|
||||||
})?;
|
|
||||||
let header = disc.header();
|
let header = disc.header();
|
||||||
let meta = disc.meta();
|
let meta = disc.meta();
|
||||||
print_header(header, &meta);
|
print_header(header, &meta);
|
||||||
|
|
||||||
if header.is_wii() {
|
if header.is_wii() {
|
||||||
for (idx, info) in disc.partitions().iter().enumerate() {
|
for (idx, info) in disc.partitions().iter().enumerate() {
|
||||||
|
let mut partition = disc.open_partition(idx)?;
|
||||||
|
let meta = partition.meta()?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("Partition {}", idx);
|
println!("Partition {}", idx);
|
||||||
println!("\tType: {}", info.kind);
|
println!("\tType: {}", info.kind);
|
||||||
|
@ -41,43 +41,50 @@ fn info_file(path: &Path) -> nod::Result<()> {
|
||||||
println!("\tStart sector: {} (offset {:#X})", info.start_sector, offset);
|
println!("\tStart sector: {} (offset {:#X})", info.start_sector, offset);
|
||||||
let data_size =
|
let data_size =
|
||||||
(info.data_end_sector - info.data_start_sector) as u64 * SECTOR_SIZE as u64;
|
(info.data_end_sector - info.data_start_sector) as u64 * SECTOR_SIZE as u64;
|
||||||
|
if info.has_encryption {
|
||||||
println!(
|
println!(
|
||||||
"\tData offset / size: {:#X} / {:#X} ({})",
|
"\tEncrypted data offset / size: {:#X} / {:#X} ({})",
|
||||||
info.data_start_sector as u64 * SECTOR_SIZE as u64,
|
info.data_start_sector as u64 * SECTOR_SIZE as u64,
|
||||||
data_size,
|
data_size,
|
||||||
Size::from_bytes(data_size)
|
Size::from_bytes(data_size)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"\tDecrypted data offset / size: {:#X} / {:#X} ({})",
|
||||||
|
offset,
|
||||||
|
data_size,
|
||||||
|
Size::from_bytes(data_size)
|
||||||
|
);
|
||||||
|
}
|
||||||
println!(
|
println!(
|
||||||
"\tTMD offset / size: {:#X} / {:#X}",
|
"\tTMD offset / size: {:#X} / {:#X}",
|
||||||
offset + info.header.tmd_off(),
|
offset + info.header.tmd_off(),
|
||||||
info.header.tmd_size()
|
info.header.tmd_size()
|
||||||
);
|
);
|
||||||
|
if let Some(content_metadata) = meta.content_metadata() {
|
||||||
|
for content in content_metadata {
|
||||||
println!(
|
println!(
|
||||||
"\tCert offset / size: {:#X} / {:#X}",
|
"\t-> Content {:08X} size: {:#X} ({})",
|
||||||
|
content.content_index.get(),
|
||||||
|
content.size.get(),
|
||||||
|
Size::from_bytes(content.size.get()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"\tCert chain offset / size: {:#X} / {:#X}",
|
||||||
offset + info.header.cert_chain_off(),
|
offset + info.header.cert_chain_off(),
|
||||||
info.header.cert_chain_size()
|
info.header.cert_chain_size()
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"\tH3 offset / size: {:#X} / {:#X}",
|
"\tH3 table offset / size: {:#X} / {:#X}",
|
||||||
offset + info.header.h3_table_off(),
|
offset + info.header.h3_table_off(),
|
||||||
info.header.h3_table_size()
|
info.header.h3_table_size()
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut partition = disc.open_partition(idx)?;
|
|
||||||
let meta = partition.meta()?;
|
|
||||||
let tmd = meta.tmd_header();
|
let tmd = meta.tmd_header();
|
||||||
let title_id_str = if let Some(tmd) = tmd {
|
let title_id_str = if let Some(tmd) = tmd {
|
||||||
format!(
|
hex::encode_upper(tmd.title_id)
|
||||||
"{: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]
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
"N/A".to_string()
|
"N/A".to_string()
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
|
use nod::{OpenOptions, PartitionEncryptionMode};
|
||||||
|
|
||||||
use crate::util::{redump, shared::convert_and_verify};
|
use crate::util::{redump, shared::convert_and_verify};
|
||||||
|
|
||||||
|
@ -24,8 +25,9 @@ pub fn run(args: Args) -> nod::Result<()> {
|
||||||
println!("Loading dat files...");
|
println!("Loading dat files...");
|
||||||
redump::load_dats(args.dat.iter().map(PathBuf::as_ref))?;
|
redump::load_dats(args.dat.iter().map(PathBuf::as_ref))?;
|
||||||
}
|
}
|
||||||
|
let options = OpenOptions { partition_encryption: PartitionEncryptionMode::Original };
|
||||||
for file in &args.file {
|
for file in &args.file {
|
||||||
convert_and_verify(file, None, args.md5)?;
|
convert_and_verify(file, None, args.md5, &options)?;
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -38,20 +38,22 @@ pub fn print_header(header: &DiscHeader, meta: &DiscMeta) {
|
||||||
println!("Title: {}", header.game_title_str());
|
println!("Title: {}", header.game_title_str());
|
||||||
println!("Game ID: {}", header.game_id_str());
|
println!("Game ID: {}", header.game_id_str());
|
||||||
println!("Disc {}, Revision {}", header.disc_num + 1, header.disc_version);
|
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");
|
println!("[!] Disc has no hashes");
|
||||||
}
|
}
|
||||||
if header.no_partition_encryption != 0 {
|
if !header.has_partition_encryption() {
|
||||||
println!("[!] Disc is not encrypted");
|
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));
|
println!("Loading {}", display(in_file));
|
||||||
let mut disc = Disc::new_with_options(in_file, &OpenOptions {
|
let mut disc = Disc::new_with_options(in_file, options)?;
|
||||||
rebuild_encryption: true,
|
|
||||||
validate_hashes: false,
|
|
||||||
})?;
|
|
||||||
let header = disc.header();
|
let header = disc.header();
|
||||||
let meta = disc.meta();
|
let meta = disc.meta();
|
||||||
print_header(header, &meta);
|
print_header(header, &meta);
|
||||||
|
|
Loading…
Reference in New Issue