JA EN
LearnSecurity
·FREE·13 min read

Cryptography from Scratch — Symmetric Keys, Public Keys, and Hashes

Cryptography isn't about producing unreadable text — it's about engineering an extreme gap in effort between people who hold the key and people who don't. We build up the three tools (symmetric, public-key, hashing) from zero, explain why factoring shows up at all, what a signature actually promises, and how every widely deployed cipher has eventually broken.

ModalitytextTasksystems

Cryptography is not about hiding

"Encryption" sounds like magic that turns text into gibberish. But what designers actually build is something far more mundane: a property where anyone holding a secret can reverse the operation cheaply, and anyone without it cannot reverse it in any practical amount of time. That gap in effort is the whole product.

Think about your front door. With a key it opens in a second. Without one, it can still be opened — by picking the lock, by force. The door is "secure" not because it can't be opened, but because opening it costs more than it's worth. Cryptography works the same way. The goal is never "unbreakable"; it's "breaking this takes longer than the age of the universe."

Once you see it that way, the whole field organizes into three tools.

  1. Symmetric encryption — same key locks and unlocks. Fast.
  2. Public-key encryption — the locking key and the unlocking key are different, which lets you share secrets with someone you've never met.
  3. Hash functions — reversal isn't even attempted. They produce a fingerprint of data.

Every real system — HTTPS, SSH, cryptocurrencies, password storage — is these three tools with the labor divided among them. We'll build them up in order, along with the problem each one was invented to solve.

1. Symmetric encryption — one key, both directions

Start with the simplest cipher there is. Take the plaintext as a string of bits, take a key of the same length, and XOR them bit by bit (XOR: 1 if the bits differ, 0 if they match).

c=mkm=ckc = m \oplus k \qquad m = c \oplus k

Here mm is the plaintext, kk is the key, and cc is the ciphertext. In words: scramble once with the key, scramble again with the same key, and you're back where you started. XOR-ing the same value twice cancels out, so locking and unlocking are literally the same operation.

If the key is truly random, as long as the message, and never reused, this scheme (the one-time pad) is provably unbreakable — every possible plaintext looks equally plausible given the ciphertext. It is also useless in practice. If you can safely deliver a 1 GB key to send a 1 GB file, you could have safely delivered the file.

So in practice we use a function that stretches a short key (128 or 256 bits) into a stream that looks random. That's a block cipher, of which AES is the standard example: think of it as an enormous substitution table over 128-bit chunks, selected by the key, designed so that without the key the output is indistinguishable from noise.

Every extra key bit doubles the brute-force cost

An attacker who doesn't have the key has one last resort: try every key. With an nn-bit key that's 2n2^n candidates — and this is where exponential growth does the heavy lifting.

FIG 1One extra key bit doubles the work. Drag n and watch everything up to O(n²) hug the floor while O(2ⁿ) blows through the ceiling — that gap is what cryptography is selling

As the figure shows, nudging nn upward changes the order of magnitude of the work. If an attacker's hardware gets 1,000× faster, adding ten bits to the key puts you back ahead. The defender's cost grows by addition; the attacker's grows by multiplication. That asymmetry is the foundation of the whole field. For the notation itself, see Complexity From Scratch.

2. The key distribution problem — sharing a key with a stranger

Symmetric encryption is fast and strong, but it carries a fatal precondition: sender and receiver must already share the same key.

Your browser and a shopping site you've never visited share nothing in advance. Send the key in the clear and any eavesdropper can decrypt too. Encrypt the key before sending it, and you need a key for that — around and around. This is the key distribution problem, and until the mid-1970s it was treated as a hard limit on what cryptography could do.

Diffie and Hellman broke that wall in 1976 with "New Directions in Cryptography." Their claim was counterintuitive: two parties can agree on a shared secret over a channel that is being fully monitored.

Here's how. Everyone knows a public number gg and a large prime pp. Alice picks a private number aa and sends gamodpg^a \bmod p. Bob picks bb and sends gbmodpg^b \bmod p. Now both can compute

(gb)a(ga)bgab(modp)(g^b)^a \equiv (g^a)^b \equiv g^{ab} \pmod{p}

where modp\bmod p means "the remainder after dividing by pp." Both sides arrive at the same value gabmodpg^{ab} \bmod p, yet all that crossed the wire was gag^a and gbg^b. To recover aa from those, an eavesdropper would have to answer "what power of gg gives this number?" — the discrete logarithm problem, which we don't know how to solve at scale. That unsolvability is the security.

3. Public-key cryptography — why factoring, of all things?

Diffie–Hellman is a procedure for agreeing on a key. Public-key encryption goes one step further and splits the locking key from the unlocking key. Picture scattering open padlocks all over town: anyone can snap one shut, but only you hold the key that opens them.

To build that out of numbers, you need an operation that is cheap in one direction and hopeless in reverse. Factoring fits:

RSA, devised in 1977, is built on exactly that asymmetry. Pick primes pp and qq, set n=pqn = pq, take ϕ(n)=(p1)(q1)\phi(n) = (p-1)(q-1), choose an ee coprime to it, and find dd with ed1(modϕ(n))ed \equiv 1 \pmod{\phi(n)}. You publish (n,e)(n, e) and keep dd secret.

c=memodnm=cdmodnc = m^e \bmod n \qquad m = c^d \bmod n
(1)

Read it as: raise the message to the power ee and take the remainder mod nn to encrypt; raise that to the power dd and take the remainder to get the message back. dd was chosen precisely so the two exponentiations cancel. And to find dd you need ϕ(n)\phi(n), and to find ϕ(n)\phi(n) you need to factor nn. The strength of the key is bolted directly to the difficulty of factoring.

Now the honest part. Nobody has proven that factoring is hard. We're relying on the empirical fact that decades of worldwide effort haven't produced a fast method. Meanwhile the records keep falling: a 768-bit modulus was factored in 2009, an 829-bit one in 2020. That's why recommended key lengths keep rising, and why RSA keys in current use are 2048 bits or more.

Newer designs mostly use elliptic-curve cryptography (ECC) instead, because it reaches the same strength with far shorter keys — a 256-bit curve key is considered comparable to a 3072-bit RSA key. The underlying hard problem changes to discrete logarithms, but the shape of the argument is identical.

Both are also orders of magnitude slower than symmetric ciphers, since both grind through exponentiation of huge numbers. That's why real HTTPS traffic is hybrid: public-key cryptography to agree on a symmetric key, then the symmetric cipher for the actual payload. Public keys are the expensive tool you use for one brief moment at the start.

4. Hash functions — the point is that you can't go back

The third tool has a different purpose entirely. A hash function crushes input of any length into a fixed-size value (say 256 bits). Crushing destroys information, so there is no way back. That's the specification, not a defect.

A cryptographic hash is expected to have three properties:

The third is the weakest link, and it's worth internalizing why. By the same reasoning as the birthday paradox, collisions in an nn-bit hash turn up after roughly 2n/22^{n/2} attempts. A 256-bit hash gives you 128 bits of collision resistance, not 256.

Don't confuse this with the hash you use in a hash table. That one is tuned to be fast and spread evenly; it makes no promise of standing up to an adversary who is choosing inputs on purpose. Reading this alongside Hashing and Nearest-Neighbor Search makes the difference in design goals concrete.

5. Signatures — public-key cryptography run backwards

Encryption gives you "only you can read this." A signature gives you the mirror image: "anyone can verify that you wrote this." You just flip which key does which job.

Only the holder of the private key can produce the value, and everyone holding the public key can confirm it came from that key. Textbook RSA signing operates not on the message but on its hash.

s=H(m)dmodnverify:  semodn=?H(m)s = H(m)^d \bmod n \qquad \text{verify}: \; s^e \bmod n \overset{?}{=} H(m)

H(m)H(m) is the message hash and ss is the signature. Read it as: take the value built with the secret exponent dd, undo it with the public exponent ee, and check that it matches the hash. Signing the hash keeps signatures a fixed size no matter how long the message is — and it also means that when the hash breaks, the signature breaks. If two documents with different contents can share a hash value, a signature on one is a valid signature on the other.

It's equally important to know what a signature does not promise. It says only "the holder of this public key produced this." It says nothing about whose key that is. Filling that last gap is the job of certificates and certificate authorities (PKI): a trusted third party signs the claim "this public key really does belong to example.com." The padlock in your browser is the result of verifying that chain.

Code you can touch (educational — never ship this)

Here's a full RSA round trip on tiny numbers.

p, q = 61, 53                      # real keys use primes of hundreds of bits
n = p * q                          # 3233
phi = (p - 1) * (q - 1)            # 3120
e = 17                             # coprime to phi
d = pow(e, -1, phi)                # 2753 = inverse of e mod phi

m = 65
c = pow(m, e, n)                   # 2790  encrypt
print(pow(c, d, n))                # 65    decrypt

s = pow(m, d, n)                   # sign (properly: sign the hash)
print(pow(s, e, n) == m)           # True  verify

pow(x, y, n) is the built-in that computes xymodnx^y \bmod n efficiently. Ten lines for a full round trip — and shipping this would get you broken every time. Real RSA layers on padding (OAEP for encryption, PSS for signatures) that mixes in randomness so the same plaintext produces a different ciphertext each time. The code above is deterministic, so an observer can tell when you send the same message twice just by comparing ciphertexts. In cryptography, the distance between "the math checks out" and "this is secure" is enormous.

A history of breakage — every cipher ages out

The most common misconception in practice is that picking a strong algorithm ends the job. In reality, every widely deployed cipher has eventually reached the end of its life.

The pattern is always the same: a theoretical weakness is announced, falling compute costs turn it into a practical attack, and the algorithm is finally removed from the standards. Because that migration takes years, the correct posture is to start moving when the weakness is announced, not when the break lands. Reacting after the fact is already too late.

The migration currently underway is preparation for quantum computers. Shor's algorithm, published in 1994, showed that a sufficiently large quantum computer solves factoring and discrete logarithms efficiently — which puts RSA and elliptic-curve cryptography squarely in the breakable column. The effect on symmetric ciphers and hashes is much milder: in theory the effective strength halves (2n2^n becomes 2n/22^{n/2}), which is a large part of why AES-256 is recommended.

Replacements are already standardized: in 2024, NIST published lattice-based key agreement and signature schemes as federal standards (ML-KEM, ML-DSA, SLH-DSA). The attack to worry about operationally is "harvest now, decrypt later" — store the ciphertext today, decrypt it once the hardware exists. The longer your data must stay secret, the more urgent the migration.

One useful contrast: error correction and cryptography both start from "assume it breaks," but they assume different opponents. Error correction faces probabilistic noise; cryptography faces an intelligence that has read your design and will choose the worst possible input. Something that works on average is not good enough.

How this shows up on the job

Who touches it, and when. Backend web developers hit it in password storage, sessions, and token verification. SRE and infrastructure engineers hit it in certificate renewal, key-management permissions, and TLS configuration. Embedded and IoT developers hit it in firmware signing and in generating keys on devices with weak sources of randomness. Anyone working on payments or healthcare systems meets it in at-rest encryption and key-rotation audits.

The names you'll actually type. For symmetric encryption, AES-256-GCM or ChaCha20-Poly1305 — AEAD modes that encrypt and detect tampering in one step. Use libsodium or your language's standard crypto library, never a hand-rolled implementation. For passwords, argon2id or bcrypt, with the cost parameters (memory, iterations) tuned to your hardware. To inspect TLS and certificates: openssl s_client -connect host:443 and openssl x509 -text. Keep keys in a key-management service — AWS KMS, Google Cloud KMS, an HSM — not in environment variables. For message authentication, use HMAC-SHA256 rather than inventing something.

Pitfalls that turn into incidents.

Questions you'll get in interviews and design reviews. "Why shouldn't passwords be stored with SHA-256?" — because it's too fast; a GPU tries enormous numbers of candidates per second, so you want something deliberately slow and memory-hungry like Argon2 or bcrypt, with a distinct salt per user. "Why does HTTPS use both public-key and symmetric crypto?" — public keys are slow, so they're spent only on agreeing a key, and the payload rides on the fast symmetric cipher. "What actually happens when a certificate expires?" — the signature is still mathematically valid, but verifiers refuse to extend trust to it. Most cryptographic failures are failures of operations, not of mathematics.

Summary

Next we'll follow how these three tools actually fit together, walking the HTTPS handshake one round trip at a time.

Comments

Sign in to comment