Three categories that look similar in storage but require completely different handling. Mixing them up is one of the easiest ways to leak something you didn't realize was sensitive.
| Category | Examples | Sensitivity | Handling |
|---|---|---|---|
| Identifier | Username, email, user_id, IP address | Low (often public) | Plain text fine; index for lookup |
| Credential | Password, PIN, biometric, hardware key | High (proves identity) | Never store plain; always hash with bcrypt/argon2/scrypt |
| Secret | API key, JWT signing key, session token, OAuth client secret | High (grants access without further proof) | Encrypted at rest; never in repo; rotate-able |
The hashing bright line
If a value is something the user knows (password, PIN), you should never be able to recover the original from your storage. The only way to verify it is to hash the input and compare hashes:
Why bcrypt and not SHA256? Because SHA256 is too fast: a GPU can try billions per second. Bcrypt is intentionally slow (~100ms per hash at default cost), making brute force economically painful even on stolen hashes.
The three failure patterns
- Identifier treated as secret — Hiding usernames or IDs as if they're sensitive. Wastes effort; they leak via logs anyway.
- Credential stored as identifier — Plain-text passwords in DB. The classic catastrophic breach.
- Secret committed to repo — API key in
.envthat got pushed once and lives forever in git history. Track 9 covers how LLM-assisted coding makes this worse.