10 Common Cryptography Mistakes Developers Make (and How to Avoid Them)
Nobody ships broken crypto on purpose. It sneaks in: a key pasted into a config file at the end of a long day, a library default nobody questioned, a "random" number generator that was never quite random. The algorithms themselves are rarely the problem. The failures live in the seams around them — how keys are stored, how modes are chosen, how bytes are generated.
These are the mistakes that show up again and again in code review and postmortems, with the fix for each. None of it requires a mathematics degree. Most of it requires only that you stop trusting defaults.
1. Hardcoded keys and secrets in the repository
A database password in a config file, a signing key checked into a mobile project, a JWT secret in a committed .env — these are the same failure wearing different clothes. Once a secret reaches git history, it lives in every clone, fork, backup, and CI cache. Deleting the line does not delete the secret.
- Treat any committed secret as compromised and rotate it. Rotation is the fix; deletion is not.
- Load secrets at runtime from a secrets manager or your platform's injection mechanism, not from files that travel with the code.
- Add a pre-commit scanner such as gitleaks or trufflehog, and run the same check in CI. Catching a key before the push is worth far more than catching it after.
- Remember that client-side "secrets" aren't secrets. Anything shipped in a mobile app, desktop binary, or browser bundle can be extracted. Clients should hold short-lived tokens issued by a server, not long-lived keys.
2. ECB mode, and encryption without authentication
ECB encrypts each block independently, so identical plaintext blocks produce identical ciphertext blocks. Encrypt an image and the outline survives. Encrypt a database column and you can see which rows share a value. But ECB is really a symptom of a wider habit: choosing a mode that provides confidentiality only.
CBC, CTR, and CFB will cheerfully decrypt data that an attacker has modified. That enables bit-flipping, padding-oracle attacks, and ciphertext splicing. The fix is to use an authenticated mode — AES-GCM or ChaCha20-Poly1305 — and let a well-reviewed library handle the tag. If a legacy system forces you into CBC, use encrypt-then-MAC: HMAC over the IV and ciphertext, verified with a constant-time comparison before you decrypt. Never compare tags with ordinary string equality; that leaks timing information.
3. Weak randomness
Every language ships two kinds of random. One is fast and predictable. The other is slower and built for secrets. Use the wrong one and your session tokens, password-reset links, API keys, salts, and IVs become guessable.
- Python: secrets and os.urandom, not random.
- JavaScript: crypto.getRandomValues from the Web Crypto API, not Math.random.
- Java: SecureRandom, not java.util.Random.
- Go: crypto/rand, not math/rand.
- C and C++: getrandom or /dev/urandom, never rand() seeded with the current time.
One more trap: don't assume a UUID library is cryptographically strong just because it says "random." Check which generator it uses before you hand it the job of minting tokens.
4. Reusing nonces and IVs
With GCM or CTR, reusing a nonce under the same key is about as bad as it gets. XOR two ciphertexts together and the keystream cancels out, exposing the XOR of the plaintexts; with GCM, an attacker can also recover material that lets them forge messages. In CBC, a fixed IV reveals when two messages begin with the same bytes.
So: generate a fresh random IV for every CBC encryption and store it next to the ciphertext. For GCM, use a 96-bit nonce and stay well below the birthday bound for random nonces under a single key — the usual guidance is to rekey long before you approach a few billion messages — or use a counter you can guarantee never repeats. If you're tempted to make the nonce deterministic so you can search the data, stop. That's a different feature with a different design, and it deserves its own key.
5. Homebrew crypto and password hashing
Rolling your own cipher, protocol, or padding scheme is almost always a mistake, and the mistakes are invisible until they're exploited. The good primitives already come wrapped: libsodium, Google Tink, age, and the crypto modules bundled with modern runtimes. A useful test — if your design includes a step you can't explain to a colleague in one sentence, it probably shouldn't ship.
Password storage is its own trap. A salted SHA-256 hash is not a password hash; it's fast by design, which is exactly what an attacker with a GPU wants. Use Argon2id, scrypt, or bcrypt at the highest cost your hardware tolerates. And never store passwords in a form you can read back — if you can decrypt them, so can someone else. For deriving keys from a shared secret, use HKDF rather than a bare hash.
6. Key management: storage, rotation, blast radius
Most crypto incidents are operational, not mathematical. Answer these before you ship:
- Where does the key live, and who can read it? A KMS or HSM beats an environment variable, which beats a file.
- How is it rotated, and has anyone actually done it?
- How many systems share it? One key for everything means one leak compromises everything.
- What happens when it's lost or an employee leaves?
Envelope encryption solves most of this: a key-encryption key held in the KMS wraps per-tenant data keys, so you can rotate the outer key without re-encrypting your data, and you keep old key versions around so existing ciphertext stays readable. Log key usage, restrict who can decrypt, and rehearse rotation on a quiet Tuesday rather than during an incident.
7. Leaking secrets through logs, errors, and telemetry
Keys escape through side doors: tokens in query strings end up in access logs and referrer headers, debug endpoints dump configuration, stack traces quote connection strings, and application monitoring tools happily capture request bodies containing passwords. A single verbose error page can undo an otherwise careful design.
Redact sensitive fields centrally in your logger rather than relying on every developer to remember. Keep secrets out of URLs. Turn off detailed errors in production. Check what your observability vendor stores and how long it keeps it. And when validating a token, compare in constant time and return the same error whether the token was malformed or merely wrong — otherwise you're handing out a free oracle.
Before you ship: a short checklist
- No secrets in the repository, the container image, or the client binary.
- AEAD everywhere, with fresh nonces generated per message.
- A cryptographically secure generator behind every token, salt, and key.
- Argon2id, scrypt, or bcrypt for passwords — never a plain hash.
- Keys in a managed store, separated by tenant, with a rotation plan someone has tested.
- Redaction in logging, and no secrets in URLs or error messages.
None of these fixes are exotic or expensive. Teams that get cryptography right aren't cleverer than everyone else; they use boring, well-tested libraries and treat keys like the production credentials they are. Start with the hardcoded secrets — that's usually an afternoon of work, and it removes the largest single risk on this list.
Photo: Lee Campbell / Pexels




