Lesson 1 of 1 in Cryptography You Should Not Write
Identifiers Are Not Secrets
Two kinds of random string, three rules about comparing them, and the bug that comes from treating one as the other.
3 min read
Not yet reviewed
Almost nobody should write cryptography, and almost everybody has to choose it. The choices are few and the wrong ones are recognisable, which makes this a shorter chapter than its reputation suggests.
Two kinds of random string
A public identifier
newId() // a UUID. Safe in a URL, safe in a log,
// safe in an email. Names a row.A bearer secret
newSecret() // 256 bits, URL-safe. Never logged,
// never in a URL, never compared with ===They look alike — both are opaque random-looking strings — and they have opposite rules. Conflating them is how a predecessor platform ended up treating an obfuscated database id as proof of having solved a challenge.
The distinction is not about entropy, it is about what possession means. Knowing an identifier means you were told a name. Knowing a secret means you are authorised. If a value grants something merely by being known, it is a secret and every secret rule applies to it.
Note
Obfuscating an identifier is not authentication. If the only thing between a stranger and somebody's data is that the URL is hard to guess, you have built a secret and given it none of a secret's protections.
Comparing secrets
Leaks how much was right
if (token === expected) { ... }Constant time
if (secretsMatch(token, expected)) { ... }=== returns as soon as two bytes differ. The time it takes is a function of how many leading bytes were correct, which is enough to recover a token one byte at a time given enough attempts.
Is that realistic over a network? Surely the noise swamps a difference that small.
It is harder over a network than in a lab, and it is not impossible — people have done it. But the argument that matters is different: the constant-time version costs you nothing. There is no trade to weigh, so weighing it is the mistake.
Passwords
Use a memory-hard function built for the job — argon2id, or bcrypt if that is what you have. Not SHA-256, not SHA-256 with a salt, not SHA-256 in a loop you wrote. The whole design goal is to be slow and memory-hungry in a way that hurts a cracking rig more than it hurts your login endpoint.
Take care
A corrupt stored hash must read as "wrong passphrase", never as a server error. The difference is observable from outside, and it tells an attacker exactly which accounts are in a broken state — which is a list worth having if you are the attacker.
How this platform does it
newId() and newSecret() are separate functions with the distinction written into their documentation, secretsMatch wraps a constant-time comparison, and the corrupt-hash rule above is a stated convention rather than a build check — which is worth saying plainly, because a course about secure coding should not imply that every rule it teaches is mechanically enforced. That one is a habit, and habits are the ones that rot.
A password reset link contains a random 32-byte token. Which rules apply to it?