Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Applied Cryptography for Developers  Module 3 – Randomness and secret values

Secure random numbers in browsers, Node and Python

Make keys, salts, nonces and tokens with a CSPRNG in browsers, Node and Python, respect the 65,536-byte limit, and keep seeds out of production code.

  • Beginner
  • 25 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8, Pyodide 314.0.7 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Generate keys, salts, nonces and tokens with a CSPRNG in the browser, Node and Python
  • Explain why Math.random and Python's random module must not produce secrets
  • Handle the 65,536-byte limit of getRandomValues and choose the right call for each runtime
  • Write tests that inject a fixed random source while production code takes no seed

Before you start

What you’ll learn · Before you start

What you will learn

  • Generate keys, salts, nonces and tokens with a CSPRNG in the browser, Node and Python
  • Explain why Math.random and Python's random module must not produce secrets
  • Handle the 65,536-byte limit of getRandomValues and choose the right call for each runtime
  • Write tests that inject a fixed random source while production code takes no seed

Before you start

On this page

A password-reset token made with Python’s random module and seeded with the clock looks like 32 random hex digits, yet a test that tries every second of the last hour rebuilds it in 3,601 guesses. Keys, salts, nonces and tokens need values that nobody can predict, and every platform ships a generator for exactly that job, next to a faster one that must never make a secret.

This lesson shows where secure random bytes come from, how to get them in browsers, Node and Python, what the 65,536-byte limit of crypto.getRandomValues means for your code, how many bytes a secret needs, and how to test code that uses randomness without ever putting a seed into production. A two-step code lab at the end builds a token function and a buffer filler.

Random-looking output is not enough for secrets

A pseudo-random number generator computes every output from a hidden state with a fixed formula, starting from a seed (of a generator): The starting value of a pseudo-random number generator. The same seed always gives the same sequence, so a seed that can be guessed, such as the clock, makes every output guessable.. Python’s random module is one: its documentation calls the Mersenne Twister at its core completely deterministic and completely unsuitable for cryptographic purposes, and warns that its generators should not be used for security purposes. MDN says not to use Math.random() for anything related to security and points to crypto.getRandomValues() instead. Both are built for simulations and games, where speed, good statistics and a repeatable run matter.

The program below makes the classic mistake on purpose. It seeds random.Random with the time a reset token is issued, then runs the check a test suite should run: can the token be rebuilt from any second of the hour before it? It runs the same check on a token from secrets.token_hex(16). Run it, then change ISSUED_AT and run it again:

A token seeded with the clock, and the check that catches it Python · predictable/predictable.py
"""A reset token seeded with the clock, and the check that catches it: rebuild the token from every second of an hour."""
import random
import secrets

ISSUED_AT = 1_700_000_000  # a clock reading in seconds, fixed so that the run repeats


def weak_token(clock):
    """The bug: a generator for simulations, seeded with the time the token was issued."""
    rng = random.Random(clock)
    return rng.getrandbits(128).to_bytes(16, "big").hex()


def rebuilt_from(token, seeds):
    """The check: how many of these seeds give exactly this token?"""
    return sum(1 for seed in seeds if weak_token(seed) == token)


hour = range(ISSUED_AT - 3600, ISSUED_AT + 1)  # every second of the hour before the token was issued
weak = weak_token(ISSUED_AT)
ones = bin(int(weak, 16)).count("1")
print(f"weak token:   {weak}")
print(f"  1-bits: {ones} of 128, so it looks random")
print(f"  seeds of the last hour that rebuild it: {rebuilt_from(weak, hour)} of {len(hour):,}")

strong = secrets.token_hex(16)
print(f"strong token: {len(strong)} hex characters from secrets.token_hex(16)")
print(f"  seeds of the last hour that rebuild it: {rebuilt_from(strong, hour)} of {len(hour):,}")

# Same seed, same sequence: right for a simulation you want to repeat, wrong for anything secret.
a, b = random.Random(42), random.Random(42)
same = all(a.random() == b.random() for _ in range(1000))
print(f"two generators seeded with 42 agree on 1,000 draws: {same}")

Output

weak token:   6b4b2d0c310f9fcb579cfc715210160b
  1-bits: 62 of 128, so it looks random
  seeds of the last hour that rebuild it: 1 of 3,601
strong token: 32 hex characters from secrets.token_hex(16)
  seeds of the last hour that rebuild it: 0 of 3,601
two generators seeded with 42 agree on 1,000 draws: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 predictable.py

The weak token has 62 one-bits out of 128, as a random value should, and nothing about its digits gives it away. One of the 3,601 seeds rebuilds it exactly, because the seed was the only unknown, and a clock reading is easy to narrow down. The secrets token survives the same check: there is no seed to find. The last line is the other side of the coin: two generators seeded with 42 agree on all 1,000 draws, which is exactly what a simulation you want to repeat needs and exactly what a secret must never allow.

Seeding with the time or a process ID is common enough to have its own entry in MITRE’s weakness list, CWE-337. One of its examples is a famous case: a change made only in Debian’s OpenSSL package left the generator predictable, so SSH, OpenVPN, DNSSEC and certificate keys made on those systems could be guessed (Debian advisory DSA-1571, CVE-2008-0166). CWE-337 notes that the generator had been left seeded with little more than the process ID.

A better seed does not make Math.random or random safe

Wrong: random.seed(os.urandom(32)), then random.getrandbits(128) for a token. The seed is now unpredictable, but the generator is still a fast formula over its state, designed for statistics rather than against someone working backwards from outputs it has already shown. Right: take secrets straight from secrets or crypto.getRandomValues, which never let you choose a seed at all.

Secure bytes come from the operating system

A CSPRNG: A cryptographically secure pseudo-random number generator: no practical computation on the bits it has given predicts its next bit noticeably better than a coin toss. Operating systems run one, seeded from an entropy source. (CSPRNG) makes a different promise: no practical computation on the bits it has already given predicts its next bit noticeably better than a coin toss, which is how the Handbook of Applied Cryptography defines one. You do not build one, and you do not seed one. The operating system runs one for every program, and each language reaches it through a small API, directly or through a library generator that the system seeds. Step through the path the bytes take, from the hardware to your key:

Interactive · stage by stage Where a secure random key comes from Step through the layers from physical noise to the 16 bytes of your key. Each step says which standard or document describes that layer.
Where a secure random key comes from, step 6 of 6. Showing Physical noise, Entropy source, The system's DRBG, One system call, Your language's call, Your key or token. In focus: Your key or token.Physical noisetimings, events, circuitsEntropy sourceraw bits, health-testedThe system's DRBGseeded and reseededOne system callgetrandom, getentropyYour language's callgetRandomValues, secretsYour key or token16 bytes or moreNo seed of yours anywhere on this path

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Noise nobody can predict. Every secure generator starts from physical events whose exact timing or value no program can know in advance. NIST SP 800-90B calls the part that observes them a noise source.
  2. An entropy source. The entropy source turns that noise into raw bits and tests its own health while it runs. Its bits are unpredictable but slow to collect and may be biased, so programs never read them directly.
  3. A deterministic generator. The system feeds those bits as a seed into a deterministic random bit generator, a DRBG of the kind SP 800-90A specifies: Hash_DRBG, HMAC_DRBG or CTR_DRBG. It stretches the seed into as many bytes as programs ask for and reseeds itself.
  4. The only door. Programs ask through one call: getrandom on Linux, getentropy on macOS and OpenBSD, BCryptGenRandom on Windows. Right after boot, getrandom waits until the kernel has collected 128 bits of entropy.
  5. Your language wraps it. crypto.getRandomValues in browsers, workers and Node, crypto.randomBytes in Node, secrets and os.urandom in Python. Each draws on a generator that the system seeds, and none of them asks you for a seed.
  6. The bytes you keep. Sixteen bytes give 128 bits, the floor ASVS 5.0 sets for anything that must not be guessed, and 32 bytes give 256 bits, the default size of a Python secrets token. Encode them as hex or Base64url to store or send them.

NIST describes each layer in its own standard: SP 800-90B covers the entropy source: The part of a random bit generator that turns unpredictable physical events into raw bits and tests its own health, as NIST SP 800-90B describes. Its output seeds a DRBG. that measure unpredictable physical events, SP 800-90A specifies the DRBG: A deterministic random bit generator, as NIST SP 800-90A specifies: Hash_DRBG, HMAC_DRBG or CTR_DRBG. It stretches a secret seed from an entropy source into as many random bytes as needed and is reseeded over time. that stretch their output (Hash_DRBG, HMAC_DRBG and CTR_DRBG; a fourth, Dual_EC_DRBG, was removed from Rev. 1), and SP 800-90C says how to combine the two into a complete generator. OWASP’s ASVS 5.0 lists the same pieces in its Appendix C, from /dev/urandom and getentropy() to the three DRBGs, as the generators it accepts.

The one moment a secure generator can make you wait is right after the computer starts, before it has collected enough entropy. Python’s documentation says that on Linux os.urandom blocks until the kernel has gathered 128 bits, and Node’s says crypto.randomBytes() does not complete until there is sufficient entropy, which should normally never take longer than a few milliseconds. After that, none of them runs out. RFC 4086, the IETF’s guide to randomness for security, explains why the home-made alternatives fail: values read from the clock carry far fewer unpredictable bits than they appear to, and a generator seeded from them leaves an attacker only a few values to try.

Key idea

Ask the operating system: every call in this lesson draws on a generator that the system seeds and reseeds itself, and none of them needs or accepts a seed from you.

Checkpoint

Check: Random-looking is not unpredictable · The operating system's generator

  1. Question 1 of 2 A service makes reset tokens with random.Random(int(time.time())) and logs the minute in which each token was issued. How many seeds must a test try to rebuild one token?

    Choose one answer.

    A hint

    The seed is a whole number of seconds, and the log narrows down which ones are possible.

    Show the answer: Question 1 of 2

    Answer: At most 60

    A seeded generator is a function of its seed. random.Random(int(time.time())) has a whole number of seconds as its seed, and a log that says which minute narrows it down to 60 candidates, each of which takes microseconds to try. The 128 bits of the token add no work at all.

  2. Question 2 of 2 Where should the seed of the generator behind your session tokens come from?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nowhere in your code: the operating system seeds its generator from its entropy source and reseeds it

    Secure generators are seeded by the operating system, which feeds an entropy source into a DRBG and reseeds it as it runs. getRandomValues, randomBytes, secrets and os.urandom all draw on it, and none of them accepts a seed.

In browsers and Node: getRandomValues, 65,536 bytes at a time

crypto.getRandomValues(array) is the call that works everywhere JavaScript runs: in pages, in workers and in Node, where globalThis.crypto is the same Web Crypto object. The Web Crypto specification makes its steps exact: refuse any array that is not an integer typed array with a TypeMismatchError, refuse more than 65,536 bytes with a QuotaExceededError, then fill the array with cryptographically secure random bytes and return it. The limit is a limit per call, so a larger buffer is filled one slice at a time, and a subarray view lets each call write into the right part of the buffer without copying anything.

This program asks for a 32-byte key, tries the three sizes and types that sit on either side of the rules, fills a mebibyte in slices, then uses node:crypto, which exists only in Node:

crypto.getRandomValues and node:crypto on Node.js JavaScript · bytes-js/random_bytes.mjs
// Secure random bytes in JavaScript: crypto.getRandomValues (browsers, workers and Node) and node:crypto (Node only).
import { randomBytes, randomUUID } from 'node:crypto';

const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
const base64url = (bytes) => Buffer.from(bytes).toString('base64url'); // Node's Buffer; browsers need a helper

const key = crypto.getRandomValues(new Uint8Array(32));
console.log(`32-byte key:  ${hex(key)}`);
console.log(`as Base64url: ${base64url(key)} (${base64url(key).length} characters)`);

const requests = [
  ['65,536 bytes', () => new Uint8Array(65_536)],
  ['65,537 bytes', () => new Uint8Array(65_537)],
  ['a Float64Array', () => new Float64Array(4)],
];
for (const [what, make] of requests) {
  try {
    crypto.getRandomValues(make());
    console.log(`getRandomValues(${what}): filled`);
  } catch (e) {
    console.log(`getRandomValues(${what}): ${e.name}`);
  }
}

// More than 65,536 bytes: fill the buffer one slice at a time.
function randomBuffer(n) {
  const out = new Uint8Array(n);
  let calls = 0;
  for (let i = 0; i < n; i += 65_536) {
    crypto.getRandomValues(out.subarray(i, i + 65_536));
    calls++;
  }
  return { out, calls };
}
const { out, calls } = randomBuffer(1 << 20);
console.log(`1 MiB in ${calls} calls; byte values seen: ${new Set(out).size} of 256`);

// node:crypto has no such limit (Node only).
console.log(`randomBytes(1 MiB): ${randomBytes(1 << 20).length.toLocaleString('en')} bytes in one call`);
console.log(`randomUUID(): version ${randomUUID()[14]} UUID`);

Output

32-byte key:  6e80625e4b42a52b344f6817953d2c306d10869a70e624db963872c0577d4e64
as Base64url: boBiXktCpSs0T2gXlT0sMG0Qhppw5iTbljhywFd9TmQ (43 characters)
getRandomValues(65,536 bytes): filled
getRandomValues(65,537 bytes): QuotaExceededError
getRandomValues(a Float64Array): TypeMismatchError
1 MiB in 16 calls; byte values seen: 256 of 256
randomBytes(1 MiB): 1,048,576 bytes in one call
randomUUID(): version 4 UUID

This output changes from run to run: the key and its Base64url form are new random bytes on every run

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node random_bytes.mjs

The first two lines change on every run: that is the point. Exactly 65,536 bytes are filled, one byte more raises QuotaExceededError, and a Float64Array raises TypeMismatchError, because the call fills integer typed arrays only. The mebibyte took 16 calls of 65,536 bytes, and all 256 byte values turned up in it.

In Node, node:crypto adds three calls with no such small limit: randomBytes(n) returns a Buffer, randomUUID() a version 4 UUID, and randomInt(min, max) an integer that avoids modulo bias, which the next lesson explains. Node’s documentation calls the getRandomValues export of node:crypto a convenient alias that does not follow the Web Crypto specification exactly, and suggests crypto.webcrypto.getRandomValues() for code that must also run in browsers; the global crypto object is that webcrypto object. In a browser, crypto.randomUUID() exists only on secure pages, while getRandomValues works on any page.

Checkpoint

Check: getRandomValues and its limits

  1. Question 1 of 2 What does crypto.getRandomValues(new Uint8Array(100_000)) do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws a QuotaExceededError and fills nothing

    getRandomValues checks the type, then the size, then fills. 100,000 bytes are above the limit of 65,536, so it throws QuotaExceededError, as the recorded run did for 65,537 bytes.

  2. Question 2 of 2 How many calls of crypto.getRandomValues does the lesson's slicing loop make to fill 200,000 bytes?

    Type a number.

    A hint

    The loop starts a new slice at 0, 65,536, 131,072 … while the start is below n.

    Show the answer: Question 2 of 2

    Answer: 4 calls

    The slices start at 0, 65,536, 131,072 and 196,608. Three full slices cover 196,608 bytes, and the fourth fills the last 3,392. A start of 262,144 is past 200,000, so the loop stops after 4 calls.

In Python: secrets, os.urandom and how many bytes

Python’s secrets module is the high-level door to the operating system’s generator. Its documentation puts it first for anything secret and leaves random to modelling and simulation. It gives tokens as bytes, hex or URL-safe Base64 text, and randbelow, randbits and choice for numbers and picks, all from the same source as os.urandom. Run the program to see the sizes it chooses when you give none:

secrets tokens, os.urandom and randbelow Python · tokens/tokens.py
"""Python's secrets module: tokens of the default size and of a size you choose, and numbers in a range."""
import os
import secrets

print(f"secrets.DEFAULT_ENTROPY: {secrets.DEFAULT_ENTROPY} bytes")
print(f"{'length of':<16}{'default':<10}16 bytes")
for name in ["token_bytes", "token_hex", "token_urlsafe"]:
    make = getattr(secrets, name)
    print(f"{name + '()':<16}{len(make()):<10}{len(make(16))}")

print(f"os.urandom(32): {len(os.urandom(32))} bytes")

rolls = [secrets.randbelow(6) + 1 for _ in range(10_000)]
print(f"10,000 dice rolls with randbelow(6) + 1, every face from 1 to 6 seen: {set(rolls) == set(range(1, 7))}")
print(f"secrets.choice('ACGT') is one of the four letters: {secrets.choice('ACGT') in 'ACGT'}")

Output

secrets.DEFAULT_ENTROPY: 32 bytes
length of       default   16 bytes
token_bytes()   32        16
token_hex()     64        32
token_urlsafe() 43        22
os.urandom(32): 32 bytes
10,000 dice rolls with randbelow(6) + 1, every face from 1 to 6 seen: True
secrets.choice('ACGT') is one of the four letters: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tokens.py

With no argument the token functions use secrets.DEFAULT_ENTROPY, 32 bytes here, which the documentation calls enough for the typical uses of the module while warning that the right amount grows as computers get faster and that the exact default may change. So pass the size whenever it matters: token_bytes(16) is 16 bytes, token_hex(16) 32 characters, and token_urlsafe(16) 22, about 1.3 characters a byte. How many bytes a value needs follows from its job:

Value Bytes Why that many
Session ID, reset link, API token 16 or more ASVS 5.0 requirement 11.5.1: a CSPRNG and at least 128 bits for anything that must not be guessed
AES-128 key 16 the cipher’s key is 128 bits
AES-256 key 32 the cipher’s key is 256 bits
Salt for password hashing 16 Python’s hashlib documentation suggests about 16 or more bytes from a proper source
AES-GCM nonce 12 the 96-bit nonce of the security-levels lesson, with its birthday limit
A token where you set no size 32 secrets.DEFAULT_ENTROPY

ASVS adds that UUIDs do not meet its 128-bit rule, which the next lesson takes apart. The choice of call depends on where your code runs and what it needs. Answer the three questions in the table below, which marks what fits:

Interactive · answer and compare Which call gives you a secure random value? Answer where your code runs, what you need and whether the value must stay secret. The table marks the calls that fit and says why the others do not.
Where does the code run?
What do you need?
Must the value stay secret, or decide anything about security?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Seven random-number calls against the four things a secret needs from them
Call Safe for secrets Size per call What it returns Can be seeded
crypto.getRandomValues Works in: browsers, workers, Node Good fills from a cryptographically secure generator Mixed up to 65,536 bytes; fill larger buffers in slices Good your integer typed array, filled Good no: the system seeds it
crypto.randomBytes Works in: Node (node:crypto) Good waits for entropy right after boot, then answers at once Good 1 MiB in one call in the lesson's run Good a Buffer of the size you ask for Good no: the system seeds it
crypto.randomUUID Works in: secure pages, workers, Node Mixed 122 random bits, but ASVS says UUIDs miss its 128-bit rule Mixed always one UUID Mixed a version 4 UUID string, not raw bytes Good no: the system seeds it
secrets.token_bytes Works in: Python Good the module Python names for secrets Good any size; 32 bytes when you give none Good bytes, hex or URL-safe text Good no: the system seeds it
os.urandom Works in: Python Good reads the operating system's generator Good any size Good bytes Good no: the system seeds it
Math.random Works in: browsers, Node Poor MDN: not for anything related to security Mixed one number per call Poor a float from 0 up to 1 Mixed the engine picks the seed, and you cannot see or set it
random.random Works in: Python Poor Mersenne Twister: unsuitable for cryptographic purposes Mixed one number per call Poor a float from 0 up to 1 Poor random.seed(42) repeats every value, by design
How the answers decide
  • “Must the value stay secret, or decide anything about security?” Yes: rules out Math.random and random.random. Both are fast generators for simulations: MDN rules Math.random out for security, and Python calls its Mersenne Twister unsuitable for cryptography.
  • “Must the value stay secret, or decide anything about security?” No, it only has to look random: adds a caveat to Math.random and random.random. Fine for shuffling a quiz or running a simulation. Switch to a secure call the moment the value grants, proves or protects anything.
  • “Where does the code run?” In a browser or a worker: rules out crypto.randomBytes, secrets.token_bytes, os.urandom and random.random. node:crypto and Python modules do not exist in a browser. crypto.getRandomValues works on every page; crypto.randomUUID on secure pages.
  • “Where does the code run?” In Node: rules out secrets.token_bytes, os.urandom and random.random. Python modules are not available in Node: use getRandomValues, randomBytes or randomUUID.
  • “Where does the code run?” In Python: rules out crypto.getRandomValues, crypto.randomBytes, crypto.randomUUID and Math.random. These are JavaScript calls. Python has secrets and os.urandom.
  • “What do you need?” Bytes for a key, a salt or a nonce: rules out crypto.randomUUID. randomUUID returns a formatted string in which 6 bits are fixed, not the raw 16 or 32 bytes a key or a salt needs.
  • “What do you need?” A token as text: adds a caveat to crypto.getRandomValues, crypto.randomBytes and os.urandom. They return bytes: encode them as hex or Base64url, or call secrets.token_urlsafe in Python.
  • “What do you need?” A random identifier and “Must the value stay secret, or decide anything about security?” Yes: rules out crypto.randomUUID. An identifier that grants access is a secret token, and ASVS 5.0 requirement 11.5.1 says UUIDs do not meet its rule: use 16 or more random bytes.
  • “What do you need?” A random identifier and “Must the value stay secret, or decide anything about security?” No, it only has to look random: adds a caveat to crypto.getRandomValues, crypto.randomBytes, secrets.token_bytes and os.urandom. They work, but randomUUID gives an identifier in the standard format in one call, which databases and logs already know.

In short: crypto.getRandomValues in browsers and workers, crypto.randomBytes or the same getRandomValues in Node, and secrets in Python, with Math.random and random kept for shuffling a quiz or running a simulation.

Checkpoint

Check: secrets, os.urandom and sizes

  1. Question 1 of 2 How many characters does secrets.token_urlsafe(32) return?

    Type a number.

    A hint

    Base64url writes 6 bits per character and leaves out the = padding.

    Show the answer: Question 1 of 2

    Answer: 43 characters

    32 bytes are 256 bits, and 256 ÷ 6 = 42.7, so the text needs 43 characters, the same as the 43-character Base64url key in the JavaScript run. The default call, token_urlsafe(), also gives 43, because the default is 32 bytes.

  2. Question 2 of 2 Which calls give a 16-byte salt for password hashing in Python?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • os.urandom(16)
    • secrets.token_bytes(16)

    A salt has to be unpredictable and different for every stored password, so it comes from the secure generator: secrets.token_bytes(16) or os.urandom(16). random.randbytes is the simulation generator, and a hash of the user name is neither random nor new.

Seeds belong in tests, never in the code that makes secrets

Tests want the opposite of security: the same output on every run, so that an expected token can be written into the test. The tempting fix is a seed parameter on the function that makes tokens, or a random.seed(1) in a module that the tests and the production code share. Both put a way to predict secrets into the code that ships. The safe design passes the random source in as a parameter: production code uses the system generator as the default, and only tests hand in a fixed source. This function takes a source the way crypto.getRandomValues fills a buffer:

A token function with its random source passed in JavaScript · injected-js/injected_rng.mjs
// A token function that takes its random source as a parameter: the operating system's generator in production,
// a fixed source in tests. Production code never sees a seed.
const systemSource = (bytes) => crypto.getRandomValues(bytes);

export function issueToken(source = systemSource) {
  const bytes = new Uint8Array(16); // 128 bits
  source(bytes);
  return Buffer.from(bytes).toString('base64url');
}

// A test source: the bytes 0, 1, 2, 3 ..., so a test knows every token in advance.
function countingSource() {
  let next = 0;
  return (bytes) => {
    for (let i = 0; i < bytes.length; i++) bytes[i] = next++ & 0xff;
    return bytes;
  };
}

const first = issueToken(countingSource());
console.log(`test source:   ${first}`);
console.log(`a second test gets the same token: ${issueToken(countingSource()) === first}`);

const a = issueToken();
const b = issueToken();
console.log(`system source: ${a.length} characters; two tokens differ: ${a !== b}`);

Output

test source:   AAECAwQFBgcICQoLDA0ODw
a second test gets the same token: true
system source: 22 characters; two tokens differ: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node injected_rng.mjs

The counting test source gives the token AAECAwQFBgcICQoLDA0ODw, the bytes 0 to 15 in Base64url, on every run, so a test can assert it exactly. With no argument the function uses the system generator, and two calls give two different 22-character tokens. Nothing in the function reads a seed, an environment variable or a debug flag, so there is no setting that could make production tokens predictable.

A seed parameter on production code

Wrong: def issue_token(seed=None): rng = random.Random(seed), which a test calls with seed=1. Anyone who learns that a deployment passes a seed, or that one code path forgets the default, can rebuild its tokens. Right: def issue_token(source=secrets.token_bytes), with tests passing lambda n: bytes(range(n)), so the production path never accepts a seed.

For simulations, games and property-based tests, a seeded random.Random(seed) is the right tool, kept in its own object rather than in the module’s shared state, and never used for a value that guards anything.

Key idea

Pass randomness in, do not seed it: a parameter that defaults to the system generator lets tests use a fixed source while the code that ships has no seed to leak.

Checkpoint

Check: Seeds only in tests

  1. One question A test needs to know in advance which token issue_token() returns. Which change keeps production safe?

    Choose one answer.

    Show the answer: One question

    Answer: Give issue_token a source parameter that defaults to secrets.token_bytes; the test passes lambda n: bytes(range(n))

    Pass randomness in rather than seeding it: a parameter whose default is the system generator lets a test supply a fixed source, while the code that ships has no seed, flag or variable that could make it predictable.

Practise: a token function and a buffer filler

Two exercises, each with sample tests that run on this page, build the two pieces of random-value code that applications need most. The first makes a token with at least the entropy you ask for, from an injected source, and its starter code rounds the length down. The second fills a large buffer from a source that refuses more than 65,536 bytes in one call, the way crypto.getRandomValues does:

Code lab · 2 steps

Build safe random-value helpers

Write a token function that reaches the entropy you ask for from an injected source, then fill large buffers within the 65,536-byte limit.

  1. A token with at least the entropy you ask for To do
  2. Fill a large buffer 65,536 bytes at a time To do

Exercise · Easy · Python, JavaScript

A token with at least the entropy you ask for

Write random_token(bits, alphabet, source) in random_token.py (JavaScript: randomToken(bits, alphabet, source) in random_token.mjs). It returns a string of characters from alphabet that carries at least bits bits of entropy:

  • The alphabet has 2, 4, 8, 16, 32, 64, 128 or 256 distinct characters, so each character carries log₂ of that many bits: 4 for hex, 5 for Base32, 6 for Base64url. Use as few characters as reach bits, rounding up: 128 bits in Base64url take 22 characters, because 21 carry only 126.
  • The random bytes come from source, one byte for each character, and the byte's low bits pick the character. In Python source(n) returns n bytes; in JavaScript source(bytes) fills the Uint8Array it is given, as crypto.getRandomValues does. Production code passes the system generator; the sample tests pass a counting source, so that every token is known in advance.
  • Raise ValueError (JavaScript: throw an Error) for any other alphabet size, which needs the rejection sampling of the next lesson, for an alphabet that repeats a character, and for bits below 1.

The starter code rounds the length down and checks nothing.

Python · Starter code · random_token.py

def random_token(bits, alphabet, source):
    """A token of characters from alphabet that carries at least `bits` bits of entropy.

    source(n) returns n random bytes: secrets.token_bytes in production, a fixed source in tests.
    """
    size = len(alphabet)
    per_character = size.bit_length() - 1  # 4 for hex, 6 for Base64url
    length = bits // per_character  # rounds down: too short whenever per_character does not divide bits
    return "".join(alphabet[b % size] for b in source(length))
The sample tests · test_random_token.py
from random_token import random_token

HEX = "0123456789abcdef"
BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"


def counting(n):
    """A test source: the bytes 0, 1, 2 ... (not random, so every test knows the token in advance)."""
    return bytes(i % 256 for i in range(n))


def raises_value_error(call):
    try:
        call()
    except ValueError:
        return True
    return False


def test_hex_lengths():
    """128 and 256 bits in hex take 32 and 64 characters"""
    assert len(random_token(128, HEX, counting)) == 32
    assert len(random_token(256, HEX, counting)) == 64


def test_base64url_rounds_up():
    """128 bits in Base64url take 22 characters: 21 carry only 126 bits"""
    assert len(random_token(128, BASE64URL, counting)) == 22
    assert len(random_token(256, BASE64URL, counting)) == 43


def test_base32():
    """130 bits in Base32 take 26 characters, and 131 take 27"""
    assert len(random_token(130, BASE32, counting)) == 26
    assert len(random_token(131, BASE32, counting)) == 27


def test_each_byte_picks_one_character():
    """byte 0 picks the first character, byte 1 the second, and so on"""
    assert random_token(64, HEX, counting) == "0123456789abcdef"
    assert random_token(30, BASE64URL, counting) == "ABCDE"
    assert random_token(5, "01", lambda n: bytes([2, 3, 4, 5, 255])) == "01011"


def test_one_call_for_the_whole_token():
    """the source is asked once, for exactly one byte per character"""
    asked = []

    def recording(n):
        asked.append(n)
        return counting(n)

    random_token(128, BASE64URL, recording)
    assert asked == [22]


def test_refuses_what_it_cannot_do_safely():
    """alphabets of 10, 62, 1 or 512 characters, a repeated character, and bits below 1"""
    wide = "".join(chr(0x100 + i) for i in range(512))  # 512 distinct characters: more than a byte can pick
    cases = [(128, "0123456789"), (128, BASE64URL[:62]), (128, "a"), (128, wide), (128, "aabbccdd"), (0, HEX), (-8, HEX)]
    for bits, alphabet in cases:
        assert raises_value_error(lambda: random_token(bits, alphabet, counting)), f"{bits} bits in {alphabet[:12]!r}"

JavaScript · Starter code · random_token.mjs

/**
 * A token of characters from alphabet that carries at least `bits` bits of entropy.
 * source(bytes) fills a Uint8Array: crypto.getRandomValues in production, a fixed source in tests.
 */
export function randomToken(bits, alphabet, source) {
  const size = alphabet.length;
  const perCharacter = Math.log2(size); // 4 for hex, 6 for Base64url
  const bytes = new Uint8Array(Math.floor(bits / perCharacter)); // rounds down: too short
  source(bytes);
  return Array.from(bytes, (b) => alphabet[b % size]).join('');
}
The sample tests · random_token.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { randomToken } from './random_token.mjs';

const HEX = '0123456789abcdef';
const BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const BASE64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';

// A test source: fills with 0, 1, 2 ... (not random, so every test knows the token in advance).
const counting = (bytes) => {
  for (let i = 0; i < bytes.length; i++) bytes[i] = i % 256;
  return bytes;
};

test('128 and 256 bits in hex take 32 and 64 characters', () => {
  assert.equal(randomToken(128, HEX, counting).length, 32);
  assert.equal(randomToken(256, HEX, counting).length, 64);
});

test('128 bits in Base64url take 22 characters: 21 carry only 126 bits', () => {
  assert.equal(randomToken(128, BASE64URL, counting).length, 22);
  assert.equal(randomToken(256, BASE64URL, counting).length, 43);
});

test('130 bits in Base32 take 26 characters, and 131 take 27', () => {
  assert.equal(randomToken(130, BASE32, counting).length, 26);
  assert.equal(randomToken(131, BASE32, counting).length, 27);
});

test('byte 0 picks the first character, byte 1 the second, and so on', () => {
  assert.equal(randomToken(64, HEX, counting), '0123456789abcdef');
  assert.equal(randomToken(30, BASE64URL, counting), 'ABCDE');
  const fixed = (bytes) => {
    bytes.set([2, 3, 4, 5, 255]);
    return bytes;
  };
  assert.equal(randomToken(5, '01', fixed), '01011');
});

test('the source fills one buffer, one byte per character', () => {
  const asked = [];
  randomToken(128, BASE64URL, (bytes) => {
    asked.push(bytes.length);
    return counting(bytes);
  });
  assert.deepEqual(asked, [22]);
});

test('alphabets of 10, 62, 1 or 512 characters, a repeated character, and bits below 1 are refused', () => {
  const wide = Array.from({ length: 512 }, (_, i) => String.fromCharCode(0x100 + i)).join(''); // more than a byte can pick
  const cases = [[128, '0123456789'], [128, BASE64URL.slice(0, 62)], [128, 'a'], [128, wide], [128, 'aabbccdd'], [0, HEX], [-8, HEX]];
  for (const [bits, alphabet] of cases) assert.throws(() => randomToken(bits, alphabet, counting), `${bits} bits in ${alphabet.slice(0, 12)}`);
});
A hint

With an alphabet of size 2ᵏ, each character carries k bits, so the length is bits / k rounded up: in Python -(-bits // k), in JavaScript Math.ceil(bits / k). A size is a power of two when size & (size - 1) is 0, and byte & (size - 1) keeps the byte's low k bits, which picks every character equally often.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · Python, JavaScript

Fill a large buffer 65,536 bytes at a time

crypto.getRandomValues refuses more than 65,536 bytes in one call. Write random_buffer(n, source) in random_buffer.py (JavaScript: randomBuffer(n, fill) in random_buffer.mjs) that returns n random bytes from a source with the same limit:

  • Python: source(k) returns k bytes and raises ValueError when k is above 65,536. Return a bytes of length n, made from as few calls as possible, in order.
  • JavaScript: fill(view) fills a Uint8Array in place and throws when the view is longer than 65,536 bytes, as crypto.getRandomValues does. Return a new Uint8Array(n) filled through views of it (subarray), so that nothing is copied.
  • n of 0 gives an empty result without any call; a negative or fractional n raises ValueError (JavaScript: throws an Error).

The starter code asks for everything at once, which works only up to 65,536 bytes.

Python · Starter code · random_buffer.py

LIMIT = 65_536  # bytes per call, as crypto.getRandomValues allows


def random_buffer(n, source):
    """n random bytes from source(k), which refuses more than LIMIT bytes in one call."""
    return source(n)  # one call: fails for anything above 65,536 bytes
The sample tests · test_random_buffer.py
from random_buffer import random_buffer

PATTERN = bytes(range(256)) * 258


class LimitedSource:
    """A test source with getRandomValues's limit: it counts 0, 1, 2 ... across calls and records each request."""

    def __init__(self):
        self.calls = []
        self.position = 0

    def __call__(self, k):
        if k > 65_536:
            raise ValueError(f"{k} bytes in one call: the limit is 65,536")
        self.calls.append(k)
        start = self.position % 256
        self.position += k
        return PATTERN[start : start + k]


def raises_value_error(call):
    try:
        call()
    except ValueError:
        return True
    return False


def test_small_and_exact():
    """up to 65,536 bytes take one call"""
    for n in [1, 32, 65_536]:
        source = LimitedSource()
        assert len(random_buffer(n, source)) == n
        assert source.calls == [n], f"{n} bytes"


def test_one_byte_over():
    """65,537 bytes take two calls: 65,536 and 1"""
    source = LimitedSource()
    assert len(random_buffer(65_537, source)) == 65_537
    assert source.calls == [65_536, 1]


def test_one_mebibyte():
    """1 MiB takes 16 calls of 65,536 bytes, and the bytes keep their order"""
    source = LimitedSource()
    data = random_buffer(1 << 20, source)
    assert source.calls == [65_536] * 16
    assert data == PATTERN[:256] * 4096


def test_zero_bytes():
    """0 bytes: an empty result and no call at all"""
    source = LimitedSource()
    assert random_buffer(0, source) == b""
    assert source.calls == []


def test_bad_sizes():
    """a negative or fractional size is refused"""
    for bad in [-1, 2.5]:
        assert raises_value_error(lambda: random_buffer(bad, LimitedSource())), f"{bad} should raise ValueError"

JavaScript · Starter code · random_buffer.mjs

export const LIMIT = 65_536; // bytes per call, as crypto.getRandomValues allows

/** n random bytes from fill(view), which refuses views longer than LIMIT. */
export function randomBuffer(n, fill) {
  const out = new Uint8Array(n);
  fill(out); // one call: fails for anything above 65,536 bytes
  return out;
}
The sample tests · random_buffer.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { randomBuffer } from './random_buffer.mjs';

// A test fill with getRandomValues's limit: it counts 0, 1, 2 ... across calls and records the length of each view.
function limitedFill() {
  let position = 0;
  const calls = [];
  const fill = (view) => {
    if (view.length > 65_536) throw new Error(`${view.length} bytes in one call: the limit is 65,536`);
    calls.push(view.length);
    for (let i = 0; i < view.length; i++) view[i] = (position + i) & 0xff;
    position += view.length;
    return view;
  };
  return { fill, calls };
}

test('up to 65,536 bytes take one call', () => {
  for (const n of [1, 32, 65_536]) {
    const { fill, calls } = limitedFill();
    assert.equal(randomBuffer(n, fill).length, n);
    assert.deepEqual(calls, [n], `${n} bytes`);
  }
});

test('65,537 bytes take two calls: 65,536 and 1', () => {
  const { fill, calls } = limitedFill();
  assert.equal(randomBuffer(65_537, fill).length, 65_537);
  assert.deepEqual(calls, [65_536, 1]);
});

test('1 MiB takes 16 calls of 65,536 bytes, and the bytes keep their order', () => {
  const { fill, calls } = limitedFill();
  const data = randomBuffer(1 << 20, fill);
  assert.deepEqual(calls, Array(16).fill(65_536));
  let inOrder = true;
  for (let i = 0; i < data.length; i += 4099) if (data[i] !== (i & 0xff)) inOrder = false;
  assert.ok(inOrder, 'byte i should be i % 256');
});

test('0 bytes: an empty result and no call at all', () => {
  const { fill, calls } = limitedFill();
  assert.equal(randomBuffer(0, fill).length, 0);
  assert.deepEqual(calls, []);
});

test('a negative or fractional size is refused', () => {
  for (const bad of [-1, 2.5]) assert.throws(() => randomBuffer(bad, limitedFill().fill), `${bad} should throw`);
});
A hint

Step through the buffer in strides of 65,536: start at 0, 65,536, 131,072 … and ask each time for the smaller of 65,536 and what is left, min(65_536, n - start). In JavaScript out.subarray(start, start + 65_536) stops at the end of the buffer by itself.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Random String & Code Generator Make codes from your own alphabet with the browser's secure generator and compare their entropy with the token sizes above.

Then take a mixed run through the five ideas of this lesson. The first questions go to your weakest idea, a right answer earns a harder one, and anything you miss comes back before the end:

Practice that adapts to you

Practise this lesson

14 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 For which job is Python's random module the right tool?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: A simulation of queue lengths that you want to rerun with the same seed

    The random module is a fast, seeded generator for modelling and simulation. Anything that grants, proves or protects something, such as reset codes, salts and session identifiers, comes from secrets or os.urandom.

  2. Question 2 of 14 Two servers start in the same second, and each seeds random.Random with int(time.time()) to make its first token with getrandbits(128). What happens?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: The two servers give out the same first token

    int(time.time()) is the same whole number on both servers, and a seeded generator is a function of its seed, so both first tokens are identical: a collision as well as a predictable token.

  3. Question 3 of 14 A pull request replaces random.seed(time.time()) with random.seed(os.urandom(32)) and keeps random.getrandbits(128) for API keys. Is that enough?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: No: the seed is fixed, but the generator is still the Mersenne Twister; take the keys from secrets.token_bytes(16) instead

    A better seed fixes CWE-337, the predictable seed, but not the generator: random is a simulation generator. The fix is to drop it for anything secret and take the bytes from secrets, which never lets you pick a seed.

  4. Question 4 of 14 Which are DRBG mechanisms of NIST SP 800-90A Rev. 1?

    Choose every answer that is right.

    Show the answer: Question 4 of 14

    Answer:

    • HMAC_DRBG
    • Hash_DRBG
    • CTR_DRBG

    SP 800-90A Rev. 1 specifies Hash_DRBG, HMAC_DRBG and CTR_DRBG. Dual_EC_DRBG was removed in that revision, and ASVS 5.0 lists the three remaining DRBGs among the generators it accepts.

  5. Question 5 of 14 A Python service on a freshly started Linux virtual machine pauses in its first call to os.urandom, and never again. Why?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: The kernel had not yet collected 128 bits of entropy, and getrandom waits until it has

    Right after boot the kernel may not have collected enough entropy to seed its generator. On Linux, os.urandom uses getrandom in blocking mode and waits until 128 bits are collected; after that it answers at once, every time.

  6. Question 6 of 14 A teammate proposes making tokens as SHA-256(time.time_ns() + process ID + a counter), "so we do not depend on the operating system". What do you answer?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: The output looks random, but it holds only the few unpredictable bits of its inputs, which can be narrowed down and tried; use the system generator

    A hash of guessable inputs is guessable: the work for an attacker is the number of likely inputs, not the 256 bits of the digest. The operating system's CSPRNG is fed by a real entropy source and is the only input a token needs.

  7. Question 7 of 14 crypto.getRandomValues(new Float64Array(8)) throws a TypeMismatchError. Why?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: The call fills only integer typed arrays, from Int8Array to BigUint64Array

    The specification lists the integer typed arrays the call accepts and throws TypeMismatchError for anything else, before it checks the size. Use a Uint8Array for bytes, and build any other value from those bytes.

  8. Question 8 of 14 A page served over plain http calls crypto.randomUUID() and gets "crypto.randomUUID is not a function", while crypto.getRandomValues works. Why?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: randomUUID exists only in secure contexts, such as pages served over HTTPS, while getRandomValues exists on every page

    In the Web Crypto specification, randomUUID is exposed only in secure contexts; on an insecure page the property is missing, so calling it fails. Serve the page over HTTPS, or build the identifier from getRandomValues.

  9. Question 9 of 14 This function is called with n = 1,048,576. What does it return?

    Read the code, then choose one answer.

    function randomBuffer(n) {
      const out = new Uint8Array(n);
      for (let i = 0; i < n; i += 65_536) {
        crypto.getRandomValues(out.subarray(i, 65_536));
      }
      return out;
    }
    Show the answer: Question 9 of 14

    Answer: A buffer whose first 65,536 bytes are random and whose other 983,040 bytes are all zero

    subarray takes a start and an end position, not a length. With a fixed end of 65,536, only the first slice has any bytes, and the rest of the buffer stays zero: a key built from it would be mostly zeros, and no error says so. A test that counts the distinct byte values, as the lesson's run does, catches it.

  10. Question 10 of 14 How many characters does secrets.token_hex() return when you give no size, on the Python of the lesson's run?

    Type a number.

    Show the answer: Question 10 of 14

    Answer: 64 characters

    With no size the token functions use secrets.DEFAULT_ENTROPY, which was 32 bytes in the run, and hex writes two characters per byte: 64. The documentation says the default may change, so pass the size when it matters.

  11. Question 11 of 14 Which call gives a password-reset code of at least 128 bits that fits in a URL as it is?
    Show the answer: Question 11 of 14

    Answer: secrets.token_urlsafe(16)

    ASVS 5.0 requirement 11.5.1 asks for a CSPRNG and at least 128 bits for anything that must not be guessed. token_urlsafe(16) gives exactly 128 bits in 22 characters that need no escaping in a URL.

  12. Question 12 of 14 Which of these sizes follow the lesson's table?

    Choose every answer that is right.

    Show the answer: Question 12 of 14

    Answer:

    • An AES-GCM nonce of 12 bytes
    • An AES-256 key of 32 bytes from secrets.token_bytes(32)

    Sizes follow the job: 16 bytes for anything that must not be guessed, the key size of the cipher for a key, 12 bytes for an AES-GCM nonce and about 16 for a salt. A short lifetime or a public salt does not lower them.

  13. Question 13 of 14 Where does random.Random(seed) belong?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: In simulations, games and tests that must repeat, as an object of their own

    Seeded generators are good tools for repeatable work. Keep each in its own object for simulations and tests, and keep everything that guards a system on the unseeded system generator.

  14. Question 14 of 14 During an incident, someone set TOKEN_SEED=7 on one production server so that tokens could be compared with a log, and the code seeds random.Random with it when the variable is set. What do you do?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: Treat every token that server issued since then as known, revoke them, and remove the variable and the code path that reads it

    A code path that seeds the token generator is a latent backdoor. Revoke what it produced, delete the path, and give tests their fixed source through a parameter instead, so the shipped code has nothing to switch on.

Interview questions

Warm-up (fresher to mid level): why must Math.random() never make a session token, and what do you use instead?

Math.random() is a fast pseudo-random generator for simulations and games: every output follows from its internal state by a fixed formula, and MDN says it must not be used for anything related to security. A session token must be unguessable, so someone who learns enough about the generator’s state can predict the tokens of other users, and take over their sessions. Use the platform’s cryptographically secure generator instead: crypto.getRandomValues() in a browser or Node, crypto.randomBytes() in Node, secrets.token_urlsafe() in Python. Take at least 16 bytes, the 128 bits ASVS 5.0 asks for, encode them as Base64url or hex, and never seed that generator yourself. Python’s random module has the same problem, and its documentation says so.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why can a reset token made with Python's random module, seeded with the clock, be rebuilt?

    Its output follows from the seed by a fixed formula, and a clock reading is easy to narrow down. In the lesson's run one of 3,601 seeds, one for each second of the hour before, rebuilt the 128-bit token exactly, although it had 62 one-bits out of 128 like a random value. A secrets token has no seed to find. Seeding with the time or process ID is CWE-337.

  2. Does seeding random with os.urandom(32) make random.getrandbits(128) safe for a token?

    No. The seed is unpredictable, but the generator is still a fast formula over its state, built for statistics rather than against someone working backwards from outputs it has shown. Take secrets straight from secrets or crypto.getRandomValues, which never let you choose a seed.

  3. Where do secure random bytes come from, and which NIST standards describe the layers?

    From the operating system's CSPRNG, which you neither build nor seed. SP 800-90B covers the entropy sources that measure unpredictable physical events, SP 800-90A the deterministic random bit generators that stretch them (Hash_DRBG, HMAC_DRBG, CTR_DRBG), and SP 800-90C how to combine the two. A CSPRNG is one whose next bit no practical computation predicts better than a coin toss.

  4. What does crypto.getRandomValues refuse, and how do you fill a larger buffer?

    It throws TypeMismatchError for anything but an integer typed array (a Float64Array fails) and QuotaExceededError for more than 65,536 bytes in one call. Fill a larger buffer in slices, using subarray views so each call writes into the right part without copying: the lesson filled 1 MiB in 16 calls.

  5. Which random calls does Node.js add beyond getRandomValues?

    randomBytes(n) returns a Buffer with no 65,536-byte limit, randomUUID() a version 4 UUID, and randomInt(min, max) an integer that avoids modulo bias. In a browser, randomUUID() exists only on secure pages, while getRandomValues works on any page.

  6. How many random bytes do common secrets need, and how do Python's secrets functions size their output?

    Session IDs, reset links and API tokens: 16 or more bytes (ASVS 5.0, 11.5.1). AES-128 key 16, AES-256 key 32, password salt 16, AES-GCM nonce 12. With no argument the token functions use secrets.DEFAULT_ENTROPY, which may change, so pass the size: token_bytes(16) is 16 bytes, token_hex(16) 32 characters, token_urlsafe(16) 22.

  7. How should tests get predictable randomness without a seed in the code that makes secrets?

    Pass the random source in as a parameter that defaults to the system generator: def issue_token(source=secrets.token_bytes). Tests hand in a fixed source such as lambda n: bytes(range(n)). A seed parameter on production code lets anyone who learns it, or a code path that forgets the default, rebuild the tokens.

  8. What real failure shows the danger of a poorly seeded generator?

    A change made only in Debian's OpenSSL package left the generator seeded with little more than the process ID, so SSH, OpenVPN, DNSSEC and certificate keys made on those systems could be guessed (Debian advisory DSA-1571, CVE-2008-0166). MITRE lists the weakness as CWE-337.

Key takeaways

  • A generator built for simulations is predictable: a token seeded with the clock was rebuilt from 3,601 guesses, however random its 128 bits looked.
  • Secure random bytes come from the operating system’s CSPRNG, built from an entropy source and a DRBG as NIST SP 800-90A, 800-90B and 800-90C describe, and no call in this lesson asks you for a seed.
  • crypto.getRandomValues fills at most 65,536 bytes of an integer typed array per call, so larger buffers are filled in slices; Node adds randomBytes, randomUUID and randomInt.
  • In Python, use secrets or os.urandom, with at least 16 bytes for anything that must not be guessed and an explicit size wherever the size matters.
  • Tests get a fixed random source through a parameter; the code that ships uses the system generator and accepts no seed.

Terms in this lesson

CSPRNG
A cryptographically secure pseudo-random number generator: no practical computation on the bits it has given predicts its next bit noticeably better than a coin toss. Operating systems run one, seeded from an entropy source.
DRBG
A deterministic random bit generator, as NIST SP 800-90A specifies: Hash_DRBG, HMAC_DRBG or CTR_DRBG. It stretches a secret seed from an entropy source into as many random bytes as needed and is reseeded over time.
entropy source
The part of a random bit generator that turns unpredictable physical events into raw bits and tests its own health, as NIST SP 800-90B describes. Its output seeds a DRBG.
seed (of a generator)
The starting value of a pseudo-random number generator. The same seed always gives the same sequence, so a seed that can be guessed, such as the clock, makes every output guessable.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.