diff --git a/src/ngcw/wii_crypto.c b/src/ngcw/wii_crypto.c
new file mode 100644
index 0000000..193040c
--- /dev/null
+++ b/src/ngcw/wii_crypto.c
@@ -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
+ * .
+ *
+ * Nintendo Wii disc encryption: partition key map, group encrypt/decrypt.
+ */
+
+#include
+#include
+#include
+
+#include
+
+#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, ®ions, &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;
+ }
+}
diff --git a/src/ngcw/wii_crypto.h b/src/ngcw/wii_crypto.h
new file mode 100644
index 0000000..2026da1
--- /dev/null
+++ b/src/ngcw/wii_crypto.h
@@ -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
+ * .
+ *
+ * Nintendo Wii disc encryption: partition key map, group encrypt/decrypt.
+ */
+
+#ifndef LIBAARUFORMAT_NGCW_WII_CRYPTO_H
+#define LIBAARUFORMAT_NGCW_WII_CRYPTO_H
+
+#include
+#include
+
+/* 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 */