Hashing, deep — tables, crypto, rings, passwords, Bloom

cs · memo

In one line: A hash maps arbitrary input to a fixed-size value. Table hashes want speed + uniformity and a secret seed against flooding (Swift: SipHash, reseeded per process); cryptographic hashes want one-wayness and collision resistance (SHA-256); password hashes want to be slow and memory-hard; a MAC needs a key. Choosing the wrong family is the classic mistake.

Download PDF Print view LaTeX source

Hashing, deep — tables, crypto, rings, passwords, Bloom — figure 1

What makes a hash good

  • Deterministic (within its seed), uniform (keys spread evenly over buckets), avalanche (flip 1 input bit → ∼half the output bits flip, so near-identical keys don’t cluster), fast (O(key length)).
  • Table hashes: FNV, Murmur, xxHash (fast, unkeyed → floodable); SipHash (Aumasson & Bernstein 2012) is a fast keyed PRF built to stop flooding. Swift’s Hasher uses SipHash-1-3, keyed by a random per-process seed (SWIFT_DETERMINISTIC_HASHING=1 to pin it in tests).
  • HashDoS (28C3, 2011): an attacker who knows an unkeyed hash posts thousands of keys that collide → every insert walks one chain → O(n2) CPU from one request. The secret seed makes the collision set unpredictable. Cost: hashValue and Set order change every launch.

Collisions — normal, then resolved

  • Birthday bound: with m possible values, a collision is likely after ∼√(m) items: P ≈1 - e-n2/2m. 23 people → 50 %; a 64-bit hash → ∼232 (4 billion) items; UUIDv4 (122 random bits) → ∼2.7×1018. SHA-256 collision work = 2128, not 2256.
  • Chaining (list per bucket; Java HashMap turns a chain of ≥8 into a red-black tree). Open addressing: linear probing (cache-friendly, primary clustering), quadratic, double hashing, Robin Hood (steal the slot from a key nearer its home — evens probe lengths), cuckoo (2 tables, O(1) worst-case lookup). Deletion needs tombstones or backward shift.
  • Rehash: at load α ≈ 0.75 (Swift: 3/4) double and re-insert all — O(n) once, O(1) amortised, a visible latency spike. Redis rehashes incrementally (two tables, move a few buckets per op).
  • Consistent hashing: hash(key) % N remaps ∼all keys when N changes; the ring moves ∼K/N. 100–200 vnodes per server smooth load and spread a dead node’s keys over all survivors. Alternatives: rendezvous (highest random weight), jump hash (no memory, numbered buckets only).

Example — Hashable right, and a stable hash

struct User: Hashable {
  let id: Int; var displayName: String         // not identity
  static func == (a: User, b: User) -> Bool { a.id == b.id }
  func hash(into h: inout Hasher) { h.combine(id) }  // SAME fields as ==
}
// hashValue changes per launch: never persist it. A stable key:
import CryptoKit
let name = SHA256.hash(data: Data(url.absoluteString.utf8))
  .map { String(format: "%02x", $0) }.joined()  // cache file name

Cryptographic hashes

propertyattacker cannot…SHA-256 cost
preimagefind x from H(x)2256
second preimagegiven x, find x’ ≠x, same H2256
collisionfind any pair x ≠y2128 (birthday)
MD5: collisions in seconds; SHA-1: public collision 2017 (SHAttered). Both dead for signatures/integrity; fine only as checksums. SHA-256 / SHA-512 are Merkle–Damgård → length extension: from H(k ‖m) anyone computes H(k ‖m ‖pad ‖m’). SHA-3 and BLAKE2/3 don’t have it.

MAC: HMAC vs a plain hash

A plain hash proves integrity only against accidents — anyone can recompute it. HMAC(K,m) = H((K ⊕opad) ‖H((K ⊕ipad) ‖m)): only a key holder can make or check the tag, and the nesting defeats length extension. Compare tags in constant time (CryptoKit isValidAuthenticationCode), never with ==.

Password hashing — slow on purpose

functionOWASP minimumnote
Argon2idm=19 MiB, t=2, p=1PHC winner; memory-hard; first choice
scryptN=217, r=8, p=1memory-hard
bcryptcost ≥ 10CPU only; input cut at 72 bytes
PBKDF2600 000 × HMAC-SHA256FIPS; GPU-friendly, weakest
SHA-256 does ∼1010 guesses/s on one GPU; Argon2id a handful per core — that ratio is the defence. Salt (unique, public) kills rainbow tables and same-password-same-hash; pepper (secret, outside the DB) makes a DB-only leak uncrackable. Hash on the server.

Bloom filter

m bits, k hash functions. No false negatives, tunable false positives: p ≈(1-e-kn/m)k; best k = (m/n)ln2; 1 % ≈ 9.6 bits per item, k = 7. No delete (counting Bloom filter can). Use: skip a disk/network lookup for keys that surely aren’t there (LSM-tree databases, CDN caches).

Interview traps

  • Persisting hashValue / asserting Set order — changes per launch.
  • hash(into:) using a field == ignores, or mutating a key after insertion → lost or duplicate entries.
  • SHA-256(password), even salted — too fast. And H(key ‖msg) is not a MAC.
  • “A hash is encryption” — no key, no inverse. “Collision = 2256” — it’s 2128.

Remember

Tables: fast + seeded. Integrity: SHA-256. Authenticity: HMAC. Passwords: Argon2id + salt. Membership at scale: Bloom.

Likely questions

  1. Why is Swift’s hash seeded? — HashDoS; so never persist it.
  2. Why vnodes? — even load; a dead node’s keys spread over all.
  3. Bloom says yes? — maybe; says no — definitely no.
  4. SHA-256 for a Dictionary? — too slow; seeded SipHash already stops flooding.
  5. Why HMAC, not H(k ‖m)? — length extension forges the latter.
  6. Store passwords? — Argon2id, per-user salt, PHC string; re-hash on login when params rise.