Skip to contentExploitQuest

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.

Wrenlearner

Is that realistic over a network? Surely the noise swamps a difference that small.

Rookmentor

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?