Generate unguessable API keys

An integration service issues 16-character API keys from Math.random, which is not built to be unpredictable. Take every character from the system’s secure generator, lengthen the key and map bytes without bias.

Requires Node.js 22 or newer. No package installation or server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit api-keys.mjs, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same cases.mjs with the same fixtures; locally Node runs your code directly. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. See where the key comes from
Run the starter. Its keys do not come from the bytes randomBytes returned, so the derivation cases fail, and 16 characters do not meet the length rule. MDN says Math.random does not provide cryptographically secure random numbers.

2. Use the system generator
Request bytes with randomBytes(32) and map each one to ALPHABET[byte % 62]. Keep the "ak_" prefix and stop at 32 characters.

3. Discard the bytes that cause bias
256 is not a multiple of 62. Taking every byte modulo 62 makes the first eight characters (A to H) come up five times in 256 instead of four. Skip bytes of 248 and above, as the discard method in NIST SP 800-90A Rev. 1 Appendix A.5 does.

4. Draw again when you run short
Discarded bytes mean one call may not be enough. Loop until the key has 32 characters, and check that two keys in a row differ and each comes only from the bytes of its own calls.

SUPPLIED CONTRACT
newApiKey(randomBytes) returns a new key: "ak_" followed by 32 characters from ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" (62 characters; 32 of them carry about 190 bits). randomBytes(n) is the system CSPRNG: it returns an array of n random byte values (0 to 255), what crypto.getRandomValues(new Uint8Array(n)) fills in a browser or crypto.randomBytes(n) returns in Node.js; n must be an integer from 1 to 65,536. The sandbox has no Web Crypto, so use randomBytes and nothing else: no Math.random and no clock. Pick each character from one byte, in the order the bytes arrive: a byte below 248 (4 × 62, the largest multiple of 62 that fits in a byte) gives ALPHABET[byte % 62]; a byte from 248 to 255 is discarded. Call randomBytes again whenever you run out before you have 32 characters; bytes left over at the end are simply not used.

REFERENCE
reference.mjs contains an explained solution: Bytes 0, 1, 2, … 31 give ak_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef. Byte 247 gives "9" (247 % 62 = 61), and 248 to 255 are skipped wherever they appear, including forty of them in a row at the start, which needs a second call to randomBytes. Two keys in a row, and twenty in a row, each come only from the bytes returned during their own call, so they differ whenever the generator’s output differs. Math.random, a byte scaled with Math.floor(byte / 256 * 62), byte % 62 without the discard, a cutoff of 249 and a key that stops after one call all fail a case.

SCOPE
randomBytes stands in for the operating system’s generator; in your code, call crypto.getRandomValues or crypto.randomBytes directly, and prefer encoding raw bytes (base64url of 32 bytes) when you do not need a particular alphabet. The lab does not cover storing the key: keep only a hash of it, compare in constant time, scope it, and let users revoke and rotate it. A key prefix helps secret scanners find leaked keys but adds no entropy.

NEXT TASK
Search your code for Math.random, random.random, java.util.Random and math/rand near words such as token, key, code, nonce or password. Which of them produce values that someone must not be able to guess?

SOURCE
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
