Random String Generator
A random string is the most general of the credential shapes: an alphabet, a length, and a good source of randomness. Everything else — API keys, session IDs, tokens — is this with a convention on top.
Length Is Determined by the Alphabet
Entropy per character is log2(alphabet size):
| Alphabet | Size | Bits per character | Characters for 128 bits |
|---|---|---|---|
| Digits | 10 | 3.32 | 39 |
| Lowercase | 26 | 4.70 | 28 |
| Alphanumeric | 62 | 5.95 | 22 |
| Base64url | 64 | 6.00 | 22 |
| Alphanumeric + symbols | 94 | 6.55 | 20 |
Excluding Ambiguous Characters
If a human will ever read the string aloud, copy it from a printed page or type it from a
screen, dropping 0, O, I, l and 1 is worth the small entropy loss. Crockford's
Base32 formalises this and additionally treats the confusable characters as equivalent on
input.
For strings only ever handled by machines, keep the full alphabet.
Modulo Bias
The subtle bug in hand-rolled generators. Taking a random byte modulo 62 does not produce a uniform distribution, because 256 is not divisible by 62 — the first few characters of the alphabet appear slightly more often.
The correct approach rejects out-of-range values and draws again:
``javascript
function pick(alphabet) {
const max = 256 - (256 % alphabet.length);
const buf = new Uint8Array(1);
do { crypto.getRandomValues(buf); } while (buf[0] >= max);
return alphabet[buf[0] % alphabet.length];
}
`
The bias is small, and it is exactly the kind of small that cryptanalysis is built on.
Not Math.random()
Math.random() is a fast pseudo-random generator seeded from a small state. Its output is
statistically fine for shuffling a list and useless for anything an attacker cares about —
observing enough output reveals the state and therefore every future value.
Every string here comes from crypto.getRandomValues().
The Avalanche Effect
A one-character change produces a completely different digest — not a similar one. That
property is what makes a hash useful as a fingerprint:
| Input | MD5 | CRC32 |
|---|---|---|
hello | 5d41402abc4b2a76b9719d911017c592 | 3610a686 |
hello. | d94c10e437d18531e122ed0b45badd2a | 0a39d4f1 |
Hello | 8b1a9953c4611296a827abf8c47804d7 | f7d18982 |
hello and Hello differ by one bit of one byte, and share no part of their output.
RIPEMD-160 of hello is 108f07b8382412612c048d07d13f814118445acd, and of Hello is
d44426aca8ae0a69cdbc4021c64fa5ad68ca32fe` — same story.Digest Length and Collision Resistance
| Algorithm | Output | Birthday bound | Status |
|---|---|---|---|
| CRC32 | 32 bits | ~77,000 values | Checksum only |
| MD5 | 128 bits | 2⁶⁴ in theory | Broken — collisions in seconds |
| SHA-1 | 160 bits | 2⁸⁰ in theory | Broken — SHAttered, 2017 |
| RIPEMD-160 | 160 bits | 2⁸⁰ | No practical attack |
| SHA-256 | 256 bits | 2¹²⁸ | Current standard |
| SHA-512 | 512 bits | 2²⁵⁶ | Standard, faster on 64-bit |
Never Hash a Password With These
A general-purpose hash is designed to be fast, which is exactly wrong for passwords: speed helps the attacker. Use a deliberately slow KDF — bcrypt, scrypt or Argon2id — with a per-password salt. A GPU tries billions of SHA-256 guesses a second and a few thousand bcrypt guesses a second, and that gap is the entire defence.