redzilla
Security

Practical Security: Passwords, Hashes & Tokens

“P@ssw0rd!” looks safe and falls in seconds; “correct horse battery staple” looks like a joke and lasts centuries. The difference is called entropy, and you can compute it. Welcome to security with numbers.

≈ 12 min read

During an audit at a small business we found the NVR password on a sticky note: "P@ssw0rd!". The IT guy was relaxed — it had an uppercase letter, a number and a symbol, so it "met the policy". We ran it against a leaked-password dictionary and it fell before the coffee got cold: it's on every list on the planet. Meanwhile "correct horse battery staple" — which looks like a joke — would have outlived the company. The difference between the two isn't opinion or magic: it's arithmetic, and by the end of this guide you'll be able to do it in your head.

Entropy: strength is length × log₂(alphabet)

An attacker doesn't "read" your password: they guess it by trying combinations. A password's strength is, quite literally, the size of the haystack it's hiding in. That size is measured in bits of entropy, and the formula fits on a napkin: entropy = length × log₂(alphabet size). Every additional bit doubles the combinations an attacker has to try.

Think of a bike lock: adding weird symbols makes each dial slightly bigger; adding characters adds whole dials. A lowercase letter is worth log₂(26) = 4.70 bits; a character drawn from all four classes (94 printable ASCII symbols) is worth 6.55. The per-character difference is small — the difference from length is brutal, because length multiplies.

So what about "P@ssw0rd!"? On paper, 9 characters × 6.55 = 59 bits. But the formula assumes every character was chosen at random, and that's the catch: "P@ssw0rd!" is the word "password" with the a→@ and o→0 mutations that every cracking tool tries first, by rule. Its real entropy is a handful of bits: it sits a couple million guesses from the front of the line, not 2⁵⁹ deep. A passphrase does the opposite: it picks words at random from the EFF list of 7776 words, and each word is worth log₂(7776) = 12.92 bits even though it reads like a bedtime story.

AlphabetSizeBits per unit
lowercase only264.70 / char
+ uppercase525.70 / char
+ digits625.95 / char
+ symbols (printable ASCII)946.55 / char
random word (EFF list)777612.92 / word
Entropy bars: 8 lowercase gives 37.6 bits; 8 characters with 4 classes, 52.4; 12 with 4 classes, 78.7; a 4-word passphrase, 51.7 and a 6-word one, 77.5 Entropy = length × log₂(alphabet) every bit doubles the attacker's work 50 bits 75 bits 8 lowercase 37.6 8 chars · 4 classes 52.4 12 chars · 4 classes 78.7 4 EFF words 51.7 6 EFF words 77.5 0 20 40 60 80 bits at 10¹⁰ guesses/s: 37.6 bits ≈ 10 s · 51.7 ≈ 2 days · 77.5 ≈ 350,000 years
Entropy of typical policies. Below 50 bits a GPU cracks it in days; above 75 bits the attack dies of old age.

The worked example: your password vs a GPU

Realistic scenario: a service got its database leaked and the passwords were stored with a fast hash (SHA-256, no salt, no stretching — it happens more than you'd think). A single desktop GPU tries on the order of 10¹⁰ hashes per second, and on average finds the password after searching half the space. Let's run the whole thing:

  1. The formula: entropy = length × log₂(alphabet). For 8 lowercase letters: 8 × log₂(26) = 8 × 4.70 = 37.6 bits, i.e. 26⁸ ≈ 2.09 × 10¹¹ combinations.
  2. Average time = (2^bits ÷ 2) ÷ rate. At 10¹⁰ guesses/s: 2.09 × 10¹¹ ÷ 2 ÷ 10¹⁰ ≈ 10.4 seconds. Your 8-lowercase password lasts less than a coffee run.
  3. Move up to 12 characters with 4 classes (pool 94): 12 × 6.55 = 78.7 bits → 94¹² ≈ 4.76 × 10²³ combinations. Average: 2.38 × 10¹³ seconds ≈ 754,000 years. Same GPU, same attack.
  4. The passphrase: 4 random EFF words = 4 × 12.92 = 51.7 bits → ≈ 2.1 days against a fast hash. Borderline. With 6 words: 77.5 bits → ≈ 350,000 years, and you can memorize it on the walk to your car.
  5. The takeaway: going from 4 to 6 words multiplies the attacker's work by 7776² ≈ 60 million times. Entropy doesn't add — it multiplies.

Two honest caveats. First, this is an offline attack against a fast hash: if the service stored bcrypt or scrypt, the rate collapses to around 10⁴ guesses/s and those same 8 lowercase letters go from 10 seconds to about 4 months (121 days). Second, an online attack against a rate-limited login is orders of magnitude slower still. Play with policies, rates and scenarios in the entropy and crack-time calculator.

Diceware, the classic method: 5 rolls of an ordinary die pick one word from the EFF list (6⁵ = 7776 — that's why the list is exactly that long). Or let the password generator do it with cryptographic randomness. The non-negotiable part: the words must be chosen at random, not "the four that popped into your head" — your brain is a terrible random number generator.

The password manager: the logical conclusion

Follow the math all the way. You need unique passwords per service — because when that RC-plane forum you signed up for in 2019 gets breached, the first thing attackers do is try that same email and password against your bank, your VPN and your work email (it's called credential stuffing and it's the most profitable attack there is). And each one needs 75+ bits. Nobody memorizes eighty random 12-character strings: the arithmetic leaves exactly one way out.

That way out is the password manager: one master passphrase of 6 words (77.5 bits, memorable, written down nowhere) and everything else machine-generated — 20 characters, unique, never meant to be remembered or typed by hand. The manager isn't a convenience: it's the only architecture where the formula balances. And wherever the service offers it, a second factor on top.

Hashes: a fingerprint, not encryption

New topic, same numeric spirit. A hash is a one-way function: anything goes in — a single letter, a 2 GB firmware — and a fixed-length fingerprint comes out. Like an actual fingerprint: it identifies you without letting anyone reconstruct you. Here lives the most repeated misconception in the trade: hashing is not encryption. Encrypted data can be decrypted with the key; a hash has no way back — there is no "unhash". And it has the avalanche effect: change one letter of the input and the whole fingerprint changes.

Use 1: verifying integrity. You downloaded a firmware and the vendor publishes its SHA-256: if your hash matches, the file arrived intact and nobody tampered with it along the way. It looks like this (and yes, one different letter would have produced an unrecognizable fingerprint):

fingerprints of the same input
$ echo -n "redzilla" | md5sum
e9b5a2402e7ab94f63d139974709317a  -
$ echo -n "redzilla" | sha256sum
bcff34d1effbe914707d30410323f9ead560f3f610c8aa6aa710f95993551d15  -

Use 2: storing passwords. A well-built service never stores your password: it stores its hash, under two conditions. A unique salt per user — a random value mixed with the password before hashing, so two users with the same password don't share a hash and precomputed tables become useless. And an algorithm that is slow on purpose: bcrypt, scrypt or Argon2 run thousands of iterations precisely so the GPU from our example drops from 10¹⁰ to 10⁴ guesses per second. MD5 and SHA-1 are the opposite: designed in the 90s to be fast, which is exactly what you don't want facing an attacker.

Finding unsalted md5(password) in a production system isn't "tech debt" — it's a breach waiting for a date. A GPU computes tens of billions of MD5 hashes per second, and precomputed tables with billions of common passwords already exist. If you inherit a system like that, migrate every hash to bcrypt or scrypt on each user's next successful login — don't wait for the big redesign.

Field bonus: hashes can be identified by eye from length and format. Practice with the hash calculator and the hash identifier:

Looks likeFormatProbably is
e9b5a2402e7ab94f…32 hexMD5
8581bd83aafb4d6d…40 hexSHA-1
bcff34d1effbe914…64 hexSHA-256
$2b$12$N9qo8uLO…starts with $2b$bcrypt (salt and cost built in)

Inside a JWT: a laminated ID, not a sealed envelope

JWT tokens carry half the sessions on the internet and drag along the same misunderstanding as hashes. A JWT is three Base64URL blocks joined by dots: header (the algorithm), payload (the data: who you are, when it expires) and signature. The right analogy is a laminated ID card: anyone who picks it up can read your name and date of birth — the plastic hides nothing. What the hologram (the signature) does is expose whether someone altered it.

Anatomy of a JWT: header and payload are Base64URL readable by anyone; only the signature requires the key to verify header.payload.signature — three Base64URL blocks eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMDI0IiwibmFtZSI6IkFtYW5kYSIs… . X8p2vQ… HEADER { "alg": "HS256", "typ": "JWT" } readable by anyone PAYLOAD { "sub": "1024", "name": "Amanda", "iat": 1783684800, "exp": 1783688400 } readable WITHOUT a key: not encrypted SIGNATURE HMAC-SHA256( header + "." + payload, secret ) only verifiable with the key decoding ≠ decrypting: the signature guarantees integrity, not secrecy iat and exp are Unix seconds: 1783684800 = Jul 10, 2026 12:00 UTC
A JWT decodes without any key at all. The signature answers exactly one question: did anyone tamper with this?

Try it yourself — decoding the header requires no secret whatsoever, just undoing the Base64:

decoding is not decrypting
$ echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
{"alg":"HS256","typ":"JWT"}

Hence the two golden rules. One: never put secrets in the payload — no keys, no sensitive data, nothing you wouldn't show the token's holder, because they (and any proxy along the way) can read it with two lines of terminal. Two: the iat (issued at) and exp (expires) claims are Unix seconds, and a short exp isn't backend fussiness: a stolen JWT can't easily be "revoked", so its lifetime is your only brake. Paste any token into the JWT decoder — it runs 100% in your browser — and see what it's declaring inside.

Base64, UUIDs and other disguises

You've already met the reason the payload is readable: Base64 is encoding, not encryption. It turns arbitrary bytes into 64 harmless characters so they survive trips through email, JSON or URLs — and anyone can reverse it without a key, paying a 33% size tax along the way. Writing in Greek doesn't hide the message from someone who reads Greek. If you ever see data "protected" with Base64, run it through the encoder and say hi to the plaintext.

And to complete the kit, UUIDs in one sentence each. UUID v4: 122 bits of cryptographic randomness — impossible to guess, perfect as a public identifier. UUID v7: puts a millisecond timestamp up front, so IDs are born in chronological order and don't wreck your database indexes. Generate both (plus ULIDs) in the UUID generator — but remember an identifier is not a credential: the thing that guards access is called a token, and it gets signed.

That's practical security in three habits: a long passphrase for the one thing you memorize, a manager for everything else, and an instant reflex of distrust toward anything that "looks" encrypted but is merely encoded. Run your current policy through the entropy calculator and crack open one of your own tokens in the JWT decoder: numbers persuade better than any lecture.

Tools to practice with