Skip to content
academia.sh

Lesson 14 / 17

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close