acdream/tests/AcDream.Core.Net.Tests/Cryptography/IsaacRandomTests.cs
Erik 293584d6e8 feat(net): AcDream.Core.Net scaffold + ISAAC keystream (Phase 4.1)
First step of Phase 4 (networking). Adds a new AcDream.Core.Net project
for the AC UDP protocol implementation and a matching AcDream.Core.Net.Tests
project. Keeps networking isolated from rendering and the dat layer,
which also keeps the AGPL-reference-material hygiene cleaner.

AcDream.Core.Net/NOTICE.md documents the attribution policy: we read
ACE's AGPL network code (and holtburger's Rust ac-protocol crate) to
understand AC's wire format, but we reimplement everything in acdream's
own style. Wire-format facts aren't copyrightable; specific code is.

This commit adds one component: IsaacRandom — AC's variant of Bob
Jenkins' ISAAC PRNG, used to XOR a keystream into the CRC field of
every outbound packet for authentication. Clean-room reimplementation
based on reading:
  - references/ACE/Source/ACE.Common/Cryptography/ISAAC.cs (AGPL oracle)
  - Bob Jenkins' public ISAAC algorithm description

Implementation notes:
  - 256 uint32 mm[] state, 256 uint32 rsl[] output buffer, a/b/c regs
  - Initialize() runs 4 golden-ratio Mix() warmup rounds then two fold-in
    passes over rsl[] and mm[] (fresh instance → both start as zeroes)
  - AC variant: seed is exactly 4 bytes, interpreted as little-endian
    uint32 assigned to a = b = c before the first Scramble()
  - Scramble() produces 256 output words in one pass; Next() consumes
    them backwards from offset 255 → 0, re-scrambling at offset -1
  - Test seed 0x12345678 matches ACE's reference output byte-for-byte
    across the first 16 values (golden vectors transcribed from a
    throwaway oracle harness that compiled ACE's ISAAC.cs and printed
    its output; the harness was deleted after extracting the values)

Tests (5, all passing):
  - Next_Seed12345678_MatchesAceGoldenVectors: 16 golden uint32 values
  - Next_TwoInstancesSameSeed_ProduceIdenticalSequence: 1000 outputs
  - Next_DifferentSeeds_ProduceDifferentFirstOutput
  - Next_512Calls_SpansTwoScrambleBatches: >400 distinct values in 512
    outputs (catches all-zero / stuck-at-one bugs at scramble boundary)
  - Ctor_ShortSeed_Throws

Both test projects still green: 77 core + 5 net = 82/82.

Phase 4.2 (packet framing + checksum) next.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:14:28 +02:00

79 lines
2.8 KiB
C#

using AcDream.Core.Net.Cryptography;
namespace AcDream.Core.Net.Tests.Cryptography;
public class IsaacRandomTests
{
/// <summary>
/// Golden vectors derived from ACE's reference ISAAC implementation
/// (references/ACE/Source/ACE.Common/Cryptography/ISAAC.cs) with seed
/// bytes [0x78, 0x56, 0x34, 0x12] (= 0x12345678 as little-endian uint32).
/// Ran a throwaway oracle harness that compiles ACE's ISAAC.cs + a Main
/// that calls Next() 16 times and prints the values; the numbers below
/// are factual outputs of that algorithm, not copied code.
/// </summary>
private static readonly uint[] GoldenVectorsSeed12345678 =
{
0xFDD4AFEBu, 0x398311D2u, 0xC71829A4u, 0xB19DF96Au,
0x7ED4A1E7u, 0xB718A446u, 0x5FA77DF1u, 0x5CB793D2u,
0x52C95BACu, 0xA8F8FD7Eu, 0x18582C04u, 0xD257F506u,
0x4D11A3F0u, 0x919DE7A7u, 0x1D809C6Bu, 0xE97F65F8u,
};
[Fact]
public void Next_Seed12345678_MatchesAceGoldenVectors()
{
var seed = new byte[] { 0x78, 0x56, 0x34, 0x12 };
var isaac = new IsaacRandom(seed);
for (int i = 0; i < GoldenVectorsSeed12345678.Length; i++)
{
Assert.Equal(GoldenVectorsSeed12345678[i], isaac.Next());
}
}
[Fact]
public void Next_TwoInstancesSameSeed_ProduceIdenticalSequence()
{
var seed = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };
var a = new IsaacRandom(seed);
var b = new IsaacRandom(seed);
for (int i = 0; i < 1000; i++)
{
Assert.Equal(a.Next(), b.Next());
}
}
[Fact]
public void Next_DifferentSeeds_ProduceDifferentFirstOutput()
{
var a = new IsaacRandom(new byte[] { 1, 0, 0, 0 });
var b = new IsaacRandom(new byte[] { 2, 0, 0, 0 });
Assert.NotEqual(a.Next(), b.Next());
}
[Fact]
public void Next_512Calls_SpansTwoScrambleBatches()
{
// Two full scramble rounds (256 outputs each). Tests that the
// scramble boundary at offset 0 → offset 255 doesn't emit garbage or
// repeat values trivially.
var isaac = new IsaacRandom(new byte[] { 0x01, 0x02, 0x03, 0x04 });
var outputs = new uint[512];
for (int i = 0; i < 512; i++) outputs[i] = isaac.Next();
// Not a statistical test — just catch obvious bugs like "all zero"
// or "stuck at one value". A healthy PRNG produces far more than
// 100 distinct values in 512 calls.
var distinct = new HashSet<uint>(outputs);
Assert.True(distinct.Count > 400,
$"Expected >400 distinct values in 512 outputs, got {distinct.Count}");
}
[Fact]
public void Ctor_ShortSeed_Throws()
{
Assert.Throws<ArgumentException>(() => new IsaacRandom(new byte[] { 1, 2, 3 }));
}
}