[{"data":1,"prerenderedAt":1619},["ShallowReactive",2],{"blog-what-is-bcrypt-why-md5-is-dead":3},{"id":4,"title":5,"alt":6,"author":7,"body":8,"category":1593,"description":1594,"extension":1595,"faq":1596,"image":1607,"meta":1608,"navigation":651,"path":1609,"publishedAt":1610,"seo":1611,"stem":1612,"tags":1613,"__hash__":1618},"blog\u002Fen\u002Fwhat-is-bcrypt-why-md5-is-dead.md","What is Bcrypt? Why MD5 is Dead for Passwords","bcrypt vs md5 password hashing comparison — secure algorithm vs broken algorithm","Alex Vibe, Senior Security Dev",{"type":9,"value":10,"toc":1575},"minimark",[11,15,23,30,35,38,41,47,53,59,62,66,69,75,90,93,103,114,119,126,132,138,145,149,152,241,244,248,251,254,269,272,275,286,289,295,299,302,305,366,369,372,376,379,383,386,401,407,410,482,489,503,510,513,517,528,538,578,582,594,617,1172,1175,1205,1214,1307,1310,1314,1320,1326,1332,1336,1339,1346,1514,1524,1527,1531,1536,1539,1544,1547,1552,1555,1560,1563,1568,1571],[12,13,14],"p",{},"MD5 is not a password hash. It never was. It's a checksum algorithm — designed in 1992 to verify file integrity, not protect credentials. Yet millions of password databases still use it, and they pay for that mistake when they get breached.",[12,16,17,18,22],{},"Here's the blunt reality: an RTX 4090 can attempt ",[19,20,21],"strong",{},"164 billion MD5 hashes per second",". Your users' \"secure\" 8-character passwords — the ones with the capital letter and the exclamation mark — fall in under 200 milliseconds. Bcrypt was engineered to make that attack economically impossible. This guide explains exactly why, with numbers.",[12,24,25],{},[26,27],"img",{"alt":28,"src":29},"bcrypt vs md5 hash algorithm comparison diagram","\u002Fimages\u002Fblog\u002Fwhat-is-bcrypt-why-md5-is-dead\u002Falgorithm-comparison.webp",[31,32,34],"h2",{"id":33},"why-md5-is-broken-for-passwords","Why MD5 is Broken for Passwords",[12,36,37],{},"MD5 produces a 128-bit hash in under a microsecond. That speed is a feature for file integrity verification. For passwords, it's a catastrophic flaw.",[12,39,40],{},"The attack surface has three layers:",[12,42,43,46],{},[19,44,45],{},"1. Raw cracking speed."," Modern GPUs treat MD5 hashing as embarrassingly parallel work. hashcat on an RTX 4090 cluster doesn't \"crack\" passwords — it just pre-computes them faster than you can rotate them.",[12,48,49,52],{},[19,50,51],{},"2. Rainbow tables."," Since MD5 is deterministic and unsalted by default, the same password always produces the same hash. Attackers pre-compute billions of hash→password mappings. When they steal your database, lookup is O(1).",[12,54,55,58],{},[19,56,57],{},"3. Collision attacks."," MD5 is cryptographically broken — two different inputs can produce the same hash. This matters for digital signatures, not password cracking, but it's another reason the algorithm has zero legitimacy in a security context.",[12,60,61],{},"The fix isn't \"use SHA-256 instead.\" SHA-256 is also fast — ~23 billion hashes\u002Fsec on an RTX 4090. Fast is the problem.",[31,63,65],{"id":64},"what-bcrypt-actually-does","What Bcrypt Actually Does",[12,67,68],{},"Bcrypt was published in 1999 by Niels Provos and David Mazières. It's based on the Blowfish block cipher and was specifically designed for password storage with two key properties that MD5 lacks entirely.",[12,70,71,74],{},[19,72,73],{},"Built-in salt."," Bcrypt generates a cryptographically random 128-bit salt per password, automatically. The salt is stored alongside the hash. This makes rainbow table attacks impossible — each hash is unique even for identical passwords.",[12,76,77,80,81,85,86,89],{},[19,78,79],{},"Adaptive cost factor."," The work factor (cost) is a tunable parameter. At ",[82,83,84],"code",{},"cost=12",", bcrypt performs 2¹² = 4,096 internal iterations of the Blowfish key setup. Bump it to ",[82,87,88],{},"cost=13"," and you double the computation time. As hardware gets faster, you increase the cost. MD5 gives you no such lever.",[12,91,92],{},"A bcrypt hash looks like this:",[94,95,100],"pre",{"className":96,"code":98,"language":99},[97],"language-text","$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8\u002FLevH.6\u002FJwejc5\u002FHre\n","text",[82,101,98],{"__ignoreMap":102},"",[12,104,105,106,109,110,113],{},"Breaking it down: ",[82,107,108],{},"$2b$"," = algorithm version, ",[82,111,112],{},"$12$"," = cost factor, next 22 chars = salt, remaining 31 chars = hash. Everything you need to verify the password lives in that string.",[115,116,118],"h3",{"id":117},"the-hashing-pipeline-what-actually-happens","The Hashing Pipeline: What Actually Happens",[12,120,121,122,125],{},"Understanding the data flow kills a lot of implementation bugs. Here's what bcrypt does under the hood when you call ",[82,123,124],{},"bcrypt.hash(password, 12)",":",[94,127,130],{"className":128,"code":129,"language":99},[97],"password (plaintext)\n        │\n        ▼\n  [Generate 128-bit salt]  ← crypto.getRandomValues() or OS \u002Fdev\u002Furandom\n        │\n        ▼\n  [Concatenate: salt + password]\n        │\n        ▼\n  [EksBlowfishSetup: 2^cost key expansion iterations]\n        │                    ← this is the intentionally slow part\n        ▼\n  [64-bit Blowfish encrypt  \"OrpheanBeholderScryDoubt\"  × 64]\n        │\n        ▼\n  [Encode: version + cost + salt + hash → 60-char string]\n        │\n        ▼\n  \"$2b$12$\u003C22-char salt>\u003C31-char hash>\"  ← store this in DB\n",[82,131,129],{"__ignoreMap":102},[12,133,134,137],{},[19,135,136],{},"Verification is the reverse:"," extract the salt and cost from the stored string, run the same pipeline on the login attempt, compare outputs. The plaintext never needs to be stored anywhere. If the strings match, the password is correct.",[12,139,140,141,144],{},"Two things to notice: the salt goes ",[19,142,143],{},"in"," before the KDF, not appended after. And the output is self-contained — you never need a separate lookup to verify.",[31,146,148],{"id":147},"the-numbers-gpu-benchmarks-in-2026","The Numbers: GPU Benchmarks in 2026",[12,150,151],{},"This is where the gap becomes visceral. All figures are hashcat benchmarks on a single NVIDIA RTX 4090:",[153,154,155,171],"table",{},[156,157,158],"thead",{},[159,160,161,165,168],"tr",{},[162,163,164],"th",{},"Algorithm",[162,166,167],{},"Hashes\u002Fsec (RTX 4090)",[162,169,170],{},"Time to crack 8-char password",[172,173,174,186,197,208,219,230],"tbody",{},[159,175,176,180,183],{},[177,178,179],"td",{},"MD5",[177,181,182],{},"~164 billion\u002Fsec",[177,184,185],{},"\u003C 0.1 seconds",[159,187,188,191,194],{},[177,189,190],{},"SHA-1",[177,192,193],{},"~61 billion\u002Fsec",[177,195,196],{},"\u003C 0.3 seconds",[159,198,199,202,205],{},[177,200,201],{},"SHA-256",[177,203,204],{},"~23 billion\u002Fsec",[177,206,207],{},"~1 second",[159,209,210,213,216],{},[177,211,212],{},"bcrypt cost=10",[177,214,215],{},"~184,000\u002Fsec",[177,217,218],{},"~150 days",[159,220,221,224,227],{},[177,222,223],{},"bcrypt cost=12",[177,225,226],{},"~46,000\u002Fsec",[177,228,229],{},"~1,600 days",[159,231,232,235,238],{},[177,233,234],{},"Argon2id",[177,236,237],{},"~15,000\u002Fsec",[177,239,240],{},"~13 years",[12,242,243],{},"That's not a small gap. It's four to six orders of magnitude. The difference between MD5 and bcrypt is the difference between a screen door and a vault.",[31,245,247],{"id":246},"the-entropy-angle","The Entropy Angle",[12,249,250],{},"Password entropy tells you how much brute-force work is needed regardless of the hashing algorithm. The formula:",[12,252,253],{},"$$H = L \\times \\log_2(R)$$",[12,255,256,257,260,261,264,265,268],{},"Where ",[19,258,259],{},"H"," = entropy in bits, ",[19,262,263],{},"L"," = password length, ",[19,266,267],{},"R"," = character pool size (charset).",[12,270,271],{},"For an 8-character password using full printable ASCII (95 characters):",[12,273,274],{},"$$H = 8 \\times \\log_2(95) \\approx 8 \\times 6.57 \\approx 52.6 \\text{ bits}$$",[12,276,277,278,281,282,285],{},"52.6 bits sounds like a lot. Against MD5 at 164 billion attempts\u002Fsec, that's 2^52.6 \u002F 164×10⁹ ≈ ",[19,279,280],{},"~25 seconds"," to exhaust the full space. Against bcrypt at cost=12 running 46,000 attempts\u002Fsec: 2^52.6 \u002F 46,000 ≈ ",[19,283,284],{},"~1,600 years",".",[12,287,288],{},"Same password. Same entropy. Wildly different practical security. The algorithm is doing 99% of the work here.",[12,290,291],{},[26,292],{"alt":293,"src":294},"password entropy formula and crack time visual","\u002Fimages\u002Fblog\u002Fwhat-is-bcrypt-why-md5-is-dead\u002Fentropy-crack-time.webp",[31,296,298],{"id":297},"choosing-a-cost-factor","Choosing a Cost Factor",[12,300,301],{},"Bcrypt's cost factor is an exponent. Cost=12 means 4,096 iterations. Cost=13 means 8,192. Each increment doubles the work.",[12,303,304],{},"OWASP's current guidance for interactive login:",[153,306,307,320],{},[156,308,309],{},[159,310,311,314,317],{},[162,312,313],{},"Environment",[162,315,316],{},"Minimum Cost",[162,318,319],{},"Target Login Time",[172,321,322,333,344,355],{},[159,323,324,327,330],{},[177,325,326],{},"Low-traffic web app",[177,328,329],{},"10",[177,331,332],{},"\u003C 100ms",[159,334,335,338,341],{},[177,336,337],{},"Standard production",[177,339,340],{},"12",[177,342,343],{},"100–200ms",[159,345,346,349,352],{},[177,347,348],{},"High-security (banking, admin)",[177,350,351],{},"13–14",[177,353,354],{},"200–400ms",[159,356,357,360,363],{},[177,358,359],{},"API endpoints (no UX constraint)",[177,361,362],{},"14+",[177,364,365],{},"up to 1 sec",[12,367,368],{},"Benchmark on your actual hardware. The goal is to make it genuinely slow without ruining UX. If you're at cost=10 and login takes 50ms, you have two free increments before you hit 200ms.",[12,370,371],{},"Rehash on login. When a user authenticates, check their stored cost factor. If it's below your current minimum, rehash with the new factor and update the record. This upgrades your hash database transparently, without forcing a password reset.",[31,373,375],{"id":374},"bcrypt-vs-argon2id-do-i-need-to-migrate","Bcrypt vs Argon2id: Do I Need to Migrate?",[12,377,378],{},"Argon2id won the Password Hashing Competition in 2015. It's memory-hard — configurable RAM requirements make it dramatically more expensive for GPU and ASIC attacks because memory is harder to parallelize than raw compute.",[115,380,382],{"id":381},"why-memory-hard-destroys-gpu-attacks","Why \"Memory-Hard\" Destroys GPU Attacks",[12,384,385],{},"GPUs win at password cracking because they have thousands of small cores running in parallel. An RTX 4090 has 16,384 CUDA cores. Against MD5, all 16,384 cores can work independently — no coordination, no shared state, pure throughput.",[12,387,388,389,392,393,396,397,400],{},"Memory-hard algorithms break that model. Argon2id requires each hash operation to allocate and actively use a configurable amount of RAM (the ",[82,390,391],{},"m"," parameter). At ",[82,394,395],{},"m=19456"," (19 MB), a single hash needs 19 MB of memory ",[19,398,399],{},"throughout"," its execution — not just at the start.",[12,402,403],{},[26,404],{"alt":405,"src":406},"CPU vs GPU memory architecture for Argon2id memory-hard hashing","\u002Fimages\u002Fblog\u002Fwhat-is-bcrypt-why-md5-is-dead\u002Fcpu-gpu-memory.webp",[12,408,409],{},"The math kills the GPU: an RTX 4090 has 24 GB of VRAM. At 19 MB per hash attempt, you can run at most ~1,260 parallel Argon2id operations simultaneously. Compare that to bcrypt, where the memory footprint is trivial and GPU parallelism is only limited by compute.",[153,411,412,426],{},[156,413,414],{},[159,415,416,419,421,424],{},[162,417,418],{},"Resource",[162,420,179],{},[162,422,423],{},"bcrypt",[162,425,234],{},[172,427,428,442,456,470],{},[159,429,430,433,436,439],{},[177,431,432],{},"Memory per attempt",[177,434,435],{},"\u003C 1 KB",[177,437,438],{},"~4 KB",[177,440,441],{},"19 MB (configurable)",[159,443,444,447,450,453],{},[177,445,446],{},"RTX 4090 parallel ops",[177,448,449],{},"~16,000",[177,451,452],{},"~1,000",[177,454,455],{},"~1,260",[159,457,458,461,464,467],{},[177,459,460],{},"Cache pressure",[177,462,463],{},"None",[177,465,466],{},"Low",[177,468,469],{},"Extreme",[159,471,472,475,477,479],{},[177,473,474],{},"ASIC resistance",[177,476,463],{},[177,478,466],{},[177,480,481],{},"High",[12,483,484,485,488],{},"The key insight: ",[19,486,487],{},"memory bandwidth is the same bottleneck for both CPUs and GPUs",". A modern CPU with a large L3 cache actually handles Argon2id's access patterns more efficiently than GPU VRAM. That's by design — the algorithm was built to level the playing field between a defender's server CPU and an attacker's GPU farm.",[12,490,491,492,494,495,498,499,502],{},"For new systems: use Argon2id. The OWASP recommendation is ",[82,493,395],{}," (19 MB), ",[82,496,497],{},"t=2"," iterations, ",[82,500,501],{},"p=1"," parallelism.",[12,504,505,506,509],{},"For existing systems using bcrypt at cost ≥ 12: ",[19,507,508],{},"don't panic-migrate",". Bcrypt at high cost is still practically uncrackable with today's hardware. The attack delta between bcrypt-12 and Argon2id is real but measured in threat models, not emergency patches. Migrate on your next major auth refactor, not tonight at 2am.",[12,511,512],{},"What you should migrate immediately: anything still using MD5, SHA-1, or unsalted SHA-256. Those are actively exploitable.",[31,514,516],{"id":515},"verifying-hashes-client-side","Verifying Hashes Client-Side",[12,518,519,520,527],{},"To test SHA-256 and HMAC hashes without sending data to a server, use our ",[19,521,522],{},[523,524,526],"a",{"href":525},"\u002Fhash-generator","Hash Generator"," — runs 100% in your browser, zero data sent to any server — supporting MD5, SHA-1, SHA-256, and SHA-512 with HMAC mode and file input. Useful for verifying checksums, testing HMAC implementations, and understanding the raw output difference between algorithm families.",[12,529,530,531,537],{},"For checking the practical strength of the passwords you're about to store, the ",[19,532,533],{},[523,534,536],{"href":535},"\u002Fpassword-strength-checker","Password Strength Checker"," gives you entropy bits and crack-time estimates against MD5 and bcrypt — instantly, client-side.",[539,540,541,546,549],"blockquote",{},[12,542,543],{},[19,544,545],{},"🛡️ Security Checkpoint — Complete This Step",[12,547,548],{},"If you're shipping a login system, your algorithm choice is permanent until you force a password reset. Make the right call now.",[550,551,552,559,564],"ul",{},[553,554,555,556,558],"li",{},"→ ",[523,557,526],{"href":525}," — verify SHA-256 \u002F HMAC output without sending data to a server",[553,560,555,561,563],{},[523,562,536],{"href":535}," — see crack-time estimates against MD5 and bcrypt for real inputs",[553,565,555,566,570,571,574,575],{},[523,567,569],{"href":568},"\u002F","Password Generator"," — generate cryptographically secure passwords using ",[82,572,573],{},"crypto.getRandomValues()",", not ",[82,576,577],{},"Math.random()",[31,579,581],{"id":580},"the-right-way-argon2id-in-nodejs","The Right Way: Argon2id in Node.js",[12,583,584,585,593],{},"Theory is worthless without working code. Here's the minimal correct implementation using the ",[523,586,590],{"href":587,"rel":588},"https:\u002F\u002Fgithub.com\u002Franisalt\u002Fnode-argon2",[589],"nofollow",[82,591,592],{},"argon2"," package — the most widely-used Node.js binding for libargon2:",[94,595,599],{"className":596,"code":597,"language":598,"meta":102,"style":102},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","npm install argon2\n","bash",[82,600,601],{"__ignoreMap":102},[602,603,606,610,614],"span",{"class":604,"line":605},"line",1,[602,607,609],{"class":608},"sBMFI","npm",[602,611,613],{"class":612},"sfazB"," install",[602,615,616],{"class":612}," argon2\n",[94,618,622],{"className":619,"code":620,"language":621,"meta":102,"style":102},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import argon2 from 'argon2'\n\n\u002F\u002F Hash a password (on registration \u002F password change)\nasync function hashPassword(plaintext) {\n  return argon2.hash(plaintext, {\n    type: argon2.argon2id,      \u002F\u002F Argon2id = hybrid of Argon2i + Argon2d\n    memoryCost: 19456,          \u002F\u002F 19 MB — OWASP minimum 2026\n    timeCost: 2,                \u002F\u002F 2 passes over memory\n    parallelism: 1,             \u002F\u002F 1 thread (scale up on multi-core servers)\n  })\n  \u002F\u002F Returns: \"$argon2id$v=19$m=19456,t=2,p=1$\u003Csalt>$\u003Chash>\"\n  \u002F\u002F Store the entire string — salt is embedded\n}\n\n\u002F\u002F Verify a password (on login)\nasync function verifyPassword(storedHash, plaintext) {\n  return argon2.verify(storedHash, plaintext)\n  \u002F\u002F Returns: true | false\n  \u002F\u002F argon2.verify extracts salt + params from storedHash automatically\n}\n\n\u002F\u002F Example login handler\nasync function login(email, passwordAttempt) {\n  const user = await db.users.findByEmail(email)\n  if (!user) return false                           \u002F\u002F don't reveal \"no such user\"\n\n  const valid = await verifyPassword(user.passwordHash, passwordAttempt)\n  if (!valid) return false\n\n  \u002F\u002F Rehash on login if params are outdated (transparent upgrade)\n  if (argon2.needsRehash(user.passwordHash, { memoryCost: 19456, timeCost: 2 })) {\n    const newHash = await hashPassword(passwordAttempt)\n    await db.users.updateHash(user.id, newHash)\n  }\n\n  return true\n}\n","js",[82,623,624,646,653,660,687,711,731,748,764,780,789,795,801,807,812,818,842,864,870,876,881,886,892,916,950,978,983,1012,1031,1036,1042,1094,1116,1148,1154,1159,1167],{"__ignoreMap":102},[602,625,626,630,634,637,641,643],{"class":604,"line":605},[602,627,629],{"class":628},"s7zQu","import",[602,631,633],{"class":632},"sTEyZ"," argon2 ",[602,635,636],{"class":628},"from",[602,638,640],{"class":639},"sMK4o"," '",[602,642,592],{"class":612},[602,644,645],{"class":639},"'\n",[602,647,649],{"class":604,"line":648},2,[602,650,652],{"emptyLinePlaceholder":651},true,"\n",[602,654,656],{"class":604,"line":655},3,[602,657,659],{"class":658},"sHwdD","\u002F\u002F Hash a password (on registration \u002F password change)\n",[602,661,663,667,670,674,677,681,684],{"class":604,"line":662},4,[602,664,666],{"class":665},"spNyl","async",[602,668,669],{"class":665}," function",[602,671,673],{"class":672},"s2Zo4"," hashPassword",[602,675,676],{"class":639},"(",[602,678,680],{"class":679},"sHdIc","plaintext",[602,682,683],{"class":639},")",[602,685,686],{"class":639}," {\n",[602,688,690,693,696,698,701,704,706,709],{"class":604,"line":689},5,[602,691,692],{"class":628},"  return",[602,694,695],{"class":632}," argon2",[602,697,285],{"class":639},[602,699,700],{"class":672},"hash",[602,702,676],{"class":703},"swJcz",[602,705,680],{"class":632},[602,707,708],{"class":639},",",[602,710,686],{"class":639},[602,712,714,717,719,721,723,726,728],{"class":604,"line":713},6,[602,715,716],{"class":703},"    type",[602,718,125],{"class":639},[602,720,695],{"class":632},[602,722,285],{"class":639},[602,724,725],{"class":632},"argon2id",[602,727,708],{"class":639},[602,729,730],{"class":658},"      \u002F\u002F Argon2id = hybrid of Argon2i + Argon2d\n",[602,732,734,737,739,743,745],{"class":604,"line":733},7,[602,735,736],{"class":703},"    memoryCost",[602,738,125],{"class":639},[602,740,742],{"class":741},"sbssI"," 19456",[602,744,708],{"class":639},[602,746,747],{"class":658},"          \u002F\u002F 19 MB — OWASP minimum 2026\n",[602,749,751,754,756,759,761],{"class":604,"line":750},8,[602,752,753],{"class":703},"    timeCost",[602,755,125],{"class":639},[602,757,758],{"class":741}," 2",[602,760,708],{"class":639},[602,762,763],{"class":658},"                \u002F\u002F 2 passes over memory\n",[602,765,767,770,772,775,777],{"class":604,"line":766},9,[602,768,769],{"class":703},"    parallelism",[602,771,125],{"class":639},[602,773,774],{"class":741}," 1",[602,776,708],{"class":639},[602,778,779],{"class":658},"             \u002F\u002F 1 thread (scale up on multi-core servers)\n",[602,781,783,786],{"class":604,"line":782},10,[602,784,785],{"class":639},"  }",[602,787,788],{"class":703},")\n",[602,790,792],{"class":604,"line":791},11,[602,793,794],{"class":658},"  \u002F\u002F Returns: \"$argon2id$v=19$m=19456,t=2,p=1$\u003Csalt>$\u003Chash>\"\n",[602,796,798],{"class":604,"line":797},12,[602,799,800],{"class":658},"  \u002F\u002F Store the entire string — salt is embedded\n",[602,802,804],{"class":604,"line":803},13,[602,805,806],{"class":639},"}\n",[602,808,810],{"class":604,"line":809},14,[602,811,652],{"emptyLinePlaceholder":651},[602,813,815],{"class":604,"line":814},15,[602,816,817],{"class":658},"\u002F\u002F Verify a password (on login)\n",[602,819,821,823,825,828,830,833,835,838,840],{"class":604,"line":820},16,[602,822,666],{"class":665},[602,824,669],{"class":665},[602,826,827],{"class":672}," verifyPassword",[602,829,676],{"class":639},[602,831,832],{"class":679},"storedHash",[602,834,708],{"class":639},[602,836,837],{"class":679}," plaintext",[602,839,683],{"class":639},[602,841,686],{"class":639},[602,843,845,847,849,851,854,856,858,860,862],{"class":604,"line":844},17,[602,846,692],{"class":628},[602,848,695],{"class":632},[602,850,285],{"class":639},[602,852,853],{"class":672},"verify",[602,855,676],{"class":703},[602,857,832],{"class":632},[602,859,708],{"class":639},[602,861,837],{"class":632},[602,863,788],{"class":703},[602,865,867],{"class":604,"line":866},18,[602,868,869],{"class":658},"  \u002F\u002F Returns: true | false\n",[602,871,873],{"class":604,"line":872},19,[602,874,875],{"class":658},"  \u002F\u002F argon2.verify extracts salt + params from storedHash automatically\n",[602,877,879],{"class":604,"line":878},20,[602,880,806],{"class":639},[602,882,884],{"class":604,"line":883},21,[602,885,652],{"emptyLinePlaceholder":651},[602,887,889],{"class":604,"line":888},22,[602,890,891],{"class":658},"\u002F\u002F Example login handler\n",[602,893,895,897,899,902,904,907,909,912,914],{"class":604,"line":894},23,[602,896,666],{"class":665},[602,898,669],{"class":665},[602,900,901],{"class":672}," login",[602,903,676],{"class":639},[602,905,906],{"class":679},"email",[602,908,708],{"class":639},[602,910,911],{"class":679}," passwordAttempt",[602,913,683],{"class":639},[602,915,686],{"class":639},[602,917,919,922,925,928,931,934,936,939,941,944,946,948],{"class":604,"line":918},24,[602,920,921],{"class":665},"  const",[602,923,924],{"class":632}," user",[602,926,927],{"class":639}," =",[602,929,930],{"class":628}," await",[602,932,933],{"class":632}," db",[602,935,285],{"class":639},[602,937,938],{"class":632},"users",[602,940,285],{"class":639},[602,942,943],{"class":672},"findByEmail",[602,945,676],{"class":703},[602,947,906],{"class":632},[602,949,788],{"class":703},[602,951,953,956,959,962,965,968,971,975],{"class":604,"line":952},25,[602,954,955],{"class":628},"  if",[602,957,958],{"class":703}," (",[602,960,961],{"class":639},"!",[602,963,964],{"class":632},"user",[602,966,967],{"class":703},") ",[602,969,970],{"class":628},"return",[602,972,974],{"class":973},"sfNiH"," false",[602,976,977],{"class":658},"                           \u002F\u002F don't reveal \"no such user\"\n",[602,979,981],{"class":604,"line":980},26,[602,982,652],{"emptyLinePlaceholder":651},[602,984,986,988,991,993,995,997,999,1001,1003,1006,1008,1010],{"class":604,"line":985},27,[602,987,921],{"class":665},[602,989,990],{"class":632}," valid",[602,992,927],{"class":639},[602,994,930],{"class":628},[602,996,827],{"class":672},[602,998,676],{"class":703},[602,1000,964],{"class":632},[602,1002,285],{"class":639},[602,1004,1005],{"class":632},"passwordHash",[602,1007,708],{"class":639},[602,1009,911],{"class":632},[602,1011,788],{"class":703},[602,1013,1015,1017,1019,1021,1024,1026,1028],{"class":604,"line":1014},28,[602,1016,955],{"class":628},[602,1018,958],{"class":703},[602,1020,961],{"class":639},[602,1022,1023],{"class":632},"valid",[602,1025,967],{"class":703},[602,1027,970],{"class":628},[602,1029,1030],{"class":973}," false\n",[602,1032,1034],{"class":604,"line":1033},29,[602,1035,652],{"emptyLinePlaceholder":651},[602,1037,1039],{"class":604,"line":1038},30,[602,1040,1041],{"class":658},"  \u002F\u002F Rehash on login if params are outdated (transparent upgrade)\n",[602,1043,1045,1047,1049,1051,1053,1056,1058,1060,1062,1064,1066,1069,1072,1074,1076,1078,1081,1083,1085,1088,1091],{"class":604,"line":1044},31,[602,1046,955],{"class":628},[602,1048,958],{"class":703},[602,1050,592],{"class":632},[602,1052,285],{"class":639},[602,1054,1055],{"class":672},"needsRehash",[602,1057,676],{"class":703},[602,1059,964],{"class":632},[602,1061,285],{"class":639},[602,1063,1005],{"class":632},[602,1065,708],{"class":639},[602,1067,1068],{"class":639}," {",[602,1070,1071],{"class":703}," memoryCost",[602,1073,125],{"class":639},[602,1075,742],{"class":741},[602,1077,708],{"class":639},[602,1079,1080],{"class":703}," timeCost",[602,1082,125],{"class":639},[602,1084,758],{"class":741},[602,1086,1087],{"class":639}," }",[602,1089,1090],{"class":703},")) ",[602,1092,1093],{"class":639},"{\n",[602,1095,1097,1100,1103,1105,1107,1109,1111,1114],{"class":604,"line":1096},32,[602,1098,1099],{"class":665},"    const",[602,1101,1102],{"class":632}," newHash",[602,1104,927],{"class":639},[602,1106,930],{"class":628},[602,1108,673],{"class":672},[602,1110,676],{"class":703},[602,1112,1113],{"class":632},"passwordAttempt",[602,1115,788],{"class":703},[602,1117,1119,1122,1124,1126,1128,1130,1133,1135,1137,1139,1142,1144,1146],{"class":604,"line":1118},33,[602,1120,1121],{"class":628},"    await",[602,1123,933],{"class":632},[602,1125,285],{"class":639},[602,1127,938],{"class":632},[602,1129,285],{"class":639},[602,1131,1132],{"class":672},"updateHash",[602,1134,676],{"class":703},[602,1136,964],{"class":632},[602,1138,285],{"class":639},[602,1140,1141],{"class":632},"id",[602,1143,708],{"class":639},[602,1145,1102],{"class":632},[602,1147,788],{"class":703},[602,1149,1151],{"class":604,"line":1150},34,[602,1152,1153],{"class":639},"  }\n",[602,1155,1157],{"class":604,"line":1156},35,[602,1158,652],{"emptyLinePlaceholder":651},[602,1160,1162,1164],{"class":604,"line":1161},36,[602,1163,692],{"class":628},[602,1165,1166],{"class":973}," true\n",[602,1168,1170],{"class":604,"line":1169},37,[602,1171,806],{"class":639},[12,1173,1174],{},"Three things worth calling out explicitly:",[1176,1177,1178,1187,1195],"ol",{},[553,1179,1180,1186],{},[19,1181,1182,1185],{},[82,1183,1184],{},"argon2.verify()"," is timing-safe"," — it uses a constant-time comparison internally. Don't roll your own string comparison.",[553,1188,1189,1194],{},[19,1190,1191],{},[82,1192,1193],{},"needsRehash()"," checks whether the stored hash was created with weaker parameters. This is how you upgrade your entire user base silently, on next login, without a forced password reset.",[553,1196,1197,1204],{},[19,1198,1199,1200,1203],{},"The full ",[82,1201,1202],{},"$argon2id$..."," string goes into one DB column."," No separate salt column, no separate params column. The format is self-describing.",[12,1206,1207,1208,125],{},"For bcrypt on a legacy system, the equivalent with ",[523,1209,1212],{"href":1210,"rel":1211},"https:\u002F\u002Fgithub.com\u002Fkelektiv\u002Fnode.bcrypt.js",[589],[82,1213,423],{},[94,1215,1217],{"className":619,"code":1216,"language":621,"meta":102,"style":102},"import bcrypt from 'bcrypt'\n\nconst COST = 12\n\nconst hash = await bcrypt.hash(plaintext, COST)\nconst valid = await bcrypt.compare(plaintext, storedHash)\n",[82,1218,1219,1234,1238,1252,1256,1282],{"__ignoreMap":102},[602,1220,1221,1223,1226,1228,1230,1232],{"class":604,"line":605},[602,1222,629],{"class":628},[602,1224,1225],{"class":632}," bcrypt ",[602,1227,636],{"class":628},[602,1229,640],{"class":639},[602,1231,423],{"class":612},[602,1233,645],{"class":639},[602,1235,1236],{"class":604,"line":648},[602,1237,652],{"emptyLinePlaceholder":651},[602,1239,1240,1243,1246,1249],{"class":604,"line":655},[602,1241,1242],{"class":665},"const",[602,1244,1245],{"class":632}," COST ",[602,1247,1248],{"class":639},"=",[602,1250,1251],{"class":741}," 12\n",[602,1253,1254],{"class":604,"line":662},[602,1255,652],{"emptyLinePlaceholder":651},[602,1257,1258,1260,1263,1265,1267,1270,1272,1274,1277,1279],{"class":604,"line":689},[602,1259,1242],{"class":665},[602,1261,1262],{"class":632}," hash ",[602,1264,1248],{"class":639},[602,1266,930],{"class":628},[602,1268,1269],{"class":632}," bcrypt",[602,1271,285],{"class":639},[602,1273,700],{"class":672},[602,1275,1276],{"class":632},"(plaintext",[602,1278,708],{"class":639},[602,1280,1281],{"class":632}," COST)\n",[602,1283,1284,1286,1289,1291,1293,1295,1297,1300,1302,1304],{"class":604,"line":713},[602,1285,1242],{"class":665},[602,1287,1288],{"class":632}," valid ",[602,1290,1248],{"class":639},[602,1292,930],{"class":628},[602,1294,1269],{"class":632},[602,1296,285],{"class":639},[602,1298,1299],{"class":672},"compare",[602,1301,1276],{"class":632},[602,1303,708],{"class":639},[602,1305,1306],{"class":632}," storedHash)\n",[12,1308,1309],{},"That's it. If your implementation is longer than this, you're probably doing something wrong — or reimplementing something the library already handles.",[31,1311,1313],{"id":1312},"common-implementation-mistakes","Common Implementation Mistakes",[12,1315,1316,1319],{},[19,1317,1318],{},"Hashing before bcrypt."," Some developers SHA-256 the password first, then bcrypt the result. Don't. Bcrypt truncates input at 72 bytes — pre-hashing to hex (64 bytes) is fine, but it adds complexity with no security benefit for normal passwords. It matters only for passwords longer than 72 characters, which almost no user creates.",[12,1321,1322,1325],{},[19,1323,1324],{},"Storing the salt separately."," You don't need to. The salt is embedded in the bcrypt output string. If you're pulling it out and storing it in a separate column, you misread the docs.",[12,1327,1328,1331],{},[19,1329,1330],{},"Logging the pre-hash password."," This happens more than anyone admits. Your debug logger catches the login request, logs the payload, and now you have plaintext passwords in your log files. Scrub password fields at the framework layer before they reach your logger.",[115,1333,1335],{"id":1334},"the-pepper-pattern-salts-underrated-sibling","The Pepper Pattern: Salt's Underrated Sibling",[12,1337,1338],{},"Salt protects against rainbow tables. But here's the threat model salt doesn't cover: an attacker who dumps your entire database can see every salt. They know the algorithm. They can run offline attacks against every hash simultaneously.",[12,1340,1341,1342,1345],{},"A ",[19,1343,1344],{},"pepper"," (sometimes called a server-side secret) closes that gap. It's a secret value stored outside the database — in an environment variable, a secrets manager, or HSM — that gets mixed with the password before it reaches the KDF. If the database leaks but the server isn't compromised, the attacker has hashes they can't crack because they don't have the pepper.",[94,1347,1349],{"className":619,"code":1348,"language":621,"meta":102,"style":102},"\u002F\u002F Pepper pattern — append before hashing, never store the pepper itself\nconst PEPPER = process.env.PASSWORD_PEPPER   \u002F\u002F 32-byte random hex, from secrets manager\n\nasync function hashWithPepper(plaintext) {\n  return argon2.hash(plaintext + PEPPER, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2 })\n}\n\nasync function verifyWithPepper(storedHash, plaintext) {\n  return argon2.verify(storedHash, plaintext + PEPPER)\n}\n",[82,1350,1351,1356,1381,1385,1402,1457,1461,1465,1486,1510],{"__ignoreMap":102},[602,1352,1353],{"class":604,"line":605},[602,1354,1355],{"class":658},"\u002F\u002F Pepper pattern — append before hashing, never store the pepper itself\n",[602,1357,1358,1360,1363,1365,1368,1370,1373,1375,1378],{"class":604,"line":648},[602,1359,1242],{"class":665},[602,1361,1362],{"class":632}," PEPPER ",[602,1364,1248],{"class":639},[602,1366,1367],{"class":632}," process",[602,1369,285],{"class":639},[602,1371,1372],{"class":632},"env",[602,1374,285],{"class":639},[602,1376,1377],{"class":632},"PASSWORD_PEPPER   ",[602,1379,1380],{"class":658},"\u002F\u002F 32-byte random hex, from secrets manager\n",[602,1382,1383],{"class":604,"line":655},[602,1384,652],{"emptyLinePlaceholder":651},[602,1386,1387,1389,1391,1394,1396,1398,1400],{"class":604,"line":662},[602,1388,666],{"class":665},[602,1390,669],{"class":665},[602,1392,1393],{"class":672}," hashWithPepper",[602,1395,676],{"class":639},[602,1397,680],{"class":679},[602,1399,683],{"class":639},[602,1401,686],{"class":639},[602,1403,1404,1406,1408,1410,1412,1414,1416,1419,1422,1424,1426,1429,1431,1433,1435,1437,1439,1441,1443,1445,1447,1449,1451,1453,1455],{"class":604,"line":689},[602,1405,692],{"class":628},[602,1407,695],{"class":632},[602,1409,285],{"class":639},[602,1411,700],{"class":672},[602,1413,676],{"class":703},[602,1415,680],{"class":632},[602,1417,1418],{"class":639}," +",[602,1420,1421],{"class":632}," PEPPER",[602,1423,708],{"class":639},[602,1425,1068],{"class":639},[602,1427,1428],{"class":703}," type",[602,1430,125],{"class":639},[602,1432,695],{"class":632},[602,1434,285],{"class":639},[602,1436,725],{"class":632},[602,1438,708],{"class":639},[602,1440,1071],{"class":703},[602,1442,125],{"class":639},[602,1444,742],{"class":741},[602,1446,708],{"class":639},[602,1448,1080],{"class":703},[602,1450,125],{"class":639},[602,1452,758],{"class":741},[602,1454,1087],{"class":639},[602,1456,788],{"class":703},[602,1458,1459],{"class":604,"line":713},[602,1460,806],{"class":639},[602,1462,1463],{"class":604,"line":733},[602,1464,652],{"emptyLinePlaceholder":651},[602,1466,1467,1469,1471,1474,1476,1478,1480,1482,1484],{"class":604,"line":750},[602,1468,666],{"class":665},[602,1470,669],{"class":665},[602,1472,1473],{"class":672}," verifyWithPepper",[602,1475,676],{"class":639},[602,1477,832],{"class":679},[602,1479,708],{"class":639},[602,1481,837],{"class":679},[602,1483,683],{"class":639},[602,1485,686],{"class":639},[602,1487,1488,1490,1492,1494,1496,1498,1500,1502,1504,1506,1508],{"class":604,"line":766},[602,1489,692],{"class":628},[602,1491,695],{"class":632},[602,1493,285],{"class":639},[602,1495,853],{"class":672},[602,1497,676],{"class":703},[602,1499,832],{"class":632},[602,1501,708],{"class":639},[602,1503,837],{"class":632},[602,1505,1418],{"class":639},[602,1507,1421],{"class":632},[602,1509,788],{"class":703},[602,1511,1512],{"class":604,"line":782},[602,1513,806],{"class":639},[12,1515,1516,1519,1520,1523],{},[19,1517,1518],{},"Pepper rotation is the hard part."," When you rotate the pepper (and you should, on a schedule or after any server compromise), you can't verify existing hashes — you need the old pepper to verify and re-hash. Standard approach: store a ",[82,1521,1522],{},"pepper_version"," column alongside the hash, keep old peppers in your secrets manager under versioned keys, migrate on next login.",[12,1525,1526],{},"The threat model is narrow but real. If you store highly sensitive credentials (banking, healthcare, admin accounts) and you expect targeted database exfiltration, the pepper is worth the operational complexity. For general-purpose web apps with bcrypt cost ≥ 12, the practical risk delta is small.",[31,1528,1530],{"id":1529},"frequently-asked-questions","Frequently Asked Questions",[12,1532,1533],{},[19,1534,1535],{},"Is MD5 safe for password storage in 2026?",[12,1537,1538],{},"No. An RTX 4090 runs MD5 at ~164 billion hashes per second. An 8-character all-ASCII password has a search space of about 2^52.6 — that's exhausted in under 30 seconds at full throughput. MD5 was designed for file integrity checking in 1992. It was never a password hashing algorithm.",[12,1540,1541],{},[19,1542,1543],{},"What is bcrypt and how does it work?",[12,1545,1546],{},"Bcrypt is an adaptive password hashing function built on the Blowfish cipher. Each call generates a fresh 128-bit cryptographic salt, runs 2^cost iterations of the Blowfish key setup, and outputs a 60-character string containing the version, cost factor, salt, and hash — everything needed to verify the password later. The cost factor is the key innovation: you increase it as hardware improves, keeping brute-force attacks slow indefinitely.",[12,1548,1549],{},[19,1550,1551],{},"What cost factor should I use for bcrypt in 2026?",[12,1553,1554],{},"OWASP recommends cost=12 as the minimum for production systems. Benchmark on your hardware: aim for 100–200ms login time. If you're under 100ms, increment the cost. If you're over 400ms, consider whether your authentication infrastructure needs scaling before reducing cost. Re-benchmark every 18 months — GPU performance improvements are relentless.",[12,1556,1557],{},[19,1558,1559],{},"Is bcrypt better than Argon2?",[12,1561,1562],{},"Argon2id has better theoretical security because it's memory-hard, which degrades GPU parallelism. But bcrypt at cost ≥ 12 is still practically uncrackable with current hardware. For new projects, start with Argon2id. For existing bcrypt implementations: upgrade on your next auth refactor, don't do emergency migrations.",[12,1564,1565],{},[19,1566,1567],{},"Can I use SHA-256 for password hashing?",[12,1569,1570],{},"No. SHA-256 is a general-purpose cryptographic hash function optimized for speed — ~23 billion hashes\u002Fsec on an RTX 4090. It has no cost factor, no built-in salt, and no adaptive properties. Use it for data integrity, HMAC authentication, and digital signatures. For passwords, use bcrypt (cost ≥ 12) or Argon2id.",[1572,1573,1574],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":102,"searchDepth":648,"depth":648,"links":1576},[1577,1578,1581,1582,1583,1584,1587,1588,1589,1592],{"id":33,"depth":648,"text":34},{"id":64,"depth":648,"text":65,"children":1579},[1580],{"id":117,"depth":655,"text":118},{"id":147,"depth":648,"text":148},{"id":246,"depth":648,"text":247},{"id":297,"depth":648,"text":298},{"id":374,"depth":648,"text":375,"children":1585},[1586],{"id":381,"depth":655,"text":382},{"id":515,"depth":648,"text":516},{"id":580,"depth":648,"text":581},{"id":1312,"depth":648,"text":1313,"children":1590},[1591],{"id":1334,"depth":655,"text":1335},{"id":1529,"depth":648,"text":1530},"Security","MD5 crumbles in seconds on modern GPUs. Bcrypt was built to resist that. Here's the math, the benchmarks, and why your choice of hash algorithm is a life-or-death decision for your users.","md",[1597,1599,1601,1603,1605],{"question":1535,"answer":1598},"No. An RTX 4090 can attempt ~164 billion MD5 hashes per second, which means an 8-character password hash cracks in under a second. MD5 was designed for checksums, not password storage — never use it for credentials.",{"question":1543,"answer":1600},"Bcrypt is a password-hashing function based on the Blowfish cipher. It incorporates a salt (preventing rainbow table attacks) and a configurable cost factor that makes hashing intentionally slow, scaling with hardware improvements.",{"question":1551,"answer":1602},"Cost factor 12 is the current minimum recommended by OWASP for interactive logins. If your server handles login in under 200ms at cost 12, bump to 13. Re-benchmark every 18 months as hardware improves.",{"question":1559,"answer":1604},"Argon2id is technically superior — it's memory-hard and was designed for modern GPU attacks. But bcrypt remains battle-tested and widely supported. For new systems, use Argon2id. For legacy systems already using bcrypt, upgrading isn't urgent unless you're under active threat.",{"question":1567,"answer":1606},"No. SHA-256 is a general-purpose cryptographic hash — fast by design. An RTX 4090 does ~23 billion SHA-256 hashes per second. Use it for data integrity and HMAC, not passwords. Passwords need a slow, adaptive KDF like bcrypt or Argon2id.","\u002Fimages\u002Fblog\u002Fwhat-is-bcrypt-why-md5-is-dead\u002Fhero.webp",{},"\u002Fen\u002Fwhat-is-bcrypt-why-md5-is-dead","2026-05-13",{"title":5,"description":1594},"en\u002Fwhat-is-bcrypt-why-md5-is-dead",[423,1614,1615,1616,1617],"md5","password hashing","hash generator","cryptography","9R5LQTZuVfJ32xH-465LEC0MsBDYuBy615PmaEgiHUM",1779173265912]