Tokens, hashes, and identifiers
Four primitives turn up constantly in application code: random identifiers, signed tokens, cryptographic hashes, and generated passwords. They are all built from the same underlying material, which is why they get confused with each other, and the confusion produces a specific and recurring set of vulnerabilities. This guide covers what each one actually guarantees.
Identifiers versus secrets
A version 4 UUID contains 122 bits of randomness, which is enough that you will never see a collision. The usual illustration: generating a billion per second for a century leaves the probability of a single duplicate at roughly one in two billion. For practical purposes uniqueness is guaranteed without coordination, which is the entire point, because it means any number of clients and servers can mint identifiers independently.
That randomness makes UUIDs look like secrets, and they are routinely misused as such. Password reset tokens, unguessable share links, and session identifiers all get built from UUIDs. The reason this is wrong is not the entropy, which is ample, but everything around it: identifiers get logged by web servers and proxies, appear in Referer headers, get copied into support tickets, and are stored without hashing because nobody thinks of them as credentials. A value that leaks through five different channels is not a secret regardless of how random it is.
The distinction that matters is intent. An identifier names a thing and is expected to be visible. A secret authorises an action and requires protection at rest, protection in transit, an expiry, and single use. If a value does the second job, generate it as a secret and handle it accordingly.
What a hash function does and does not do
A cryptographic hash maps input of any length to a fixed-length digest, deterministically and in one direction. SHA-256 always yields 256 bits. The properties that make it useful are preimage resistance, meaning you cannot work backwards from a digest to an input, and collision resistance, meaning you cannot find two inputs producing the same digest.
Hashing is not encryption, because there is no key and no way to recover the input. That is a feature for integrity checking and a liability if you were hoping to get the data back. Anyone who describes a password as encrypted in the database has almost certainly hashed it, and the distinction matters when reasoning about a breach.
The most important thing SHA-256 is not suitable for is passwords. It is deliberately fast, and speed is exactly what an attacker with a stolen database wants: commodity hardware computes billions of SHA-256 digests per second, so an eight-character password falls in minutes regardless of how it was salted. Password storage needs a deliberately slow, memory-hard function — bcrypt, scrypt, or Argon2 — where the cost parameter can be raised as hardware improves. Use SHA-256 for file integrity, deduplication, content addressing, and HMAC signatures. Not for credentials.
Why decoding a JWT is not the same as trusting it
A JSON Web Token has three dot-separated parts: a header naming the signing algorithm, a payload of claims, and a signature. The first two are base64url encoded, not encrypted. Anyone holding the token can read every claim in it without any key at all.
Two consequences follow immediately. First, never put anything confidential in a payload — no internal identifiers you would not expose, no personal data beyond what the client already knows. Second, decoding tells you nothing about authenticity. The signature is the only thing standing between a legitimate token and one an attacker constructed by editing the payload, and verifying it requires the secret or public key.
The historical failure here is instructive. Early libraries read the algorithm from the header and dispatched accordingly, which meant a token declaring alg: none was accepted with no signature at all, and a token declaring HS256 against a server expecting RS256 could be signed with the server public key as if it were an HMAC secret. Both were exploited in the wild. Modern libraries require the caller to specify the expected algorithm, and any code that does not is suspect.
Beyond the signature, verification means checking the claims: exp for expiry, nbf for not-before, iss for the expected issuer, and aud for the expected audience. A valid signature on an expired token from the wrong issuer is still a rejection.
Password strength is arithmetic, not intuition
The number of possible passwords is the character set size raised to the length. Ninety-four printable ASCII characters at length twelve gives about 2 to the 78, which resists offline attack against a properly slow hash. The same character set at length eight gives about 2 to the 52, which does not.
Length dominates character variety, and the maths is unambiguous. Adding one character multiplies the search space by the size of the character set; adding symbols to an existing set multiplies it by a much smaller factor once. A sixteen-character lowercase-only password has more entropy than a ten-character password using everything on the keyboard.
The catch is that this arithmetic only holds for genuinely random passwords. A human-chosen password that satisfies a complexity rule — capital at the front, digit and exclamation mark at the end — sits in a search space of a few billion candidates rather than 2 to the 52, because attackers model the pattern directly. Entropy measures the generation process, not the resulting string. This is why generated passwords in a manager beat memorable ones, and why complexity requirements have largely been abandoned in current guidance in favour of length minimums and breach-list checks.
Where the randomness comes from
Every primitive above depends on unpredictable random numbers, and there are two kinds available in a browser. Math.random is a fast, statistically reasonable pseudorandom generator with a small internal state that is seeded in an implementation-defined way. Its output is predictable to anyone who observes enough of it, and it has never been suitable for security purposes.
crypto.getRandomValues draws from the operating system entropy pool, the same source used for TLS key material. It is the correct source for identifiers, tokens, passwords, salts, and initialisation vectors. crypto.randomUUID, where available, is a convenience wrapper that produces a compliant v4 UUID from the same source.
The failure mode is silent, which is what makes it dangerous. A token generated from Math.random looks identical to one generated properly. Nothing breaks, no test fails, and the weakness is only visible by reading the code.
Frequently asked questions
Can I use a UUID as a session token?
The entropy is sufficient but the handling is not. Identifiers get logged by servers and proxies, leak through Referer headers, and are stored unhashed. Generate session tokens as secrets, with expiry and single-use semantics.
Why should I not hash passwords with SHA-256?
Because it is fast. Commodity hardware computes billions of digests per second, so a stolen database falls quickly regardless of salting. Use bcrypt, scrypt, or Argon2, which are deliberately slow and tunable.
Is a JWT payload encrypted?
No. It is base64url encoded and readable by anyone holding the token. The signature provides integrity, not confidentiality.
What actually makes a password strong?
Length, applied to a randomly generated string. Adding a character multiplies the search space; adding symbols once multiplies it far less. Human-chosen passwords that satisfy complexity rules are much weaker than their character count implies.
Why does Math.random matter here?
It is predictable from observed output and has a small internal state. Use crypto.getRandomValues for anything security-relevant. The failure is silent, since weak output looks exactly like strong output.