From 34c4ca02378296fde309d018c1fd5fc875f7492b Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 1 Jun 2026 11:35:53 -0400 Subject: [PATCH] fix(battle-node): NodeCrypto.GenerateKey masks rand source with & 0xF --- SVSim.BattleNode/Wire/NodeCrypto.cs | 9 +++++---- .../BattleNode/Wire/NodeCryptoTests.cs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/SVSim.BattleNode/Wire/NodeCrypto.cs b/SVSim.BattleNode/Wire/NodeCrypto.cs index af749ed..98cc2d3 100644 --- a/SVSim.BattleNode/Wire/NodeCrypto.cs +++ b/SVSim.BattleNode/Wire/NodeCrypto.cs @@ -12,9 +12,10 @@ public static class NodeCrypto { /// /// Generate a fresh 32-char key for server-initiated encryption. - /// Calls 32 times expecting a value in [0,15], - /// formats each as a single hex char, then base64-encodes the resulting 32-char ASCII - /// string and truncates to 32 chars. + /// Calls 32 times; the result is masked with + /// & 0xF so a misbehaving caller that returns a larger int still produces + /// exactly one hex digit per iteration (the internal contract is "32 hex chars"). + /// The 32-char ASCII string is then base64-encoded and truncated to 32 chars. /// /// /// Differs from the client's Cryptographer.generateKeyString in input shape: @@ -29,7 +30,7 @@ public static class NodeCrypto var sb = new StringBuilder(32); for (var i = 0; i < 32; i++) { - sb.Append(randHexDigit().ToString("x")); + sb.Append((randHexDigit() & 0xF).ToString("x")); } var ascii = Encoding.ASCII.GetBytes(sb.ToString()); return Convert.ToBase64String(ascii).Substring(0, 32); diff --git a/SVSim.UnitTests/BattleNode/Wire/NodeCryptoTests.cs b/SVSim.UnitTests/BattleNode/Wire/NodeCryptoTests.cs index 202ae9d..6695deb 100644 --- a/SVSim.UnitTests/BattleNode/Wire/NodeCryptoTests.cs +++ b/SVSim.UnitTests/BattleNode/Wire/NodeCryptoTests.cs @@ -42,6 +42,21 @@ public class NodeCryptoTests Assert.Throws(() => NodeCrypto.DecryptForNode("tooshort")); } + [Test] + public void GenerateKey_RandSourceReturnsOutOfRange_MasksToLowFourBits() + { + // Defensive: misbehaving caller returns 31 (binary 11111). Internal contract is + // "each call produces one hex digit"; without masking, 31 widens to "1f" (two + // chars) which throws off the base64 length math. After masking with & 0xF, + // 31 becomes 15 — one hex digit "f". This pair distinguishes because + // base64-of-repeated-"1f" and base64-of-repeated-"f" differ at every position. + var keyFromThirtyOne = NodeCrypto.GenerateKey(() => 31); + var keyFromFifteen = NodeCrypto.GenerateKey(() => 15); + + Assert.That(keyFromThirtyOne, Is.EqualTo(keyFromFifteen), + "31 & 0xF == 15 — GenerateKey must mask out-of-range bits, not let them widen the hex digit."); + } + [Test] public void EncryptForNode_FixedVector_ProducesStableOutput() {