Add Wii disc encryption support: implement partition key map and group encrypt/decrypt functions

This commit is contained in:
2026-03-16 18:39:18 +00:00
parent d5d6e036e4
commit a128473f41
2 changed files with 336 additions and 0 deletions

211
src/ngcw/wii_crypto.c Normal file
View File

@@ -0,0 +1,211 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*
* Nintendo Wii disc encryption: partition key map, group encrypt/decrypt.
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <aaruformat.h>
#include "../lib/aes128.h"
#include "wii_crypto.h"
/* ---- Little-endian helpers ---- */
static uint32_t read_le32(const uint8_t *p)
{ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); }
static void write_le32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)((v >> 8) & 0xFF);
p[2] = (uint8_t)((v >> 16) & 0xFF);
p[3] = (uint8_t)((v >> 24) & 0xFF);
}
/* ---- Key lookup ---- */
const uint8_t *wii_get_sector_key(const WiiPartitionRegion *regions, uint32_t region_count, uint64_t logical_sector)
{
if(regions == NULL || region_count == 0) return NULL;
/* Convert logical (2048-byte) sector to physical (0x8000-byte) group */
uint64_t phys_group = logical_sector / WII_LOGICAL_PER_GROUP;
for(uint32_t i = 0; i < region_count; i++)
{
if(phys_group >= regions[i].start_sector && phys_group < regions[i].end_sector)
{
/* Partition header group is plaintext */
if(phys_group == regions[i].start_sector) return NULL;
return regions[i].key;
}
}
/* Outside any known partition — plaintext */
return NULL;
}
bool wii_is_sector_encrypted(const WiiPartitionRegion *regions, uint32_t region_count, uint64_t logical_sector)
{ return wii_get_sector_key(regions, region_count, logical_sector) != NULL; }
/* ---- Group encrypt/decrypt ---- */
void wii_encrypt_group(const uint8_t key[16], const uint8_t *hash_block, const uint8_t *data_in, uint8_t *out)
{
/* Hash block: first 0x400 bytes, IV = all zeros */
uint8_t iv[16];
memset(iv, 0, sizeof(iv));
memcpy(out, hash_block, WII_GROUP_HASH_SIZE);
aes128_cbc_encrypt(key, iv, out, WII_GROUP_HASH_SIZE);
/* Data block: next 0x7C00 bytes.
* IV = bytes 0x3D0..0x3DF of the ENCRYPTED hash output (just written to out). */
uint8_t data_iv[16];
memcpy(data_iv, out + 0x3D0, 16);
memcpy(out + WII_GROUP_HASH_SIZE, data_in, WII_GROUP_DATA_SIZE);
aes128_cbc_encrypt(key, data_iv, out + WII_GROUP_HASH_SIZE, WII_GROUP_DATA_SIZE);
}
void wii_decrypt_group(const uint8_t key[16], const uint8_t *in, uint8_t *hash_block, uint8_t *data_out)
{
/* Hash block: first 0x400 bytes, IV = all zeros */
uint8_t iv[16];
memset(iv, 0, sizeof(iv));
memcpy(hash_block, in, WII_GROUP_HASH_SIZE);
aes128_cbc_decrypt(key, iv, hash_block, WII_GROUP_HASH_SIZE);
/* Data block: next 0x7C00 bytes.
* IV = bytes 0x3D0..0x3DF of the ENCRYPTED input (not the decrypted hash block). */
uint8_t data_iv[16];
memcpy(data_iv, in + 0x3D0, 16);
memcpy(data_out, in + WII_GROUP_HASH_SIZE, WII_GROUP_DATA_SIZE);
aes128_cbc_decrypt(key, data_iv, data_out, WII_GROUP_DATA_SIZE);
}
/* ---- Serialization (same format as Wii U) ---- */
int32_t wii_serialize_partition_key_map(const WiiPartitionRegion *regions, uint32_t count, uint8_t **out_data,
uint32_t *out_len)
{
if(out_data == NULL || out_len == NULL) return -1;
if(count > WII_MAX_PARTITIONS) return -3;
uint32_t size = 4 + count * 24;
uint8_t *buf = (uint8_t *)malloc(size);
if(buf == NULL) return -4;
write_le32(buf, count);
for(uint32_t i = 0; i < count; i++)
{
uint32_t offset = 4 + i * 24;
write_le32(buf + offset, regions[i].start_sector);
write_le32(buf + offset + 4, regions[i].end_sector);
memcpy(buf + offset + 8, regions[i].key, 16);
}
*out_data = buf;
*out_len = size;
return 0;
}
int32_t wii_deserialize_partition_key_map(const uint8_t *data, uint32_t data_len, WiiPartitionRegion **regions,
uint32_t *count)
{
if(data == NULL || regions == NULL || count == NULL) return -1;
if(data_len < 4) return -2;
uint32_t region_count = read_le32(data);
if(region_count > WII_MAX_PARTITIONS) return -3;
if(region_count == 0)
{
*regions = NULL;
*count = 0;
return 0;
}
uint32_t required = 4 + region_count * 24;
if(data_len < required) return -2;
WiiPartitionRegion *r = (WiiPartitionRegion *)malloc(region_count * sizeof(WiiPartitionRegion));
if(r == NULL) return -4;
for(uint32_t i = 0; i < region_count; i++)
{
uint32_t offset = 4 + i * 24;
r[i].start_sector = read_le32(data + offset);
r[i].end_sector = read_le32(data + offset + 4);
memcpy(r[i].key, data + offset + 8, 16);
if(r[i].start_sector >= r[i].end_sector)
{
memset(r, 0, region_count * sizeof(WiiPartitionRegion));
free(r);
return -5;
}
}
*regions = r;
*count = region_count;
return 0;
}
/* ---- Lazy initialization ---- */
void wii_lazy_init(aaruformat_context *ctx)
{
if(ctx == NULL) return;
/* Read and deserialize partition key map from media tags */
if(ctx->wii_partition_regions == NULL)
{
mediaTagEntry *item = NULL;
int32_t tag = kMediaTagWiiPartitionKeyMap;
HASH_FIND_INT(ctx->mediaTags, &tag, item);
if(item != NULL && item->length >= 4)
{
WiiPartitionRegion *regions = NULL;
uint32_t count = 0;
if(wii_deserialize_partition_key_map(item->data, item->length, &regions, &count) == 0)
{
ctx->wii_partition_regions = regions;
ctx->wii_partition_region_count = count;
}
}
}
/* Allocate the encrypted group cache if needed */
if(ctx->wii_encrypted_group_cache == NULL)
{
ctx->wii_encrypted_group_cache = (uint8_t *)malloc(WII_GROUP_SIZE);
ctx->wii_cache_valid = false;
}
}

125
src/ngcw/wii_crypto.h Normal file
View File

@@ -0,0 +1,125 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* <https://www.gnu.org/licenses/>.
*
* Nintendo Wii disc encryption: partition key map, group encrypt/decrypt.
*/
#ifndef LIBAARUFORMAT_NGCW_WII_CRYPTO_H
#define LIBAARUFORMAT_NGCW_WII_CRYPTO_H
#include <stdbool.h>
#include <stdint.h>
/* Forward declaration */
typedef struct aaruformat_context aaruformat_context;
#ifdef __cplusplus
extern "C"
{
#endif
#define WII_GROUP_SIZE 0x8000 /**< Wii physical group size (32 KiB). */
#define WII_GROUP_HASH_SIZE 0x0400 /**< Hash block size within a group (1 KiB). */
#define WII_GROUP_DATA_SIZE 0x7C00 /**< User data size within a group (31 KiB). */
#define WII_LOGICAL_PER_GROUP 16 /**< Number of 2048-byte logical sectors per group. */
#define WII_MAX_PARTITIONS 32 /**< Maximum number of partitions supported. */
/**
* @brief A Wii partition region entry (in-memory representation).
*
* Stores the physical sector range and AES-128 key for one partition.
* Physical sector numbers are in 0x8000-byte units.
*/
typedef struct WiiPartitionRegion
{
uint32_t start_sector; /**< First physical sector of partition. */
uint32_t end_sector; /**< End physical sector (exclusive). */
uint8_t key[16]; /**< AES-128 partition key. */
} WiiPartitionRegion;
/**
* @brief Get the encryption key for a given logical sector (2048-byte).
*
* Converts the logical sector address to a physical sector address,
* then looks up which partition it belongs to and whether it is encrypted.
*
* @param regions Array of partition regions.
* @param region_count Number of partition regions.
* @param logical_sector Logical sector address (2048-byte units).
* @return Pointer to the 16-byte key if encrypted, NULL if plaintext.
*/
const uint8_t *wii_get_sector_key(const WiiPartitionRegion *regions, uint32_t region_count,
uint64_t logical_sector);
/**
* @brief Check if a logical sector is in an encrypted region.
*/
bool wii_is_sector_encrypted(const WiiPartitionRegion *regions, uint32_t region_count, uint64_t logical_sector);
/**
* @brief Encrypt a Wii group (0x8000 bytes) from separate hash_block + data.
*
* The hash block IV is all zeros. The data IV is bytes 0x3D0..0x3DF of the
* encrypted hash block output (matching Dolphin's VolumeWii::EncryptBlock).
*
* @param key 16-byte AES-128 partition key.
* @param hash_block 0x400-byte hash block (plaintext input).
* @param data_in 0x7C00-byte user data (plaintext input).
* @param out 0x8000-byte output buffer (encrypted).
*/
void wii_encrypt_group(const uint8_t key[16], const uint8_t *hash_block, const uint8_t *data_in, uint8_t *out);
/**
* @brief Decrypt a Wii group (0x8000 bytes) into separate hash_block + data.
*
* @param key 16-byte AES-128 partition key.
* @param in 0x8000-byte encrypted input.
* @param hash_block 0x400-byte output for hash block.
* @param data_out 0x7C00-byte output for user data.
*/
void wii_decrypt_group(const uint8_t key[16], const uint8_t *in, uint8_t *hash_block, uint8_t *data_out);
/**
* @brief Serialize a Wii partition key map for storage as a media tag.
*
* Same format as Wii U partition key map:
* [4 bytes] partition_count (uint32 LE)
* Per partition (24 bytes): start_sector (LE), end_sector (LE), key[16]
*/
int32_t wii_serialize_partition_key_map(const WiiPartitionRegion *regions, uint32_t count, uint8_t **out_data,
uint32_t *out_len);
/**
* @brief Deserialize a Wii partition key map from a media tag buffer.
*/
int32_t wii_deserialize_partition_key_map(const uint8_t *data, uint32_t data_len, WiiPartitionRegion **regions,
uint32_t *count);
/**
* @brief Lazy initialization: load partition key map from media tags.
*
* Populates ctx->wii_partition_regions and allocates the group cache.
*
* @param ctx AaruFormat context.
*/
void wii_lazy_init(aaruformat_context *ctx);
#ifdef __cplusplus
}
#endif
#endif /* LIBAARUFORMAT_NGCW_WII_CRYPTO_H */