mirror of
https://github.com/aaru-dps/libaaruformat.git
synced 2026-04-05 21:51:03 +00:00
456 lines
14 KiB
C++
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(®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));
|
|
}
|