Cryptography you can actually use.
Hashing vs encryption, symmetric vs asymmetric, and the three primitives you should reach for before rolling your own anything.
Most working engineers never need to implement a cipher. They need to know which one to use, what the inputs and outputs look like, and where the booby traps are. This article covers exactly that.
Hashing is not encryption
A hash is a one-way function: arbitrary input in, fixed-size pseudo-random output out. You cannot get the input back. Use cases: integrity checks, fingerprinting, password storage (with a slow hash).
$ echo -n "hello" | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -
Encryption is reversible: you have a key, you can decrypt. Use cases: transmitting and storing data confidentially.
If you can ever need the data back, you want encryption. If you only need to confirm two values are the same (passwords, file integrity), you want hashing.
The two flavours of encryption
Symmetric: one key, the same to encrypt and decrypt. Fast (gigabytes per second on modern CPUs). Hard to get the key to the other person securely.
Asymmetric: a key pair. Public to encrypt or verify, private to decrypt or sign. Slow (kilobytes per second). Easy to share the public half.
In practice almost everything uses both, like TLS does: asymmetric for a handshake that establishes a symmetric session key, then symmetric for the bulk traffic.
Worked example: how TLS actually uses both
The clearest place to see symmetric and asymmetric working together is the padlock in your browser's address bar. When you load a site over HTTPS, a lot happens in the time it takes to blink, and every step maps onto the two flavours above: the full mechanics, including how certificates get issued and audited, are covered in TLS certificates and Certificate Transparency; here we only care about where each type of cryptography does its job.
- The server proves who it is. Your browser receives the server's TLS certificate, issued by a certificate authority and containing an asymmetric public key. The server proves it holds the matching private key without ever transmitting that private key anywhere.
- Both sides agree on a shared secret, asymmetrically. Using a key-exchange algorithm – modern TLS uses ECDHE, elliptic-curve Diffie-Hellman – each side sends a public value and independently computes the same shared secret. An eavesdropper watching every byte of that exchange still cannot compute the secret; that is the hard problem asymmetric cryptography is built on.
- The shared secret becomes a symmetric session key. It gets fed through a key-derivation function to produce a session key for a fast symmetric cipher – in modern TLS, usually AES-256-GCM or ChaCha20-Poly1305.
- The rest of the connection is symmetric. Every request and response – HTML, images, API calls – is encrypted and authenticated with that one symmetric key, at line rate, for as long as the connection stays open.
The division of labour is deliberate. Asymmetric cryptography is slow but solves the hard problem of agreeing on a secret over a network between two parties who have never met; symmetric cryptography is fast but needs that secret to already exist. Neither one alone works for the web at scale – asymmetric-only would be too slow to stream video, symmetric-only would require every browser and server to somehow pre-share a key, which does not scale to billions of devices. For the layer underneath all of this – how those packets actually find the server in the first place – see how networks actually work.
The three primitives you should know by name
| Primitive | Type | Use it for |
|---|---|---|
| AES-256-GCM | Symmetric AEAD | Encrypting files, sessions, anything bulk |
| X25519 + Ed25519 | Asymmetric (ECC) | Key exchange and signatures |
| Argon2id | Password hash | Storing user passwords |
That is the floor. Reach for these by name in code review and you will be right 95% of the time. The remaining 5% – interoperability with old systems, regulated environments, hardware constraints – needs an expert anyway.
The algorithm cheat sheet: AES, RSA, ECC, SHA-256 and friends
Names like AES and RSA get thrown around as if they were interchangeable options on the same menu. They are not – they solve different problems, and knowing which category each one belongs to is most of the battle.
| Algorithm | Category | Key / output size | Typical use case |
|---|---|---|---|
| AES (usually AES-256-GCM) | Symmetric cipher | 128/192/256-bit key | Encrypting bulk data – files, database columns, TLS traffic |
| RSA | Asymmetric (integer factorization) | 2048–4096-bit key | Legacy TLS key exchange, code signing, some JWT signing (RS256); being phased out in favour of ECC for new systems |
| ECC (X25519 / Ed25519 / P-256) | Asymmetric (elliptic curve) | ~256-bit key, roughly equivalent strength to a 3072-bit RSA key | Modern key exchange and signatures – smaller keys and faster operations than RSA at the same security level |
| SHA-256 | Cryptographic hash | 256-bit output | Integrity checks, digital signatures, commit IDs – never for password storage |
| ChaCha20-Poly1305 | Symmetric AEAD | 256-bit key | Same job as AES-GCM; preferred on hardware without AES acceleration, such as many mobile CPUs |
| Argon2id | Password hash (memory-hard) | Tunable | Storing user passwords – deliberately slow, unlike SHA-256 |
Two things are worth pulling out of that table. First, RSA and ECC solve the same category of problem – asymmetric key exchange and signatures – but ECC does it with far smaller keys and less CPU, which is why nearly all new protocol design defaults to ECC and treats RSA as the thing you keep around for backward compatibility. Second, SHA-256 and Argon2id are both called "hashes" in casual conversation but are built for opposite goals: SHA-256 is optimized to be fast, which is good for verifying a large file's integrity, while Argon2id is deliberately slow and memory-hungry, which is good for password storage – where speed only ever helps an attacker guessing passwords.
AEAD – the one word that protects you from yourself
Older ciphers like AES-CBC encrypt your data but do not detect tampering. AEAD (Authenticated Encryption with Associated Data) does both at once. AES-GCM and ChaCha20-Poly1305 are AEADs. They produce a ciphertext and a tag; decryption fails loudly if the tag does not check out.
You should never see plain AES-CBC, AES-CTR, or 3DES in new code in 2026. If you do, replace it.
Password storage, the right way
Three rules:
- Hash, never encrypt.
- Use a slow hash designed for the job – Argon2id, scrypt, or bcrypt – not SHA-256.
- Tune the parameters so a single login takes 100–500 ms on your hardware.
# libargon2 reference settings, 2026
argon2id memory=64MB iterations=3 parallelism=4
Why memory? Because attackers crack passwords on GPUs, which have lots of cores but little memory per core. A memory-hard function levels the playing field.
Things that will trip you up
- Reusing a nonce with AES-GCM destroys confidentiality. Generate a fresh 96-bit random nonce every encryption, or use a counter.
- Comparing secrets with
==leaks timing. Use a constant-time compare (hmac.compare_digestin Python,crypto.timingSafeEqualin Node). - Storing a key in the database next to the ciphertext defeats the entire exercise. Keys live in a KMS, an HSM, an env var injected at runtime – anywhere the data is not.
- Inventing your own protocol by composing primitives. Use a high-level library – libsodium, age, Tink – that already composed them for you.
Those four mistakes account for the great majority of crypto-related breaches you will read about. Get them right and you are ahead of most of the industry.
Why you shouldn't roll your own crypto
"Just use a well-known library" sounds like generic risk-averse advice until you have seen what happens when someone doesn't. None of the following requires a novel attack – these are well-understood failure modes that show up whenever someone composes primitives by hand instead of reaching for a reviewed library.
Weak randomness
Every primitive above assumes its keys and nonces come from a cryptographically secure random number generator (CSPRNG) – one designed so its output is unpredictable even to someone who can observe part of it. General-purpose random functions, such as a language's default random() or anything seeded from the system clock, are built for statistical distribution, not unpredictability, and their internal state can often be reconstructed from a handful of outputs. A key generated this way isn't weakened a little: it can collapse the entire keyspace an attacker has to search, sometimes down to something small enough to brute-force in minutes.
ECB mode and pattern leakage
AES is a block cipher: it encrypts fixed-size chunks one at a time. In ECB (Electronic Codebook) mode, identical plaintext blocks always produce identical ciphertext blocks. That sounds like a minor detail until you encrypt something with repeating structure: a bitmap image is the classic demonstration: encrypt it in ECB mode and the outlines are still visible in the ciphertext, because identically coloured regions of pixels map to identical ciphertext blocks. The same leakage applies to any structured data with repeated patterns, not just images. This is exactly why AEAD modes like GCM exist: they chain each block against the previous state or a nonce, so identical plaintext blocks never produce identical ciphertext.
Timing side-channels
A naive comparison function that checks a MAC, a password hash, or an API token byte by byte and returns as soon as it finds a mismatch leaks information through how long it takes to say no. An attacker who can measure response time precisely enough can recover a secret one byte at a time: try every possible first byte, keep whichever one takes fractionally longer to reject (meaning it matched further before failing), and repeat down the string. It is slow over the open internet but entirely practical on a local network or against anything with stable latency. This is why every serious crypto library ships a constant-time comparison function, and why a plain equality check should never appear in code that handles keys, tokens, or password hashes.
None of these are exotic. They are the standard set of mistakes a competent, well-intentioned engineer makes the first time they build a cipher or a comparison routine from scratch instead of using a battle-tested library. The fix in all three cases is the one from the primitives table earlier: don't hand-roll a randomness source, a block cipher mode, or a comparison function – pull them from libsodium, age, Tink, or your language's own vetted crypto standard library, all of which already got this right.
Frequently asked questions
What is the difference between hashing and encryption?
What is the difference between symmetric and asymmetric encryption?
What is the right way to store passwords securely?
What is AEAD encryption and why does it matter?
Daniel A. and Óscar S. run Breachfolio, a small independent site about security and AI. This article was drafted with AI assistance and reviewed by a person before it went live. We write from documentation, vendor sources and published research rather than from original lab benchmarks, and we link a source in the sentence that relies on it. How we work · About us