Files
libaaruformat/tests/ngcw.cpp

456 lines
14 KiB
C++

/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* Tests for Nintendo GameCube/Wii LFG PRNG, junk map serialization,
* and Wii partition key map serialization.
* Uses synthetic data only (no disc images due to copyright).
*/
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include "gtest/gtest.h"
extern "C"
{
#include "../src/lib/aes128.h"
#include "../src/ngcw/lfg.h"
#include "../src/ngcw/ngcw_junk.h"
#include "../src/ngcw/wii_crypto.h"
}
/* ================================================================== */
/* LFG PRNG Tests */
/* ================================================================== */
/* Known seed for testing — arbitrary 17-word seed */
static const uint32_t test_seed[NGC_LFG_SEED_SIZE] = {
0x01234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210, 0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0, 0x0FEDCBA9,
0x87654321, 0xBEEFCAFE, 0xBABEFACE, 0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00, 0xA5A5A5A5};
TEST(LFG, SetSeedAndGenerate)
{
struct ngc_lfg_ctx lfg;
ngc_lfg_set_seed(&lfg, test_seed);
/* Generate some bytes */
uint8_t out[256];
ngc_lfg_get_bytes(&lfg, out, sizeof(out));
/* Output should not be all zeros */
bool all_zero = true;
for(size_t i = 0; i < sizeof(out); i++)
{
if(out[i] != 0)
{
all_zero = false;
break;
}
}
EXPECT_FALSE(all_zero);
}
TEST(LFG, Deterministic)
{
struct ngc_lfg_ctx lfg1, lfg2;
ngc_lfg_set_seed(&lfg1, test_seed);
ngc_lfg_set_seed(&lfg2, test_seed);
uint8_t out1[1024], out2[1024];
ngc_lfg_get_bytes(&lfg1, out1, sizeof(out1));
ngc_lfg_get_bytes(&lfg2, out2, sizeof(out2));
EXPECT_EQ(0, memcmp(out1, out2, sizeof(out1)));
}
TEST(LFG, ChunkedOutputMatchesBulk)
{
struct ngc_lfg_ctx lfg1, lfg2;
ngc_lfg_set_seed(&lfg1, test_seed);
ngc_lfg_set_seed(&lfg2, test_seed);
/* Bulk read */
uint8_t bulk[4096];
ngc_lfg_get_bytes(&lfg1, bulk, sizeof(bulk));
/* Chunked read: various chunk sizes */
uint8_t chunked[4096];
size_t pos = 0;
size_t chunk_sizes[] = {1, 7, 13, 64, 100, 521 * 4 - 1, 3, 2048};
int ci = 0;
while(pos < sizeof(chunked))
{
size_t chunk = chunk_sizes[ci % (sizeof(chunk_sizes) / sizeof(chunk_sizes[0]))];
if(pos + chunk > sizeof(chunked)) chunk = sizeof(chunked) - pos;
ngc_lfg_get_bytes(&lfg2, chunked + pos, chunk);
pos += chunk;
ci++;
}
EXPECT_EQ(0, memcmp(bulk, chunked, sizeof(bulk)));
}
TEST(LFG, SeedExtraction)
{
struct ngc_lfg_ctx lfg;
ngc_lfg_set_seed(&lfg, test_seed);
/* Generate a large buffer of known LFG output */
size_t buf_size = NGC_LFG_K * 4 * 2; /* 2 full buffer lengths */
uint8_t *buf = (uint8_t *)malloc(buf_size);
ASSERT_NE(nullptr, buf);
ngc_lfg_get_bytes(&lfg, buf, buf_size);
/* Try to extract seed from offset 0 */
uint32_t extracted_seed[NGC_LFG_SEED_SIZE];
size_t matched = ngc_lfg_get_seed(buf, buf_size, 0, extracted_seed);
EXPECT_GT(matched, (size_t)0);
if(matched > 0)
{
/* Verify extracted seed produces same output */
struct ngc_lfg_ctx lfg2;
ngc_lfg_set_seed(&lfg2, extracted_seed);
uint8_t *verify = (uint8_t *)malloc(buf_size);
ASSERT_NE(nullptr, verify);
ngc_lfg_get_bytes(&lfg2, verify, buf_size);
EXPECT_EQ(0, memcmp(buf, verify, matched));
free(verify);
}
free(buf);
}
TEST(LFG, SeedExtractionWithOffset)
{
struct ngc_lfg_ctx lfg;
ngc_lfg_set_seed(&lfg, test_seed);
/* Generate a large buffer starting from a known offset */
size_t offset = 2048; /* simulate a sector-aligned offset */
size_t buf_size = NGC_LFG_K * 4 * 2;
/* Generate and discard the first `offset` bytes */
uint8_t discard[2048];
ngc_lfg_get_bytes(&lfg, discard, offset);
/* Now generate the data we'll test */
uint8_t *buf = (uint8_t *)malloc(buf_size);
ASSERT_NE(nullptr, buf);
ngc_lfg_get_bytes(&lfg, buf, buf_size);
/* Try to extract seed at the given offset */
uint32_t extracted_seed[NGC_LFG_SEED_SIZE];
size_t matched = ngc_lfg_get_seed(buf, buf_size, offset, extracted_seed);
EXPECT_GT(matched, (size_t)0);
if(matched > 0)
{
/* Verify: seed + advance to offset should give same data */
struct ngc_lfg_ctx lfg2;
ngc_lfg_set_seed(&lfg2, extracted_seed);
uint8_t skip[2048];
ngc_lfg_get_bytes(&lfg2, skip, offset);
uint8_t *verify = (uint8_t *)malloc(matched);
ASSERT_NE(nullptr, verify);
ngc_lfg_get_bytes(&lfg2, verify, matched);
EXPECT_EQ(0, memcmp(buf, verify, matched));
free(verify);
}
free(buf);
}
/* ================================================================== */
/* Junk Map Serialization Tests */
/* ================================================================== */
TEST(JunkMap, SerializeDeserializeEmpty)
{
uint8_t *data = NULL;
uint32_t length = 0;
int32_t ret = ngcw_serialize_junk_map(NULL, 0, &data, &length);
EXPECT_EQ(0, ret);
EXPECT_NE(nullptr, data);
EXPECT_EQ(8u, length); /* header only */
NgcwJunkEntry *entries = NULL;
uint32_t count = 0;
uint16_t seed_size = 0;
ret = ngcw_deserialize_junk_map(data, length, &entries, &count, &seed_size);
EXPECT_EQ(0, ret);
EXPECT_EQ(0u, count);
EXPECT_EQ(NGC_LFG_SEED_SIZE, seed_size);
EXPECT_EQ(nullptr, entries);
free(data);
}
TEST(JunkMap, SerializeDeserializeRoundTrip)
{
NgcwJunkEntry entries[3];
entries[0].offset = 0x1000;
entries[0].length = 0x800;
entries[0].partition_index = 0xFFFF;
memset(entries[0].seed, 0xAA, sizeof(entries[0].seed));
entries[1].offset = 0x8000;
entries[1].length = 0x7C00;
entries[1].partition_index = 0;
memset(entries[1].seed, 0xBB, sizeof(entries[1].seed));
entries[2].offset = 0x100000;
entries[2].length = 0x2000;
entries[2].partition_index = 1;
memset(entries[2].seed, 0xCC, sizeof(entries[2].seed));
uint8_t *data = NULL;
uint32_t length = 0;
int32_t ret = ngcw_serialize_junk_map(entries, 3, &data, &length);
EXPECT_EQ(0, ret);
EXPECT_NE(nullptr, data);
/* Expected size: 8 header + 3 * 86 = 266 */
EXPECT_EQ(266u, length);
NgcwJunkEntry *out_entries = NULL;
uint32_t out_count = 0;
uint16_t out_seed = 0;
ret = ngcw_deserialize_junk_map(data, length, &out_entries, &out_count, &out_seed);
EXPECT_EQ(0, ret);
EXPECT_EQ(3u, out_count);
EXPECT_EQ(NGC_LFG_SEED_SIZE, out_seed);
for(int i = 0; i < 3; i++)
{
EXPECT_EQ(entries[i].offset, out_entries[i].offset);
EXPECT_EQ(entries[i].length, out_entries[i].length);
EXPECT_EQ(entries[i].partition_index, out_entries[i].partition_index);
EXPECT_EQ(0, memcmp(entries[i].seed, out_entries[i].seed, sizeof(entries[i].seed)));
}
free(data);
free(out_entries);
}
TEST(JunkMap, DeserializeTruncatedFails)
{
uint8_t truncated[4] = {0x01, 0x00, 0x01, 0x00}; /* incomplete header */
NgcwJunkEntry *entries = NULL;
uint32_t count = 0;
uint16_t seed = 0;
int32_t ret = ngcw_deserialize_junk_map(truncated, sizeof(truncated), &entries, &count, &seed);
EXPECT_NE(0, ret);
}
/* ================================================================== */
/* Junk Regeneration Test */
/* ================================================================== */
TEST(JunkMap, RegenerateMatchesLFG)
{
/* Generate known LFG output */
struct ngc_lfg_ctx lfg;
ngc_lfg_set_seed(&lfg, test_seed);
uint8_t expected[2048];
ngc_lfg_get_bytes(&lfg, expected, sizeof(expected));
/* Build a junk entry for offset 0, length 2048 */
NgcwJunkEntry entry;
entry.offset = 0;
entry.length = 2048;
entry.partition_index = 0xFFFF;
memcpy(entry.seed, test_seed, sizeof(entry.seed));
/* Regenerate */
uint8_t output[2048];
int ret = ngcw_regenerate_junk_sector(&entry, 1, 0, output, 2048);
EXPECT_EQ(0, ret);
EXPECT_EQ(0, memcmp(expected, output, 2048));
}
TEST(JunkMap, RegenerateAtOffset)
{
/* Generate known LFG output, skip first 4096 bytes */
struct ngc_lfg_ctx lfg;
ngc_lfg_set_seed(&lfg, test_seed);
uint8_t discard[4096];
ngc_lfg_get_bytes(&lfg, discard, sizeof(discard));
uint8_t expected[2048];
ngc_lfg_get_bytes(&lfg, expected, sizeof(expected));
/* Build a junk entry starting at offset 4096 within a 0x8000-aligned block.
* The seed's position 0 corresponds to the block start (offset 0),
* not the entry offset. So the entry at offset 4096 with seed for block 0
* means regeneration at disc_offset 4096 advances LFG by 4096 bytes. */
NgcwJunkEntry entry;
entry.offset = 4096;
entry.length = 2048;
entry.partition_index = 0xFFFF;
memcpy(entry.seed, test_seed, sizeof(entry.seed));
/* Regenerate at offset 4096 — the LFG advances by
* disc_offset - (entry.offset & ~0x7FFF) = 4096 - 0 = 4096 bytes */
uint8_t output[2048];
int ret = ngcw_regenerate_junk_sector(&entry, 1, 4096, output, 2048);
EXPECT_EQ(0, ret);
EXPECT_EQ(0, memcmp(expected, output, 2048));
}
TEST(JunkMap, RegenerateNotFoundReturnsError)
{
NgcwJunkEntry entry;
entry.offset = 0x10000;
entry.length = 0x800;
entry.partition_index = 0xFFFF;
memset(entry.seed, 0, sizeof(entry.seed));
uint8_t output[2048];
int ret = ngcw_regenerate_junk_sector(&entry, 1, 0x5000, output, 2048);
EXPECT_EQ(-1, ret); /* not found */
}
/* ================================================================== */
/* Wii Partition Key Map Serialization Tests */
/* ================================================================== */
TEST(WiiCrypto, KeyMapSerializeDeserializeRoundTrip)
{
WiiPartitionRegion regions[2];
regions[0].start_sector = 100;
regions[0].end_sector = 500;
memset(regions[0].key, 0x11, 16);
regions[1].start_sector = 600;
regions[1].end_sector = 1000;
memset(regions[1].key, 0x22, 16);
uint8_t *data = NULL;
uint32_t length = 0;
int32_t ret = wii_serialize_partition_key_map(regions, 2, &data, &length);
EXPECT_EQ(0, ret);
EXPECT_NE(nullptr, data);
EXPECT_EQ(52u, length); /* 4 + 2*24 */
WiiPartitionRegion *out_regions = NULL;
uint32_t out_count = 0;
ret = wii_deserialize_partition_key_map(data, length, &out_regions, &out_count);
EXPECT_EQ(0, ret);
EXPECT_EQ(2u, out_count);
for(int i = 0; i < 2; i++)
{
EXPECT_EQ(regions[i].start_sector, out_regions[i].start_sector);
EXPECT_EQ(regions[i].end_sector, out_regions[i].end_sector);
EXPECT_EQ(0, memcmp(regions[i].key, out_regions[i].key, 16));
}
free(data);
free(out_regions);
}
TEST(WiiCrypto, KeyMapInvalidRangeFails)
{
WiiPartitionRegion region;
region.start_sector = 500;
region.end_sector = 100; /* invalid: start > end */
memset(region.key, 0, 16);
uint8_t *data = NULL;
uint32_t length = 0;
wii_serialize_partition_key_map(&region, 1, &data, &length);
WiiPartitionRegion *out = NULL;
uint32_t cnt = 0;
int32_t ret = wii_deserialize_partition_key_map(data, length, &out, &cnt);
EXPECT_NE(0, ret);
free(data);
}
/* ================================================================== */
/* Wii Group Encrypt/Decrypt Round-Trip Test */
/* ================================================================== */
TEST(WiiCrypto, GroupEncryptDecryptRoundTrip)
{
uint8_t key[16];
memset(key, 0x42, 16);
/* Create synthetic hash block and data */
uint8_t hash_block[WII_GROUP_HASH_SIZE];
uint8_t data_in[WII_GROUP_DATA_SIZE];
for(int i = 0; i < WII_GROUP_HASH_SIZE; i++) hash_block[i] = (uint8_t)(i & 0xFF);
for(int i = 0; i < WII_GROUP_DATA_SIZE; i++) data_in[i] = (uint8_t)((i * 7 + 3) & 0xFF);
/* Encrypt */
uint8_t encrypted[WII_GROUP_SIZE];
wii_encrypt_group(key, hash_block, data_in, encrypted);
/* Encrypted should differ from plaintext */
EXPECT_NE(0, memcmp(encrypted, hash_block, WII_GROUP_HASH_SIZE));
/* Decrypt */
uint8_t hash_out[WII_GROUP_HASH_SIZE];
uint8_t data_out[WII_GROUP_DATA_SIZE];
wii_decrypt_group(key, encrypted, hash_out, data_out);
/* Should match originals */
EXPECT_EQ(0, memcmp(hash_block, hash_out, WII_GROUP_HASH_SIZE));
EXPECT_EQ(0, memcmp(data_in, data_out, WII_GROUP_DATA_SIZE));
}
TEST(WiiCrypto, SectorKeyLookup)
{
WiiPartitionRegion regions[2];
regions[0].start_sector = 10;
regions[0].end_sector = 100;
memset(regions[0].key, 0xAA, 16);
regions[1].start_sector = 200;
regions[1].end_sector = 300;
memset(regions[1].key, 0xBB, 16);
/* Sector in first partition's header (plaintext) */
EXPECT_EQ(nullptr, wii_get_sector_key(regions, 2, 10 * WII_LOGICAL_PER_GROUP));
/* Sector in first partition's data */
const uint8_t *key = wii_get_sector_key(regions, 2, 11 * WII_LOGICAL_PER_GROUP);
ASSERT_NE(nullptr, key);
EXPECT_EQ(0xAA, key[0]);
/* Sector in second partition */
key = wii_get_sector_key(regions, 2, 201 * WII_LOGICAL_PER_GROUP);
ASSERT_NE(nullptr, key);
EXPECT_EQ(0xBB, key[0]);
/* Sector outside any partition (plaintext) */
EXPECT_EQ(nullptr, wii_get_sector_key(regions, 2, 150 * WII_LOGICAL_PER_GROUP));
/* Encrypted check */
EXPECT_TRUE(wii_is_sector_encrypted(regions, 2, 50 * WII_LOGICAL_PER_GROUP));
EXPECT_FALSE(wii_is_sector_encrypted(regions, 2, 150 * WII_LOGICAL_PER_GROUP));
}