/* * 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 #include #include #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(®ion, 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)); }