---
title: Randomness
source: 'https://academia.sh/en/courses/secure-coding/randomness'
course: 'Secure Coding'
language: en
updated: '2026-09-26T03:02:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares the weak generator with the cryptographically secure generator on the same draw set and measures how many values can be precomputed once the seed is known'
    - "Compares the distinct-value count with the theoretical expectation to derive the generator's real output width"
    - 'Reads the bias of remainder-based range mapping from arithmetic first, then confirms it in the run'
    - 'Counts the token-generating points and separates out how many carry a security consequence from the weak generator'
---

# Randomness

A same-scale comparison of the weak generator and the cryptographically secure generator: when the seed is known, all ten thousand values can be precomputed; out of ten thousand four-digit codes, only 903 come out distinct; mapping a byte to a digit by remainder produces a 1.04 bias; and six of the nine token-generation points call the weak generator.

Every measurement in the previous lesson took one thing as given: that the source producing the
key and the starting value is unpredictable. That source is itself a call point in the codebase,
and it does not appear as a separate line in the fourteen-point cryptography inventory. The field
application's upload token, the portal's password-reset link, and the four-digit verification
code all share the same question: can a third party precompute the value that gets generated?

The question is not one of quality. A generator is either deterministic or it is not, and there
is no middle ground. The measurements below run two generators on the same draw set: one is a
hand-written linear congruential generator, the other is the cryptographically secure generator
inside `node:crypto`.

## Two Generators, One Measure

**SC4 — the weak generator is deterministic; its state advances from the seed by a single
arithmetic step, so a party that knows the seed can precompute every value it will produce.**
The cryptographically secure generator has no such seed item to guess; its output is fed from
the operating system's entropy pool.

**SC5 — the number of distinct values in a draw set gives the generator's real output width**;
if the number falls below the theoretical expectation, the generator is not using its nominal
width. **SC6 — mapping a random byte to a range by remainder does not give a uniform
distribution if the range width does not evenly divide the number of values a byte can take.**

```js
// random/generator.mjs — compares the weak generator with the cryptographically secure
// generator on the same scale. The weak generator is our own hand-written linear congruential
// generator; the seed is visible. Because the cryptographic generator's output changes from run
// to run, the values read from it are not printed directly; they go through a band check derived
// from the input.
import { randomBytes, randomInt } from 'node:crypto';

const SEED = 20260301;
const DRAW = 10000;
const makeGenerator = (seed) => {
  let state = seed;
  return () => (state = (state * 1103515245 + 12345) % 2 ** 31);
};

// 1) How many values can be precomputed once the seed is known.
const a = makeGenerator(SEED), b = makeGenerator(SEED);
let same = 0;
for (let i = 0; i < DRAW; i++) if (a() === b()) same += 1;
const g1 = randomBytes(4).readUInt32BE(0), g2 = randomBytes(4).readUInt32BE(0);
console.log(`SC4 — precomputed value when seed ${SEED} is known: ${same}/${DRAW}`);
console.log(`  the cryptographic generator has no same-seed item; are two calls equal: `
  + `${g1 === g2 ? 'yes' : 'no'}`);
console.log(`  candidate seeds per day for a generator seeded from a second-resolution clock: ${24 * 3600}`);

// 2) Four-digit verification code: weak generator's low digits vs. the secure generator.
const code = makeGenerator(SEED);
const weak = new Set();
for (let i = 0; i < DRAW; i++) weak.add(code() % 10000);
const secure = new Set();
for (let i = 0; i < DRAW; i++) secure.add(randomInt(0, 10000));
const expected = Math.round(10000 * (1 - (1 - 1 / 10000) ** DRAW));
console.log(`\nSC5 — ${DRAW} four-digit codes, theoretical expected distinct value: ${expected}`);
console.log(`  weak generator, distinct code          : ${weak.size}`);
console.log(`  secure generator, distinct code in band: `
  + `${Math.abs(secure.size - expected) < 120 ? 'yes' : 'no'} (${expected} +/- 120)`);

// 3) Bias of deriving a digit from a single byte: exact count first, then the run.
const ways = Array.from({ length: 10 }, (_, r) =>
  Array.from({ length: 256 }, (_, x) => x).filter((x) => x % 10 === r).length);
const RATIO = ways[0] / ways[9];
const DRAW2 = 1000000;
const biased = Array(10).fill(0), rejectionSampled = Array(10).fill(0);
let rejected = 0;
for (const x of randomBytes(DRAW2)) {
  biased[x % 10] += 1;                        // direct modulo: 0-5 come out too high
  if (x < 250) rejectionSampled[x % 10] += 1; else rejected += 1;   // rejection sampling
}
const ratioOf = (s) => (s.slice(0, 6).reduce((p, q) => p + q, 0) / 6)
  / (s.slice(6).reduce((p, q) => p + q, 0) / 4);
const inBand = (o, target) => (Math.abs(o - target) < 0.008 ? 'yes' : 'no');
const expectedRejected = (DRAW2 * 6) / 256;
console.log(`\nSC6 — remainder of 256 byte values divided by 10: 0-5 has ${ways[0]} ways, `
  + `6-9 has ${ways[9]} ways, ratio ${RATIO.toFixed(4)}`);
const label = (name) => `  ${name.padEnd(46)}: `;
console.log(label(`${DRAW2} bytes, direct modulo, ratio in band`)
  + `${inBand(ratioOf(biased), RATIO)} (${RATIO.toFixed(4)} +/- 0.008)`);
console.log(label('same bytes, rejection sampling, ratio in band')
  + `${inBand(ratioOf(rejectionSampled), 1)} (1.0000 +/- 0.008)`);
console.log(label('rejected byte band')
  + `${Math.abs(rejected - expectedRejected) < 600 ? 'yes' : 'no'} (${expectedRejected} +/- 600)`);
```

```
SC4 — precomputed value when seed 20260301 is known: 10000/10000
  the cryptographic generator has no same-seed item; are two calls equal: no
  candidate seeds per day for a generator seeded from a second-resolution clock: 86400

SC5 — 10000 four-digit codes, theoretical expected distinct value: 6321
  weak generator, distinct code          : 903
  secure generator, distinct code in band: yes (6321 +/- 120)

SC6 — remainder of 256 byte values divided by 10: 0-5 has 26 ways, 6-9 has 25 ways, ratio 1.0400
  1000000 bytes, direct modulo, ratio in band   : yes (1.0400 +/- 0.008)
  same bytes, rejection sampling, ratio in band : yes (1.0000 +/- 0.008)
  rejected byte band                            : yes (23437.5 +/- 600)
```

## What the Seed Gives Away

The first measurement is not a ratio but an equality: two generators starting from the same seed
produce **all ten thousand** of the ten thousand draws in the same order. The number here is
unimportant; what matters is that it is the full count. The weak generator is not a probability
distribution but a function — given a seed, its output sequence is fixed. The same measurement
cannot be made on the cryptographic generator, because there is no seed item to compare.

The third line explains why the seed is a separate item. If the generator is seeded from a
clock — from a second-resolution value at application startup — the number of candidate seeds in
a one-day window is **86,400**. This is a ceiling independent of how long the output looks: a
token that appears to be a hundred and twenty-eight bits can be carrying a guessing space of only
sixteen and a half bits. Length is a visible quantity; the seed space is not, which is why
looking at a token's length during code review is misleading.

## How Many Values a Code Has

Predictability is not a yes-or-no property but a number, and that number comes from three
separate places: the seed space, the output space the generator actually traverses, and the way
the output is mapped onto the desired range. The three are independent of each other, and the
smallest one sets the ceiling. The first measurement counted the first of these; the second and
third measurements count the remaining two.

The second measurement says something even when the seed is not known at all. When ten thousand
four-digit codes are drawn, 6,321 distinct values are theoretically expected; when ten thousand
balls are thrown into ten thousand boxes, some boxes get more than one ball. The secure generator
hits this expectation. The weak generator produces only **903** distinct codes in the same
draw — exactly one-seventh of what is expected.

The reason lies in how the code is produced: the four-digit code is taken from the **low digits**
of the generator's output, and in this generator the low digits repeat on a much shorter cycle
than the high ones. Nominally there is a space of ten thousand values; in reality only around
nine hundred values are in circulation. The security consequence of this number is direct: for a
party trying to guess the verification code, the space is not ten thousand but nine hundred, and
reaching it does not require knowing the seed — only collecting codes.

## The Bias of Deriving a Digit

The third measurement shows that even the secure generator can be called incorrectly. A random
byte takes a value between 0 and 255; if code that wants a base-ten digit takes the remainder of
a division, the remainders of the 256 values are not evenly distributed. The remainders zero
through five get **26** byte values, six through nine get **25**. The ratio is 1.04, and this
number is read from arithmetic, not from measurement; the run only confirms it. The ratio
measured over one million bytes comes out inside the band.

When the same bytes are run through rejection sampling — discarding any byte greater than or
equal to two hundred fifty and drawing a new one — the ratio drops to one. The cost is on the
third line: **23,437.5** out of one million bytes get rejected, that is, one in every
forty-three. An extra two-and-a-half percent of draws removes a four percent skew in the first
digit of a four-digit code. What the measurement says here is this: calling the secure generator
is not enough; the line that **maps** its output onto the range is also part of the defense, and
that line is usually written outside the central pattern.

## Token-Generating Points

It is time to return to the inventory measure. In the fictional measurement network there are
nine points that generate a token, an identifier, or a filename. The table below places each
point's nominal width, generator choice, and the **effective** width bounded by the seed space
side by side.

```js
// random/token.mjs — inventory of the points that generate a token in the fictional
// measurement network (model). [point, carries, generator, nominal bits]. The weak
// generator's guessing space is bounded by the seed space: a generator seeded from a
// second-resolution clock has 86400 candidate seeds per day.
const points = [
  ['portal/session-id', 'identity', 'secure', 128],
  ['portal/password-reset', 'authorization', 'weak', 128],
  ['portal/verification-code', 'authorization', 'weak', 13.3],
  ['portal/invoice-link', 'authorization', 'secure', 128],
  ['field/upload-name', 'authorization', 'weak', 64],
  ['field/work-order-id', 'identity', 'weak', 32],
  ['ingestion/package-id', 'none', 'weak', 32],
  ['report/filename', 'authorization', 'weak', 64],
  ['tracing/request-id', 'none', 'secure', 64],
];

const SEED_SPACE = Math.log2(24 * 3600);
const effective = ([, , generator, bits]) => (generator === 'weak' ? Math.min(bits, SEED_SPACE) : bits);
const count = (rule) => points.filter(rule).length;
const write = (widths, ...values) => console.log(values.map((v, i) =>
  (widths[i] < 0 ? String(v).padEnd(-widths[i]) : String(v).padStart(widths[i]))).join(''));

const D = [-26, 14, 10, 13, 15];
write(D, 'point', 'carries', 'generator', 'nominal bits', 'effective bits');
for (const n of points) write(D, n[0], n[1], n[2], n[3], effective(n).toFixed(1));

const weak = points.filter(([, , g]) => g === 'weak');
const consequential = weak.filter(([, c]) => c !== 'none');
console.log(`\npoints ${points.length}, weak generator ${weak.length}, `
  + `carrying identity or authorization ${consequential.length}, not carrying ${weak.length - consequential.length}`);
console.log(`total nominal bits ${points.reduce((a, n) => a + n[3], 0).toFixed(1)}, `
  + `total effective bits ${points.reduce((a, n) => a + effective(n), 0).toFixed(1)}`);
console.log(`largest loss: ${weak.map((n) => [n[0], n[3] - effective(n)])
  .sort((x, y) => y[1] - x[1])[0].map((v) => (typeof v === 'number' ? v.toFixed(1) : v)).join(' ')} bits`);

// Two new token-generating points are added: one where the pattern is optional, one where it is mandatory.
const newPoints = ['portal/share-link', 'field/offline-queue-id'];
const E = [-46, 8, 12, 12, 19];
console.log('\ntwo new token-generating points are added:');
write(E, 'arrangement', 'points', 'in pattern', 'uncovered', 'new gap candidate');
write(E, 'pattern exists, direct call allowed', points.length + newPoints.length,
  count(([, , g]) => g === 'secure'), count(([, , g]) => g === 'weak') + newPoints.length, newPoints.length);
write(E, 'single identity-generating function exposed', points.length + newPoints.length,
  points.length + newPoints.length, 0, 0);
console.log(`points that must be moved first in the second arrangement: ${weak.length}`);
```

```
point                            carries generator nominal bits effective bits
portal/session-id               identity    secure          128          128.0
portal/password-reset      authorization      weak          128           16.4
portal/verification-code   authorization      weak         13.3           13.3
portal/invoice-link        authorization    secure          128          128.0
field/upload-name          authorization      weak           64           16.4
field/work-order-id             identity      weak           32           16.4
ingestion/package-id                none      weak           32           16.4
report/filename            authorization      weak           64           16.4
tracing/request-id                  none    secure           64           64.0

points 9, weak generator 6, carrying identity or authorization 5, not carrying 1
total nominal bits 653.3, total effective bits 415.3
largest loss: portal/password-reset 111.6 bits

two new token-generating points are added:
arrangement                                     points  in pattern   uncovered  new gap candidate
pattern exists, direct call allowed                 11           3           8                  2
single identity-generating function exposed         11          11           0                  0
points that must be moved first in the second arrangement: 6
```

**Six** of the nine points call the weak generator, and **five** of those carry identity or
authorization. The sixth — the ingestion service's package id — is the one point whose
predictability carries no consequence; no door opens with that identifier. This distinction
matters when counting coverage: the number of uncovered points is six, the number of points
with a security consequence is five, and the work of carrying the pattern starts from those five.

The effective-bits column shows in a single line why looking at nominal width is misleading. The
password-reset link produces a string of a hundred and twenty-eight bits; once the seed space is
taken into account, only **16.4 bits** remain, producing a loss of **111.6 bits** at a single
point. Of the 653.3 nominal bits in total, 415.3 survive. The verification code is an interesting
exception: because its nominal width is already 13.3 bits, changing the generator gains it no
bits. That point's defense is not in the generator but in the rule that limits the number of
attempts — and that rule is outside the scope of this lesson.

This inventory intersects with the previous lesson's fourteen-point cryptography inventory at one
place. There, two points had been flagged with the "fixed starting value" flaw; a fixed starting
value means **never calling** the generator at all, and it reduces that item's guessing space to
zero. The same generator shows up in both inventories at once: in one place it produces a token,
in the other it produces an encryption parameter. Carrying the two patterns separately means
closing the same call twice; tying both inventories to a single "generates an unpredictable
value" function closes it once, and covers every new point added after that in both at the same
time.

## When a New Point Is Added

The last table adds two new token-generating points: the portal's share link and the field
application's offline queue id. In the first arrangement there is a pattern that calls the secure
generator, but it is not mandatory. The number of points rises from nine to eleven, the number of
points in the pattern **stays at three**, the number of uncovered points rises to eight, and the two new
points become direct gap candidates. Whoever writes a new point can choose their own generator
even if they know the pattern exists; coverage drops with every addition.

In the second arrangement, a single identity-generating function is exposed, and the generator
call appears nowhere else. All eleven of the eleven points pass through the pattern, and the
new-gap-candidate count is zero. The cost is on the last line again: moving to this arrangement
requires carrying over all six of the six points that used the weak generator. What keeps
coverage constant is not the quality of the pattern but the fact that it is the **only path**.

## Summary

- Two weak generators starting from the same seed produced all ten thousand of the ten thousand
  draws in the same order; the weak generator is a function, not a distribution.
- A generator seeded from a second-resolution clock has 86,400 candidate seeds per day; this sets
  a guessing ceiling independent of the output's length.
- The theoretical expectation for ten thousand four-digit codes is 6,321 distinct values, but the
  weak generator produced only 903 distinct codes; reading from the low digits narrowed the
  nominal space tenfold.
- Mapping a byte to a digit by remainder leaves 26 ways for 0-5 and 25 ways for 6-9, producing a
  1.04 bias; rejection sampling brings this down to one, at a cost of one in forty-three bytes.
- Six of the nine token-generating points call the weak generator, and five carry identity or
  authorization; the password-reset link alone accounts for a loss of 111.6 bits.
- When the pattern is not mandatory, two new points leave coverage at three and add two gap
  candidates; when the generator call is closed into a single function, eleven of the eleven
  points are covered.

## Next Step

These two lessons measured how the key and the token get generated, and both rested on the same
assumption: that the generated value stays only in the process that produced it. But a portion of
those values was never generated at all — it was written as a fixed literal in a configuration
line, and from there it entered version control. The next lesson counts this item over a
fictional repository history: how many commits a secret stayed in, how many days it stood exposed
from the moment it was added until the moment it was rotated, how much of that history cleanup
rescues, and how many false positives a rule that scans for the pattern produces.
