Introduction

Every website you log into stores something in a database — but if they are doing their job, it is not your password. It is a hash: the output of a one-way mathematical function. When you log in, the site hashes what you typed and compares it to the stored value. Your real password never sits on disk.

The naive approach is to use a fast cryptographic hash like MD5 or SHA-256. Fast sounds good, right? In 2009 the website RockYou was breached and 32 million passwords were stolen — stored in plain text. Sites that used fast hashes fared little better. Within days, attackers with commodity GPUs had cracked most of them by hashing billions of guesses per second until they matched.

The fundamental insight of modern password hashing is: make the hash function deliberately, tuneably slow. Not slow for the user logging in — one hash taking 100 ms is imperceptible. But slow enough that checking a billion guesses takes not hours but centuries.

Three algorithms define the field. bcrypt (Niels Provos and David Mazières, 1999) introduced a work factor that exponentially scales cost. scrypt (Colin Percival, 2009) added memory-hardness so that cheap custom hardware loses its advantage. Argon2 (Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich, 2015) — winner of the Password Hashing Competition — lets you tune time, memory, and parallelism independently. All three are deliberately polynomial-in-the-work-factor slow, and that is not a bug. It is the entire security guarantee.

The Work Factor in Action

The slider below controls a bcrypt-style work factor (called cost in bcrypt). Each increment doubles the number of internal rounds. Observe how a modest change in the slider explodes the time an attacker would need to check every possible short password.

<div class="demo">
  <div class="controls">
    <label for="cost">Work factor (bcrypt cost): <strong id="cost-val">10</strong></label>
    <input type="range" id="cost" min="4" max="20" value="10" step="1">
  </div>
  <div class="controls">
    <label for="gpu">Attacker speed: <strong id="gpu-val">10,000</strong> hashes/sec</label>
    <input type="range" id="gpu" min="1" max="6" value="4" step="1">
  </div>
  <div class="result-box" id="result">
    <div class="row"><span class="label">Hash rounds per attempt</span><span class="value" id="rounds"></span></div>
    <div class="row"><span class="label">Attacker hashes/second</span><span class="value" id="speed"></span></div>
    <div class="row"><span class="label">8-char password space (a-z)</span><span class="value">208,827,064,576</span></div>
    <div class="row highlight"><span class="label">Time to exhaust space</span><span class="value" id="time"></span></div>
    <div class="bar-wrap"><div id="bar" class="bar"></div></div>
  </div>
  <div class="note" id="note"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #f7f8fa; }
.demo { max-width: 520px; margin: 0 auto; padding: 1rem; }
.controls { margin-bottom: 1rem; }
.controls label { display: block; font-size: .9rem; margin-bottom: .3rem; color: #555; }
.controls input[type=range] { width: 100%; accent-color: #1d3557; }
.result-box { background: #fff; border: 1px solid #dde3ea; border-radius: 10px; padding: 1rem; margin-top: .5rem; }
.row { display: flex; justify-content: space-between; align-items: baseline; padding: .3rem 0; border-bottom: 1px solid #f0f0f0; font-size: .88rem; }
.row:last-of-type { border-bottom: none; }
.label { color: #555; }
.value { font-family: ui-monospace, monospace; font-weight: 700; color: #1d3557; }
.row.highlight .label { color: #1d3557; font-weight: 700; }
.row.highlight .value { color: #0a7d33; font-size: 1rem; }
.bar-wrap { background: #e8eef3; border-radius: 6px; height: 12px; margin-top: .8rem; overflow: hidden; }
.bar { height: 100%; background: linear-gradient(90deg, #0a7d33, #e63946); border-radius: 6px; transition: width .4s; }
.note { margin-top: .7rem; font-size: .83rem; color: #444; line-height: 1.45; min-height: 2.5em; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Password-hashing demo: bcrypt-style work factor exponentially scales cost.
// bcrypt cost C means 2^C internal rounds per hash attempt.
// We compute how long an attacker needs to exhaust an 8-char a-z password space.

const SPACE = Math.pow(26, 8); // 26^8 ≈ 208.8 billion — all lowercase 8-char passwords

const costSlider = document.getElementById('cost');
const gpuSlider  = document.getElementById('gpu');
const costVal    = document.getElementById('cost-val');
const gpuVal     = document.getElementById('gpu-val');
const roundsEl   = document.getElementById('rounds');
const speedEl    = document.getElementById('speed');
const timeEl     = document.getElementById('time');
const barEl      = document.getElementById('bar');
const noteEl     = document.getElementById('note');

// GPU tier labels and hash rates (bcrypt hashes per second for that tier)
// At cost 10, a high-end GPU does ~10k bcrypt/s; lower cost = more, higher cost = fewer.
// We model base rate at cost 10, then scale by 2^(10-C) for the actual cost.
const GPU_TIERS = [
  { label: '100',       base: 100    },   // old CPU, cost 10
  { label: '1,000',     base: 1000   },   // modern CPU
  { label: '10,000',    base: 10000  },   // entry GPU
  { label: '100,000',   base: 100000 },   // high-end GPU cluster
  { label: '1,000,000', base: 1e6    },   // dedicated cracking rig
  { label: '10,000,000',base: 1e7    },   // nation-state ASICs (hypothetical — memory-hard would stop this)
];

function fmt(n) {
  if (n < 60)       return i18nFmt('%n% seconds', { n: n.toFixed(1) });
  if (n < 3600)     return i18nFmt('%v% minutes', { v: (n / 60).toFixed(1) });
  if (n < 86400)    return i18nFmt('%v% hours', { v: (n / 3600).toFixed(1) });
  if (n < 31536000) return i18nFmt('%v% days', { v: (n / 86400).toFixed(1) });
  const yrs = n / 31536000;
  if (yrs < 1e6)    return i18nFmt('%yrs% years', { yrs: yrs.toFixed(0) });
  if (yrs < 1e9)    return i18nFmt('%v% million years', { v: (yrs / 1e6).toFixed(2) });
  if (yrs < 1e12)   return i18nFmt('%v% billion years', { v: (yrs / 1e9).toFixed(2) });
  return i18nFmt('%v% trillion years', { v: (yrs / 1e12).toFixed(2) });
}

function fmtNum(n) {
  return n.toLocaleString('en-US');
}

function update() {
  const C    = parseInt(costSlider.value, 10);
  const tier = parseInt(gpuSlider.value, 10) - 1;
  const gpu  = GPU_TIERS[tier];

  costVal.textContent = C;
  gpuVal.textContent  = gpu.label;

  // Rounds = 2^C (the core bcrypt iteration count)
  const rounds = Math.pow(2, C);

  // Effective hashes/sec: base is at cost 10; scale by 2^(10-C)
  const effectiveRate = gpu.base * Math.pow(2, 10 - C);

  // Time to exhaust the space
  const seconds = SPACE / effectiveRate;

  roundsEl.textContent = fmtNum(rounds);
  speedEl.textContent  = fmtNum(Math.round(effectiveRate)) + '/s';
  timeEl.textContent   = fmt(seconds);

  // Bar: map log10(seconds) from 0..log10(1e18) → 0..100%
  const logS   = Math.log10(Math.max(seconds, 0.001));
  const logMax = Math.log10(1e18);
  const pct    = Math.min(100, Math.max(1, (logS / logMax) * 100));
  barEl.style.width = pct + '%';

  // Contextual note
  const AGE_UNIVERSE_S = 4.3e17;
  let note = '';
  if (seconds < 3600) {
    note = '⚠️ Cracked in under an hour — this work factor is dangerously low for a real application.';
  } else if (seconds < 86400 * 7) {
    note = '⚠️ Cracked in under a week — suitable only for low-stakes throwaway passwords.';
  } else if (seconds < 31536000 * 100) {
    note = '✓ Cracking takes years — acceptable for many applications, but consider raising the cost as hardware improves.';
  } else if (seconds < AGE_UNIVERSE_S) {
    note = '✓✓ Cracking would take longer than human civilization — comfortable for most real-world deployments.';
  } else {
    note = '✓✓✓ Cracking would take longer than the age of the universe — practically unbreakable for any foreseeable hardware.';
  }
  noteEl.textContent = note;

  // Console log for node verification
  console.log('cost=' + C + ' rounds=' + rounds + ' rate=' + Math.round(effectiveRate) + '/s time=' + fmt(seconds));
}

costSlider.addEventListener('input', update);
gpuSlider.addEventListener('input', update);
update();
Result
Console

Notice the asymmetry: your login performs exactly one hash — the cost is imperceptible. An attacker checking millions of candidates must pay that cost every single time. Raising the work factor by 4 steps multiplies the attacker's time by 16 while adding only ~100 ms to your login. That is the power of tunable hardness.

The Real Complexity

Password hashing is a solved engineering problem — we know exactly what to do — but the solution rests on subtle computational ideas.

  • One-wayness. A cryptographic hash is easy to compute forward but believed to be hard to invert. This is a conjecture, not a theorem. If P vs NP were resolved in the wrong direction, one-way functions might not exist, and the whole edifice would collapse. In practice, no one has broken SHA-3 or the bcrypt core.
  • Work factors make brute force polynomial-to-exponential. bcrypt's cost parameter C makes each hash attempt take O(2C)O(2^{C}) rounds. An attacker who can check 10910^{9} MD5 hashes per second can check only ~10310^{3} bcrypt hashes at cost 20. The same hardware, a factor of a million harder.
  • Memory-hardness closes the hardware gap. A GPU has thousands of tiny cores but limited memory per core. scrypt and Argon2 require gigabytes of memory during hashing, so a GPU with 10 000 cores still gets at most a handful of parallel hashes — nearly the same as a CPU. Custom ASICs (the bane of Bitcoin) become economically useless.
  • Parallelism control. Argon2 lets you specify a lane count, forcing the hash to occupy p parallel threads. Even machines with exotic architectures cannot do better than serial at the individual-hash level.
  • Status: solved but evolving. bcrypt, scrypt, and Argon2 are the current standards. SHA-256 and MD5 for passwords are categorically wrong — not because those hashes are broken for their intended use, but because they are too fast. The attack surface is brute force, not cryptanalysis.

For a deeper look at why search problems resist shortcuts, see the article on factoring — the hardness of breaking RSA relies on analogous asymmetry between easy and hard directions.

Where It Matters

The "deliberately slow hash" principle shows up everywhere credentials or keys need protecting:

  • User authentication. Every modern web framework (Django, Rails, Laravel, Spring Security) defaults to bcrypt or Argon2 for password_hash. Stored password databases become useless to attackers even after a breach.
  • Disk and file encryption. When you type a passphrase to unlock a laptop or a VeraCrypt volume, a key-derivation function (KDF) — often PBKDF2, scrypt, or Argon2 — stretches that passphrase into a 256-bit encryption key. The slow hash is what makes "just guess every passphrase" infeasible.
  • Cryptocurrency wallets. HD wallets derive private keys from a seed phrase using PBKDF2-HMAC-SHA512 with 2048 rounds (BIP-39). The slowness prevents offline attacks on stolen seed backups.
  • Password managers. Apps like Bitwarden and 1Password use Argon2 to derive the master key from your master password. Even if the vault file leaks, cracking it is intended to take longer than the universe has existed for strong passwords.
  • Hardware security tokens. Devices like YubiKey derive challenge-response keys using slow KDFs to limit offline PIN cracking even if the firmware is extracted.

The common thread: anywhere a human-memorable secret must protect machine-scale data, a slow hash stands in the gap. See also how discrete logarithms underpin the key-exchange protocols that deliver those passwords over the network in the first place.

Conclusion

The insight behind password hashing is deceptively simple: one slow hash does not hurt you; a billion slow hashes crush an attacker. By making the work factor tunable, bcrypt, scrypt, and Argon2 let defenders keep pace with faster hardware simply by incrementing a number.

MD5 and SHA-256 are magnificent tools — for integrity checks, digital signatures, and message authentication. For storing passwords, they are catastrophically wrong. The RockYou breach, the LinkedIn breach, the Adobe breach — nearly every major credential leak in history has one thing in common: passwords protected only by a fast hash, or nothing at all.

The good news is that the correct solution is known, standardized, and freely available. Raise the work factor whenever hardware gets faster. Prefer memory-hard algorithms. Never store passwords in plain text or with a fast hash. These three rules, applied consistently, make a stolen database nearly worthless — and that is the closest thing to a solved problem that applied cryptography has to offer.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/password-hashing/Content licensed under CC BY-NC 4.0.