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
| tool | reversible? | needs a key? | use it for |
|---|---|---|---|
| Encoding (Base64, hex, URL-encoding) | yes, by anyone | no | making bytes safe to transport. Not secrecy. |
| Hashing (SHA-256; bcrypt/Argon2 for passwords) | no | no (salt for passwords) | fingerprints, integrity checks, password storage (slow hashes only) |
| Encryption (AES-256-GCM, ChaCha20-Poly1305) | yes, with the key | yes | keeping data secret at rest or in transit |
| Signing / MAC (HMAC-SHA256, Ed25519) | data stays readable | yes | proving data wasn't changed and came from you (tokens, cookies, webhooks) |
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.
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) | |
|---|---|---|
| Ruby | rand, Random.new, Array#sample | SecureRandom.urlsafe_base64(32), SecureRandom.uuid |
| JavaScript | Math.random() | crypto.getRandomValues(), crypto.randomUUID() |
| Python | random | secrets.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.
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.