I have observed an RNG implementation using an HMAC with SHA-256 that takes serverSeed, clientSeed, nonce, and cursor as inputs. For the specific scenario, the cursor remains zero. The key detail is that the host provides the SHA-256 hash of the serverSeed to the client before the client chooses their clientSeed. After games are played, the user may request the original serverSeed from the host as soon as they change to a new client seed. Heres the RNG implementation:
function byteGenerator({ serverSeed, clientSeed, nonce, cursor }) {
let currentRoundCursor = 0;
let currentRound = 0;
const hmac = createHmac('sha256', serverSeed);
hmac.update(`${clientSeed}:${nonce}:${currentRound}`);
const buffer = hmac.digest();
// Yields 32 bytes of output
while (currentRoundCursor < 32) {
yield Number(buffer[currentRoundCursor]);
currentRoundCursor += 1;
} }
function generateFloats({ serverSeed, clientSeed, nonce, cursor, count }) { const rng = byteGenerator({ serverSeed, clientSeed, nonce, cursor }); const bytes = [];
// Collect `count * 4` bytes while (bytes.length < count * 4) {
bytes.push(rng.next().value); }
// Converts chunks of 4 bytes into floats return _.chunk(bytes, 4).map(bytesChunk =>
bytesChunk.reduce((result, value, i) => {
const divider = 256 ** (i + 1);
return result + (value / divider);
}, 0) ); }
Example: For a game output in the range [0.00, 100.00], one set of 4 bytes (113, 141, 68, 253) leads to a value derived mostly from the first byte:
(113 / 256^1) ≈ 0.44140625
(141 / 256^2) ≈ 0.002151489258
(68 / 256^3) ≈ 0.000004053116
(253 / 256^4) ≈ 0.000000058906
Total ≈ 0.44356185128 * 10001 ≈ 4436.062074649
RNG output ≈ 44.36
Issues Identified:
1 Predictability: Once the client has chosen their seed, the host (knowing the serverSeed and incrementing the nonce) can predict all future outcomes for that seed pair.
2 Single Byte Dominance: Although the code claims to use 4 bytes from the HMAC output for each result, about 99.5% of each final number is determined by the first byte. The host only needs to influence one byte of the HMAC output to control results.
3 Hash Choice and Vulnerabilities: The host insists on using only SHA-256 or SHA-512. Both are known to be vulnerable to length extension attacks, suggesting this property could be leveraged to manipulate outcomes.
In tests with tens of millions of data points taken from the host’s platform, the results show severe statistical anomalies (e.g., outcomes with p-values < 1.0e-500), clearly indicating deliberate manipulation. Interestingly, when using truly random seeds in a controlled environment, the distribution appears uniform and fair. But with the host’s chosen seeds, the results heavily favor the host. When the results are actively being manipulated, they tend to bunch together. So for example, the first byte will be identical or near identical for three, four, sometimes five sequential output hashes. Lastly, it should be noted that before a user chooses a seed, they are provided suggested ones, which appear to facilitate even easier manipulation. the suggested seeds are always 10 characters in length and contain very few hex digits if any at all, examples: P3hmVYpIM7 l-XiEvH-4K H4UHHqM6N3. Perhaps encoding plays a role.
Question:
Given that the server provides the SHA-256 hash of the serverSeed before the client chooses the clientSeed, and eventually reveals the serverSeed so users can verify the outcomes, how can the host still manage to manipulate results efficiently? Considering that each result is overwhelmingly determined by the first output byte, and given the potential relevance of length extension attacks on SHA-256 and SHA-512, how could a bad actor use these properties to influence the RNG outputs in practice? Assume that the host has substantial financial and computational resources at their disposal.