mirror of
https://github.com/aaru-dps/libaaruformat.git
synced 2026-07-08 18:06:18 +00:00
[spec] Add annex about Nintendo Wii U Disc Encryption
This commit is contained in:
239
docs/spec/appendixes/wiiu_encryption.adoc
Normal file
239
docs/spec/appendixes/wiiu_encryption.adoc
Normal file
@@ -0,0 +1,239 @@
|
||||
[appendix]
|
||||
== 🔐 Nintendo Wii U Disc Encryption
|
||||
|
||||
This annex describes the encryption scheme used by Nintendo Wii U Optical Discs (WUOD), and how AaruFormat handles decryption for storage, deduplication, and compression.
|
||||
|
||||
=== 📖 Overview
|
||||
|
||||
Nintendo Wii U discs employ a simpler encryption scheme than the Wii: each 0x8000-byte (32 KiB) physical sector is encrypted as a single AES-128-CBC block with a constant IV of all zeros.
|
||||
There are no separate hash blocks within the encrypted sector.
|
||||
|
||||
Two tiers of keys exist:
|
||||
|
||||
* A per-disc **disc key** (obtained from a non-readable area of the disc) encrypts system partitions and the Table of Contents (TOC).
|
||||
* Per-title **title keys** (extracted from TITLE.TIK files within system partitions) encrypt game data (GM) partitions.
|
||||
|
||||
The first three physical sectors (0–2) and each partition's header sector are always plaintext.
|
||||
|
||||
AaruFormat stores Wii U disc sectors in their *decrypted* form for deduplication and compression, transparently re-encrypting them on read.
|
||||
|
||||
=== 🗂️ Disc Layout
|
||||
|
||||
[source]
|
||||
----
|
||||
Physical sector 0–2 (0x00000–0x17FFF): Disc header (plaintext)
|
||||
Physical sector 3 (0x18000): TOC (encrypted with disc key)
|
||||
Sector 4+: Partitions (encrypted, per-partition keys)
|
||||
----
|
||||
|
||||
==== Disc Header
|
||||
|
||||
The disc header occupies the first three physical sectors (0x18000 bytes total) and is always plaintext.
|
||||
Key metadata fields:
|
||||
|
||||
[cols="2,2,5",options="header"]
|
||||
|===
|
||||
|Offset |Size |Description
|
||||
|0x00 |10 bytes |Product code (ASCII)
|
||||
|0x15 |1 byte |Disc number (ASCII digit)
|
||||
|===
|
||||
|
||||
==== Table of Contents (TOC)
|
||||
|
||||
The TOC resides at disc offset `0x18000` (physical sector 3) and is encrypted with the disc key using AES-128-CBC with IV = all zeros.
|
||||
|
||||
After decryption, the TOC has the following structure:
|
||||
|
||||
[cols="2,2,5",options="header"]
|
||||
|===
|
||||
|Offset |Type |Description
|
||||
|0x00 |uint32_t BE |TOC signature (`0xCCA6E67B`)
|
||||
|0x1C |uint32_t BE |Partition count
|
||||
|0x800 + _i_ × 0x80 |128 bytes |Partition entry _i_
|
||||
|===
|
||||
|
||||
Each partition entry contains:
|
||||
|
||||
[cols="2,2,5",options="header"]
|
||||
|===
|
||||
|Offset |Type |Description
|
||||
|0x00 |char[25] |Partition identifier (e.g., `SI`, `UP`, `GI`, `GM` + title ID hex)
|
||||
|0x20 |uint32_t BE |Start physical sector (in 0x8000-byte units)
|
||||
|===
|
||||
|
||||
Partition types by identifier prefix:
|
||||
|
||||
[cols="1,3",options="header"]
|
||||
|===
|
||||
|Prefix |Description
|
||||
|SI |System Information partition
|
||||
|UP |Update partition
|
||||
|GI |Game Information partition
|
||||
|GM |Game data partition (uses per-title key)
|
||||
|===
|
||||
|
||||
=== 🔑 Key Hierarchy
|
||||
|
||||
==== Disc Key
|
||||
|
||||
The disc key is a 16-byte AES-128 key obtained from a non-standard, non-readable area of the physical disc.
|
||||
It cannot be extracted through normal optical drive commands and must be provided externally (via command-line option or sidecar `.disckey` / `.key` file).
|
||||
|
||||
The disc key encrypts:
|
||||
|
||||
* The TOC at physical sector 3
|
||||
* All partition data for **SI**, **UP**, and **GI** partitions (system partitions)
|
||||
|
||||
==== Wii U Common Key
|
||||
|
||||
A single system-wide 128-bit AES key, publicly known, is used to decrypt per-title keys from ticket files.
|
||||
|
||||
==== Per-Title Key Derivation
|
||||
|
||||
GM (game) partitions use unique per-title keys derived from TITLE.TIK files found within SI or GI partitions:
|
||||
|
||||
. **Locate TITLE.TIK**: Parse the FST (File System Table) of each SI/GI partition. The FST starts at the partition's first sector with a `FST\0` magic signature. File entries are 16 bytes each, with cluster-based addressing.
|
||||
|
||||
. **Read ticket fields**:
|
||||
** Encrypted title key (16 bytes) at ticket offset `0x1BF`
|
||||
** Title ID (8 bytes) at ticket offset `0x1DC`
|
||||
|
||||
. **Decrypt title key**:
|
||||
+
|
||||
[source]
|
||||
----
|
||||
IV = title_id || 8_zero_bytes
|
||||
title_key = AES-128-CBC-Decrypt(WIIU_COMMON_KEY, IV, encrypted_title_key)
|
||||
----
|
||||
|
||||
. **Match to GM partition**: The GM partition identifier encodes the title ID as hex digits (e.g., `GM0005000E10143500`). The decrypted title key is assigned to the GM partition whose identifier matches.
|
||||
|
||||
. **Fallback**: Partitions without a matched title key (SI, UP, GI) use the disc key directly.
|
||||
|
||||
=== 🔒 Physical Sector Encryption
|
||||
|
||||
==== Algorithm
|
||||
|
||||
All encrypted physical sectors use **AES-128-CBC** with:
|
||||
|
||||
* **Key**: The partition's key (disc key for system partitions, title key for GM partitions)
|
||||
* **IV**: All zeros (16 bytes of `0x00`)
|
||||
* **Block size**: The entire 0x8000-byte (32 KiB) physical sector is one CBC chain
|
||||
|
||||
This is significantly simpler than the Wii scheme, which uses separate IVs for hash and data blocks within each group.
|
||||
|
||||
==== Plaintext Sectors
|
||||
|
||||
The following sectors are never encrypted:
|
||||
|
||||
* Physical sectors 0–2 (disc header)
|
||||
* Each partition's first physical sector (partition header at `start_sector`)
|
||||
|
||||
=== 📦 Source Formats
|
||||
|
||||
The conversion tool accepts three Wii U disc image formats:
|
||||
|
||||
==== WUD (Wii U Disc)
|
||||
|
||||
Raw disc dump, stored as a flat binary file. Each byte corresponds directly to its disc offset.
|
||||
|
||||
==== WUX (Wii U Compressed)
|
||||
|
||||
A compressed WUD format with sector-level deduplication:
|
||||
|
||||
[cols="2,2,5",options="header"]
|
||||
|===
|
||||
|Offset |Type |Description
|
||||
|0x00 |uint32_t LE |Magic: `0x30585557` (`WUX0`)
|
||||
|0x04 |uint32_t LE |Reserved
|
||||
|0x08 |uint32_t LE |Sector size (must be `0x8000`)
|
||||
|0x0C |uint32_t LE |Reserved (0)
|
||||
|0x10 |uint64_t LE |Uncompressed disc size in bytes
|
||||
|0x18 |uint64_t LE |Reserved (0)
|
||||
|===
|
||||
|
||||
After the 32-byte header, a sector index table maps logical sectors to physical sectors:
|
||||
|
||||
* Index table: `sector_count × sizeof(uint32_t)` bytes at offset `0x20`
|
||||
* Data sectors: Start at the next 0x8000-aligned offset after the index table
|
||||
* Each index entry maps logical sector _i_ to physical sector `index[i]`
|
||||
|
||||
Identical logical sectors share the same physical sector, providing basic deduplication.
|
||||
|
||||
==== AaruFormat
|
||||
|
||||
An existing AaruFormat image with media type `WUOD`. The disc key may be stored in the `kMediaTagWiiUDiscKey` media tag.
|
||||
|
||||
=== 💾 Storage in AaruFormat
|
||||
|
||||
==== Partition Key Map
|
||||
|
||||
The partition-to-key mapping is stored as media tag `kMediaTagWiiUPartitionKeyMap` (value 82):
|
||||
|
||||
[cols="2,2,2,6",options="header"]
|
||||
|===
|
||||
|Offset |Type |Size |Description
|
||||
|0 |uint32_t LE |4 bytes |Number of partitions (maximum 8)
|
||||
|4 + _i_ × 24 |uint32_t LE |4 bytes |Start physical sector of partition _i_ (in 0x8000-byte units)
|
||||
|8 + _i_ × 24 |uint32_t LE |4 bytes |End physical sector of partition _i_ (exclusive, in 0x8000-byte units)
|
||||
|12 + _i_ × 24 |uint8_t[16] |16 bytes |AES-128 key for partition _i_
|
||||
|===
|
||||
|
||||
The disc key itself is stored separately as media tag `kMediaTagWiiUDiscKey` (value 76), 16 bytes.
|
||||
|
||||
==== Write Path (Conversion)
|
||||
|
||||
The conversion processes each physical sector sequentially:
|
||||
|
||||
. **Read** the 0x8000-byte physical sector from the source (WUD, WUX, or AaruFormat).
|
||||
|
||||
. **Classify**: Determine if the sector is plaintext or encrypted:
|
||||
** Physical sectors 0–2: always plaintext.
|
||||
** A partition's `start_sector`: always plaintext (partition header).
|
||||
** All other sectors within a partition's range: encrypted.
|
||||
** Sectors outside any partition: plaintext.
|
||||
|
||||
. **Decrypt** encrypted sectors using `wiiu_decrypt_physical_sector()` — AES-128-CBC with the partition's key and IV = all zeros.
|
||||
|
||||
. **Write** 16 logical sectors (each 2048 bytes) to the AaruFormat image:
|
||||
** Decrypted sectors: status `SectorStatusUnencrypted` (`0xA`)
|
||||
** Plaintext sectors: status `SectorStatusDumped` (`0x1`)
|
||||
|
||||
NOTE: Unlike Wii discs, Wii U conversion does not perform LFG junk detection. Wii U discs do not use the same LFG junk fill pattern as GameCube/Wii discs.
|
||||
|
||||
==== Read Path (Re-encryption)
|
||||
|
||||
When reading a sector marked `SectorStatusUnencrypted` from a WUOD image:
|
||||
|
||||
. **Lazy initialization**: Load the disc key and partition key map from media tags. Allocate a 0x8000-byte group cache.
|
||||
|
||||
. **Key lookup**: Convert the logical sector address to a physical sector number. Look up the partition key. Return NULL (plaintext) for disc header sectors and partition header sectors.
|
||||
|
||||
. **Group assembly**: Gather all 16 logical sectors of the physical sector:
|
||||
** The current sector uses the data already in the read buffer.
|
||||
** The remaining 15 are read via recursive `aaruf_read_sector()` calls with a recursion guard flag (`wiiu_building_crypto_block`) to prevent infinite re-encryption loops.
|
||||
** Because AaruFormat stores 2048-byte logical sectors via the DDT, different logical sectors from the same 0x8000-byte physical sector may reside in different compressed data blocks. The library cannot assume contiguous storage.
|
||||
|
||||
. **Re-encryption**: The assembled 0x8000-byte buffer is encrypted in-place using `wiiu_encrypt_physical_sector()` with the partition's key and IV = all zeros.
|
||||
|
||||
. **Cache**: The encrypted physical sector is cached. Subsequent reads of logical sectors from the same physical sector are served directly from the cache.
|
||||
|
||||
. The caller receives the 2048-byte slice from the encrypted sector, byte-identical to the original disc.
|
||||
|
||||
=== 📊 Storage Benefits
|
||||
|
||||
* **Deduplication**: Decrypted game data contains significant repetition (empty filesystem areas, zero-padded regions, common structures) that deduplicates effectively.
|
||||
* **Compression**: Decrypted data compresses well under LZMA/Zstandard. Encrypted data is pseudorandom and incompressible.
|
||||
* **Typical reduction**: A 25 GiB Wii U disc image commonly compresses to 5–15 GiB depending on game content.
|
||||
|
||||
=== 🏷️ Required Media Tags
|
||||
|
||||
[cols="1,2,5",options="header"]
|
||||
|===
|
||||
|Value |Tag Name |Description
|
||||
|76 |kMediaTagWiiUDiscKey |16-byte disc key (required for TOC decryption and system partition access)
|
||||
|82 |kMediaTagWiiUPartitionKeyMap |Partition-to-key mapping with sector ranges (required for re-encryption)
|
||||
|===
|
||||
|
||||
Both tags must be present for full transparent re-encryption.
|
||||
If either is absent, sectors are returned as stored (decrypted) without re-encryption.
|
||||
@@ -158,4 +158,8 @@ include::appendixes/ngcw_junk.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::appendixes/wii_encryption.adoc[]
|
||||
include::appendixes/wii_encryption.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::appendixes/wiiu_encryption.adoc[]
|
||||
Reference in New Issue
Block a user