guest@websec: ~/websec/16-cryptography — zsh — 100×40 by SaKKo ENTH code:

guest@websec ~ % man 16-cryptography

16 · Cryptography and Data Protection

Crypto bugs are rarely broken math. They're mix-ups: encoding mistaken for encryption, fast hashes for passwords, guessable random numbers, keys sitting next to the data.

16.1 Four different tools

toolreversible?needs a key?use it for
Encoding (Base64, hex, URL-encoding)yes, by anyonenomaking bytes safe to transport. Not secrecy.
Hashing (SHA-256; bcrypt/Argon2 for passwords)nono (salt for passwords)fingerprints, integrity checks, password storage (slow hashes only)
Encryption (AES-256-GCM, ChaCha20-Poly1305)yes, with the keyyeskeeping data secret at rest or in transit
Signing / MAC (HMAC-SHA256, Ed25519)data stays readableyesproving data wasn't changed and came from you (tokens, cookies, webhooks)
sim: the same secret, four waysreal Web Crypto in your browser

Base64 "encryption"

SHA-256 hash

AES-256-GCM encryption

HMAC-SHA256 signature

16.2 Protecting data at rest

First, don't keep what you don't need. For the rest, disk encryption stops stolen disks, but anyone with SQL access still sees plain text. Field encryption means dumps, backups and nosy queries only see gibberish.

Rails has Active Record Encryption, but you switch it on: bin/rails db:encryption:init, then encrypts :ssn. Use deterministic: true only when you need to search the column.
class Patient < ApplicationRecord
  encrypts :ssn                                   # non-deterministic (default, strongest)
  encrypts :email, deterministic: true, downcase: true   # queryable: where(email: …)
end

# bin/rails db:encryption:init → add the output to credentials:
# active_record_encryption:
#   primary_key: …   deterministic_key: …   key_derivation_salt: …

16.3 Keys

  • Keep keys away from the data: credentials or a secret manager.
  • One key per job, so one leak doesn't open everything.
  • Plan rotation before you need it.

16.4 Randomness

Tokens must be unguessable. rand and Math.random() are for games; with enough samples, attackers predict the next one. Use the crypto generator.

predictable (don't)cryptographic (do)
Rubyrand, Random.new, Array#sampleSecureRandom.urlsafe_base64(32), SecureRandom.uuid
JavaScriptMath.random()crypto.getRandomValues(), crypto.randomUUID()
Pythonrandomsecrets.token_urlsafe(32)

Make tokens long (128+ bits). A 6-digit code needs strict rate limits.

16.5 Timing attacks and constant-time comparison

Normal string compare stops at the first wrong character, so a closer guess takes a tiny bit longer. Measure enough and you can recover a secret letter by letter. Use a constant-time compare for secrets.

sim: leaking a token through timingsecret token: 6 hex characters
comparison:
Rails has what you need: SecureRandom, secure_compare, has_secure_token, generates_token_for, MessageVerifier. Use these instead of raw OpenSSL.
token = SecureRandom.urlsafe_base64(32)               # 256 bits
class ApiKey < ApplicationRecord
  has_secure_token :token, length: 36                 # random, unique
end

ActiveSupport::SecurityUtils.secure_compare(provided, expected)   # constant time
provided == expected                                # leaks timing

# verify a webhook signature
expected = OpenSSL::HMAC.hexdigest("SHA256", webhook_secret, request.raw_post)
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, request.headers["X-Signature"].to_s)

# sign/encrypt your own data instead of rolling crypto
verifier = Rails.application.message_verifier(:unsubscribe)
link_token = verifier.generate(user.id, expires_in: 7.days, purpose: :unsubscribe)
import crypto from "node:crypto";
const token = crypto.randomBytes(32).toString("base64url");
const a = Buffer.from(provided), b = Buffer.from(expected);
const ok = a.length === b.length && crypto.timingSafeEqual(a, b);   // constant time

16.6 Don't roll your own

  • Use AES-GCM through a good library. Never ECB, never reuse an IV.
  • MD5 and SHA-1 are fine for cache keys, not for security.
  • Don't invent token formats. If you use JWT, reject alg: none.