<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Shahzeb Zafar's blog]]></title><description><![CDATA[Shahzeb Zafar's blog]]></description><link>https://shahzebzafar.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 14 Nov 2026 23:23:33 GMT</lastBuildDate><atom:link href="https://shahzebzafar.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Bcrypt Max Password Length: The 72-Byte Limit Explained]]></title><description><![CDATA[Bcrypt only uses the first 72 bytes of a password. Why, what each library does, and tested fixes for Python, FastAPI, Node and PHP.]]></description><link>https://shahzebzafar.hashnode.dev/bcrypt-max-password-length-the-72-byte-limit-explained</link><guid isPermaLink="true">https://shahzebzafar.hashnode.dev/bcrypt-max-password-length-the-72-byte-limit-explained</guid><category><![CDATA[Bcrypt ]]></category><category><![CDATA[Security]]></category><category><![CDATA[Python]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[PHP]]></category><dc:creator><![CDATA[Shahzaib Zafar]]></dc:creator><pubDate>Fri, 13 Nov 2026 16:58:14 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>If your Python app suddenly started throwing <code>ValueError: password cannot be longer than 72 bytes</code>, this is why. <em>Originally published on <a href="https://bcryptgenerator.org/blog/bcrypt-72-byte-limit">BcryptGenerator.org</a>.</em></p>
</blockquote>
<p><strong>Bcrypt's maximum password length is 72 bytes.</strong> Anything after the 72nd byte has no effect on the hash. That much has been true since 1999. What has changed is how libraries react to longer passwords: some still ignore the extra bytes silently, while newer ones refuse them with an error. That's why code that worked for years can suddenly throw <code>ValueError: password cannot be longer than 72 bytes</code>.</p>
<p>This guide explains where the limit comes from, why it's measured in bytes rather than characters, exactly what each popular library does (we tested them), and how to handle it safely.</p>
<h2>Why 72 bytes?</h2>
<p>Bcrypt is built on the Blowfish cipher, and it feeds the password into Blowfish's key setup. That key setup has room for 18 subkeys of 32 bits each: 576 bits, or 72 bytes. The <a href="https://www.usenix.org/legacy/events/usenix99/provos/provos.pdf">original bcrypt paper</a> by Provos and Mazières uses the password as that key, so the limit comes with the design.</p>
<p>It isn't a bug, and it doesn't make bcrypt insecure. A 72-byte password is extremely long. It becomes a problem when software assumes every byte of a long input counts, or when a library upgrade changes from silently ignoring extra bytes to rejecting them.</p>
<h2>Bytes, not characters</h2>
<p>The limit counts <strong>UTF-8 bytes</strong>. For plain English text, one character is one byte, but that isn't true for everything:</p>
<table>
<thead>
<tr>
<th>Text</th>
<th>Characters</th>
<th>UTF-8 bytes</th>
</tr>
</thead>
<tbody><tr>
<td><code>a</code></td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td><code>é</code></td>
<td>1</td>
<td>2</td>
</tr>
<tr>
<td><code>中</code></td>
<td>1</td>
<td>3</td>
</tr>
<tr>
<td><code>🔐</code></td>
<td>1</td>
<td>4</td>
</tr>
<tr>
<td>19 × <code>🔐</code></td>
<td>19</td>
<td><strong>76</strong>, already over the limit</td>
</tr>
</tbody></table>
<p>So a password manager phrase in Chinese, or a passphrase with a few emoji, can pass 72 bytes with far fewer than 72 characters. Measure bytes, not characters:</p>
<table>
<thead>
<tr>
<th>Language</th>
<th>Byte length of a password</th>
</tr>
</thead>
<tbody><tr>
<td>JavaScript / Node.js</td>
<td><code>Buffer.byteLength(password, 'utf8')</code></td>
</tr>
<tr>
<td>Python</td>
<td><code>len(password.encode('utf-8'))</code></td>
</tr>
<tr>
<td>PHP</td>
<td><code>strlen($password)</code> (not <code>mb_strlen</code>)</td>
</tr>
<tr>
<td>Go</td>
<td><code>len([]byte(password))</code></td>
</tr>
</tbody></table>
<p>JavaScript has an extra trap: <code>password.length</code> counts UTF-16 code units, which are neither bytes nor characters. For 19 emoji it returns 38, while the real byte count is 76.</p>
<h2>What each library does with longer passwords</h2>
<p>We tested the libraries we could run. For the others, the table cites their official documentation.</p>
<table>
<thead>
<tr>
<th>Library</th>
<th>Version</th>
<th>Over 72 bytes</th>
<th>Source</th>
</tr>
</thead>
<tbody><tr>
<td>Node.js <code>bcrypt</code></td>
<td>6.0.0</td>
<td>Silently ignored</td>
<td>Tested</td>
</tr>
<tr>
<td>Node.js <code>bcryptjs</code></td>
<td>3.0.3</td>
<td>Silently ignored; <code>bcrypt.truncates(pw)</code> tells you</td>
<td>Tested</td>
</tr>
<tr>
<td>PHP <code>password_hash()</code></td>
<td>8.0.30, 8.5.12</td>
<td>Silently ignored</td>
<td>Tested; <a href="https://www.php.net/manual/en/function.password-hash.php">PHP manual</a></td>
</tr>
<tr>
<td>Python <code>bcrypt</code></td>
<td>4.0.1</td>
<td>Silently ignored</td>
<td>Tested</td>
</tr>
<tr>
<td>Python <code>bcrypt</code></td>
<td><strong>5.0.0</strong></td>
<td><strong><code>ValueError</code></strong> in <code>hashpw()</code> and <code>checkpw()</code></td>
<td>Tested; <a href="https://github.com/pyca/bcrypt/blob/main/CHANGELOG.rst">changelog</a></td>
</tr>
<tr>
<td>passlib + <code>bcrypt</code> 5.0</td>
<td>passlib 1.7.4</td>
<td><strong><code>ValueError</code> even for short passwords</strong> (see below)</td>
<td>Tested</td>
</tr>
<tr>
<td>Flask-Bcrypt + <code>bcrypt</code> 5.0</td>
<td>1.0.1</td>
<td><code>ValueError</code></td>
<td>Tested</td>
</tr>
<tr>
<td>pwdlib (bcrypt hasher)</td>
<td>0.3.1</td>
<td><code>ValueError</code></td>
<td>Tested</td>
</tr>
<tr>
<td>Go <code>golang.org/x/crypto/bcrypt</code></td>
<td>current</td>
<td><code>GenerateFromPassword</code> returns <code>ErrPasswordTooLong</code>; <code>CompareHashAndPassword</code> still accepts</td>
<td><a href="https://pkg.go.dev/golang.org/x/crypto/bcrypt">Go docs</a></td>
</tr>
<tr>
<td>Spring Security <code>BCryptPasswordEncoder</code></td>
<td>patched versions</td>
<td><code>IllegalArgumentException</code></td>
<td><a href="https://spring.io/security/cve-2025-22228">CVE-2025-22228</a></td>
</tr>
</tbody></table>
<p>Two patterns stand out. The libraries that ignore the extra bytes give no warning, so you only find out with a test. And the move toward rejecting long passwords is recent: Python's <code>bcrypt</code> 5.0.0 shipped in September 2025, and Spring changed after a 2025 security advisory.</p>
<h2>"ValueError: password cannot be longer than 72 bytes" in Python and FastAPI</h2>
<p>This is the full message from Python's <code>bcrypt</code> 5.0:</p>
<pre><code>ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
</code></pre>
<p>If you call <code>bcrypt</code> directly, it means the password really is over 72 bytes. The fixes are in the next section.</p>
<p>If you use <strong>passlib</strong>, which many FastAPI and Flask tutorials taught for years, the cause is different. Passlib fails on <em>every</em> password, even short ones. In our test, this tutorial-style code raised the error for the 28-character password <code>correct horse battery staple</code>:</p>
<pre><code class="language-python">from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
pwd_context.hash("correct horse battery staple")  # ValueError with bcrypt 5.0
</code></pre>
<p>The reason is in passlib's own code. The first time it hashes anything, it runs a self-test that checks for an old bcrypt bug, using a 255-byte test password. <code>bcrypt</code> 5.0 rejects that test password, so passlib fails before your password is even looked at. You may also see <code>(trapped) error reading bcrypt version</code> with <code>AttributeError: module 'bcrypt' has no attribute '__about__'</code> in your logs. That's a separate, harmless compatibility warning. Passlib's last release, 1.7.4, came out in October 2020, before either change.</p>
<p>Your options:</p>
<ol>
<li><p><strong>Move off passlib (recommended).</strong> FastAPI's own <a href="https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/">security tutorial</a> now uses <strong>pwdlib</strong> with Argon2id (<code>pip install "pwdlib[argon2]"</code>), which also has no 72-byte limit:</p>
<pre><code class="language-python">from pwdlib import PasswordHash

password_hash = PasswordHash.recommended()
hashed = password_hash.hash(password)
password_hash.verify(password, hashed)
</code></pre>
<p>Existing bcrypt hashes still need a bcrypt verifier. FastAPI's tutorial suggests passlib for verifying old hashes while hashing new ones with Argon2, and the upgrade section below shows how to verify old bcrypt hashes with the <code>bcrypt</code> package.</p>
</li>
<li><p><strong>Short-term: pin <code>bcrypt==4.0.1</code>.</strong> In our test, passlib worked again with it. This only buys time: it brings back silent truncation, and passlib itself is no longer maintained.</p>
</li>
</ol>
<h2>When ignoring bytes becomes a security bug</h2>
<p>Silently ignoring bytes past 72 is harmless when the input is just a long password. It becomes dangerous when bcrypt is used to hash something <strong>longer and structured</strong>, where the important part might land after byte 72.</p>
<ul>
<li><strong>Spring Security, <a href="https://spring.io/security/cve-2025-22228">CVE-2025-22228</a>:</strong> <code>BCryptPasswordEncoder.matches()</code> returned <code>true</code> for passwords over 72 characters when only the first 72 matched. Patched versions now throw instead.</li>
<li><strong>Okta, 2024:</strong> Okta's <a href="https://trust.okta.com/security-advisories/okta-ad-ldap-delegated-authentication-username/">advisory</a> describes a cache key made by bcrypt-hashing user ID + username + password together. For usernames of 52 characters or more, a cached login could succeed without the correct password. The advisory doesn't spell out the mechanism, but the combination fits the limit: with a long enough user ID and username, the password starts past the bytes bcrypt reads. Okta switched to PBKDF2.</li>
</ul>
<p>The lesson: only hash a user's password with bcrypt, never a concatenation that puts the password at the end.</p>
<h2>How to handle the limit</h2>
<h3>Option 1: enforce a 72-byte maximum</h3>
<p>This is the simplest fix and what <a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">OWASP's Password Storage Cheat Sheet</a> recommends for bcrypt. Check the byte length at sign-up and password change, and show a clear message:</p>
<pre><code class="language-js">function assertBcryptLength(password) {
  if (Buffer.byteLength(password, 'utf8') &gt; 72) {
    throw new Error('Password must be 72 bytes or fewer')
  }
}
</code></pre>
<pre><code class="language-python">def check_length(password: str) -&gt; None:
    if len(password.encode("utf-8")) &gt; 72:
        raise ValueError("Password must be 72 bytes or fewer")
</code></pre>
<p>The PHP version is in our <a href="https://bcryptgenerator.org/blog/bcrypt-php-tutorial">PHP guide</a>. Use a message users understand: "fewer than 72 characters, or fewer if you use emoji or non-Latin letters" is clearer than "bytes".</p>
<h3>Option 2: pre-hash with a pepper</h3>
<p>If you want to accept passphrases of any length, hash the password with a secret key first, and give bcrypt that fixed-length result. Do it carefully: OWASP warns that naive pre-hashing is dangerous because raw hash output can contain <strong>null bytes</strong>, which some implementations treat as the end of the password, and because of "password shucking" (attacking an unsalted pre-hash leaked elsewhere). Its recommended construction is <code>bcrypt(base64(hmac-sha384(password, pepper)))</code>, with the pepper stored outside the database:</p>
<pre><code class="language-js">const crypto = require('crypto')
const bcrypt = require('bcrypt') // or 'bcryptjs'

// The pepper is a long random secret kept outside the database (env var or secrets manager)
function prepare(password) {
  return crypto.createHmac('sha384', process.env.PASSWORD_PEPPER).update(password, 'utf8').digest('base64')
}

const hash = await bcrypt.hash(prepare(password), 12)
const ok = await bcrypt.compare(prepare(password), hash)
</code></pre>
<pre><code class="language-python">import base64, hashlib, hmac, os
import bcrypt

PEPPER = os.environ["PASSWORD_PEPPER"].encode()

def prepare(password: str) -&gt; bytes:
    digest = hmac.new(PEPPER, password.encode("utf-8"), hashlib.sha384).digest()
    return base64.b64encode(digest)  # 64 bytes, no null bytes

hashed = bcrypt.hashpw(prepare(password), bcrypt.gensalt())
bcrypt.checkpw(prepare(password), hashed)
</code></pre>
<p>The base64 result is always 64 bytes, under the limit, and contains no null bytes. In our tests, a 145-byte passphrase verified correctly, and the same passphrase with one character removed did not. Two caveats: the pepper becomes as important as the database (lose it and nobody can log in), and you must apply exactly the same <code>prepare()</code> at sign-up and login.</p>
<h3>Option 3: use Argon2id for new systems</h3>
<p>Argon2id has no 72-byte limit and is <a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">OWASP's first choice</a> for new systems. In Python, pwdlib's <code>PasswordHash.recommended()</code> uses it. See <a href="https://bcryptgenerator.org/blog/bcrypt-vs-argon2">bcrypt vs Argon2</a> for the trade-offs.</p>
<h2>Upgrading without locking users out</h2>
<p>Here's the trap when you move from a library that ignored extra bytes to one that rejects them. Users who chose long passwords years ago have hashes made from the <strong>first 72 bytes</strong>, and the new library now raises an error when they log in.</p>
<p>We reproduced this. A hash created by Python <code>bcrypt</code> 4.0.1 from an 87-byte password made <code>bcrypt</code> 5.0's <code>checkpw()</code> raise <code>ValueError</code>. The fix is to do exactly what the old library did, but only when verifying existing hashes:</p>
<pre><code class="language-python">def verify_legacy(password: str, stored: bytes) -&gt; bool:
    # The old library ignored everything after byte 72, so do the same here
    return bcrypt.checkpw(password.encode("utf-8")[:72], stored)
</code></pre>
<p>That verified the user correctly and still rejected a wrong password. After a successful legacy login, re-hash the password with your new scheme (pre-hash or Argon2id) so the workaround is only ever needed once per user. If you also <a href="https://bcryptgenerator.org/blog/bcrypt-compare-returns-false">compare passwords in other places</a>, check them for the same upgrade issue.</p>
<h2>Checklist</h2>
<ul>
<li>Your library's behaviour over 72 bytes is known (see the table), not assumed</li>
<li>Sign-up and password change enforce 72 bytes, measured in <strong>bytes</strong></li>
<li>Or passwords are pre-hashed as <code>bcrypt(base64(hmac-sha384(password, pepper)))</code></li>
<li>bcrypt only ever hashes the password, never <code>id + username + password</code></li>
<li>Python apps on passlib have moved to pwdlib or <code>bcrypt</code> directly, or deliberately pinned <code>bcrypt==4.0.1</code></li>
<li>Hashes made by truncating libraries are verified with the first 72 bytes, then re-hashed</li>
<li>New systems consider Argon2id</li>
</ul>
<p>Want to see the limit in action? The <a href="https://bcryptgenerator.org/">bcrypt generator</a> warns you as soon as a password passes 72 bytes, and the <a href="https://bcryptgenerator.org/verify">bcrypt checker</a> shows the cost and version of any hash.</p>
<p><em>Drafted with AI assistance; reviewed and tested by Shahzeb Zafar.</em></p>
<hr />
<p><em>This article first appeared on <a href="https://bcryptgenerator.org/blog/bcrypt-72-byte-limit">BcryptGenerator.org</a>, where you can also <a href="https://bcryptgenerator.org/">generate</a> and <a href="https://bcryptgenerator.org/verify">check</a> bcrypt hashes in your browser.</em></p>
]]></content:encoded></item><item><title><![CDATA[Can You Decrypt a Bcrypt Hash? No, and Here Is What Works Instead]]></title><description><![CDATA[Bcrypt hashes can't be decrypted. See what "bcrypt decrypt" sites really do, and how to check, reset or migrate passwords instead.]]></description><link>https://shahzebzafar.hashnode.dev/can-you-decrypt-a-bcrypt-hash-no-and-here-is-what-works-instead</link><guid isPermaLink="true">https://shahzebzafar.hashnode.dev/can-you-decrypt-a-bcrypt-hash-no-and-here-is-what-works-instead</guid><category><![CDATA[Bcrypt ]]></category><category><![CDATA[Security]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[authentication]]></category><category><![CDATA[passwords]]></category><dc:creator><![CDATA[Shahzaib Zafar]]></dc:creator><pubDate>Fri, 13 Nov 2026 16:46:57 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>Every week someone asks how to "decrypt" a bcrypt hash. Here is the short answer, and what to do instead. <em>Originally published on <a href="https://bcryptgenerator.org/blog/can-bcrypt-be-decrypted">BcryptGenerator.org</a>.</em></p>
</blockquote>
<p><strong>No, a bcrypt hash can't be decrypted.</strong> Not online, not with the salt, and not with a key, because there is no key. Bcrypt is a one-way hash: it turns a password into a fixed string, and there's no operation that turns that string back. The only way to learn the password behind a bcrypt hash is to guess passwords and check each guess, and bcrypt is deliberately designed to make that slow.</p>
<p>That's usually not what you actually need, though. Below are the things people searching "bcrypt decrypt" are usually trying to do, and how to do each one.</p>
<h2>Is bcrypt encryption or hashing?</h2>
<p><strong>Hashing.</strong> The difference matters:</p>
<table>
<thead>
<tr>
<th></th>
<th>Encryption (e.g. AES)</th>
<th>Hashing (bcrypt)</th>
</tr>
</thead>
<tbody><tr>
<td>Reversible?</td>
<td>Yes, with the key</td>
<td>No</td>
</tr>
<tr>
<td>Uses a key?</td>
<td>Yes, and it must stay secret</td>
<td>No key at all</td>
</tr>
<tr>
<td>Output</td>
<td>Grows with the data</td>
<td>Always 60 characters</td>
</tr>
<tr>
<td>Used for</td>
<td>Data you need to read back</td>
<td>Passwords you only need to check</td>
</tr>
</tbody></table>
<p>Encryption is for things your program must read back later, like a stored API token. Passwords should never be stored that way; <a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">OWASP's Password Storage Cheat Sheet</a> recommends hashing them with a slow algorithm such as Argon2id or bcrypt. If the server can decrypt them, so can anyone who steals the server's key. With a hash, a stolen database contains nothing to decrypt. See <a href="https://bcryptgenerator.org/blog/what-is-bcrypt">what bcrypt is and how it works</a> for the full picture.</p>
<h2>What you can read from a bcrypt hash</h2>
<p>A bcrypt hash isn't completely opaque. Its first parts are stored in plain view, which is what "bcrypt decoder" tools show you:</p>
<pre><code class="language-js">const hash = '$2b$12$RB2qR6GI4quP.PUs6xR5He37dCyunLcYFzlvUfwSX41XkRXgOQ3OS'
const [, version, cost, rest] = hash.split('$')
console.log({ version, cost: Number(cost), salt: rest.slice(0, 22), checksum: rest.slice(22) })
// { version: '2b', cost: 12, salt: 'RB2qR6GI4quP.PUs6xR5He', checksum: '37dCyunLcYFzlvUfwSX41XkRXgOQ3OS' }
</code></pre>
<p>That tells you the bcrypt version, the cost (how slow each check is), and the random salt. It does <strong>not</strong> reveal anything about the password. The last 31 characters are the actual hash output. The <a href="https://bcryptgenerator.org/verify">bcrypt checker</a> shows the same breakdown for any hash you paste.</p>
<h2>"Can I decrypt it with the salt?"</h2>
<p>No. The salt isn't a secret or a key; it's stored right inside the hash, as shown above. Its job is to make every hash unique, so that two users with the same password get different hashes and attackers can't use precomputed tables. Having the salt is normal: every attacker who steals a bcrypt database has every salt, and it still doesn't let them reverse anything.</p>
<h2>What "bcrypt decrypt online" sites actually do</h2>
<p>Sites offering to "decrypt" bcrypt can only do one thing: <strong>guess</strong>. They take a list of common passwords, hash each one with the salt and cost from your hash, and see whether any result matches. It works only if the password was weak or common, like <code>password123</code> or <code>qwerty</code>.</p>
<p>Bcrypt makes even that slow on purpose. In the PHP project's <a href="https://wiki.php.net/rfc/bcrypt_cost_2023">measurements for its 2023 RFC</a>, a single cost-12 bcrypt hash took about 160–330 milliseconds on ordinary CPUs. Every guess costs that much again, so trying even a modest list of common passwords takes real time, and a long, random password is out of reach. That's the whole point of bcrypt.</p>
<p>Two warnings:</p>
<ul>
<li><strong>Don't paste real users' hashes into third-party sites.</strong> You're handing someone else material they can try to crack, and possibly the user's email next to it.</li>
<li><strong>"Decrypted" results only mean the password was guessable.</strong> If a site "decrypts" your hash, treat that password as compromised.</li>
</ul>
<h2>What you probably need instead</h2>
<h3>Check whether a password matches a hash</h3>
<p>You don't need to decrypt anything. Hash the candidate password with the same salt and compare, which is exactly what every bcrypt library's compare function does:</p>
<pre><code class="language-js">const bcrypt = require('bcryptjs')
console.log(await bcrypt.compare('correct horse battery staple', hash)) // true
console.log(await bcrypt.compare('password123', hash))                  // false
</code></pre>
<p>Or paste the hash and password into the <a href="https://bcryptgenerator.org/verify">bcrypt checker</a>, which does this in your browser. In PHP use <code>password_verify()</code>, and in Python <code>bcrypt.checkpw()</code>. If a check fails for a password you're sure is right, see <a href="https://bcryptgenerator.org/blog/bcrypt-compare-returns-false">why bcrypt.compare() returns false</a>.</p>
<h3>Reset a forgotten password (including your own admin account)</h3>
<p>If you've lost the password to an account you control, you can't recover it, and you don't need to. <strong>Set a new one:</strong> generate a fresh hash and write it over the old one. Generate the hash with any of these one-liners (each produced a working cost-12 hash in our tests):</p>
<pre><code class="language-bash">node -e "require('bcryptjs').hash('NewPassword!2026', 12).then(console.log)"
</code></pre>
<pre><code class="language-bash">php -r "echo password_hash('NewPassword!2026', PASSWORD_BCRYPT, ['cost' =&gt; 12]), PHP_EOL;"
</code></pre>
<pre><code class="language-bash">python -c "import bcrypt; print(bcrypt.hashpw(b'NewPassword!2026', bcrypt.gensalt(12)).decode())"
</code></pre>
<p>Or use the <a href="https://bcryptgenerator.org/">bcrypt generator</a> in your browser. Then save it to the user's row:</p>
<pre><code class="language-sql">UPDATE users SET password_hash = '$2b$12$...your new hash...' WHERE email = 'admin@example.com';
</code></pre>
<p>Use a real new password rather than the example, and clear it from your shell history afterwards. For other people's accounts, use your app's normal password-reset email flow instead of editing the database.</p>
<p><strong>Laravel:</strong> Laravel rejects hashes that start with <code>$2b$</code> (the Node and Python output) unless you change the first four characters to <code>$2y$</code>. The PHP one-liner already produces <code>$2y$</code>. Our <a href="https://bcryptgenerator.org/blog/bcrypt-php-tutorial">PHP and Laravel guide</a> explains why.</p>
<h3>Move users to a new system</h3>
<p>Migrating to a new app or framework doesn't require decrypting passwords either. Bcrypt hashes are portable. In our tests, hashes created by PHP, Node.js and Python verified in each other's libraries, so you copy the hashes as they are, and users keep their passwords. The one exception was Node's native <code>bcrypt</code> package, which rejected PHP's <code>$2y$</code> hashes until the prefix was changed to <code>$2b$</code> (<a href="https://bcryptgenerator.org/blog/bcrypt-compare-returns-false">details</a>). Just make sure the new database column is long enough: a bcrypt hash is 60 characters, and <a href="https://bcryptgenerator.org/blog/bcrypt-hash-length-database">too-short columns break logins</a>.</p>
<h2>Quick answers</h2>
<p><strong>Is bcrypt reversible?</strong> No. There's no reverse function; you can only check guesses.</p>
<p><strong>Can bcrypt be decrypted with a key?</strong> No. Bcrypt doesn't use a key. If someone gives you a "key" to decrypt bcrypt, they're mistaken or scamming you.</p>
<p><strong>Can a bcrypt hash be decoded?</strong> Partly. The version, cost and salt are readable (see above), but the password isn't.</p>
<p><strong>Is bcrypt still secure if attackers get the database?</strong> It holds up well for strong passwords, which is why it's used. Weak passwords can still be guessed, just slowly. See <a href="https://bcryptgenerator.org/blog/how-many-bcrypt-rounds">how many bcrypt rounds to use</a> for choosing a cost that keeps guessing expensive.</p>
<p><strong>What about files ending in <code>.bfe</code>?</strong> That's a different, older tool that's also called <code>bcrypt</code>: a command-line file encrypter built on Blowfish. Those files <em>can</em> be decrypted, with the passphrase used to create them. It has nothing to do with password hashes.</p>
<p><em>Drafted with AI assistance; reviewed and tested by Shahzeb Zafar.</em></p>
<hr />
<p><em>This article first appeared on <a href="https://bcryptgenerator.org/blog/can-bcrypt-be-decrypted">BcryptGenerator.org</a>, where you can also <a href="https://bcryptgenerator.org/">generate</a> and <a href="https://bcryptgenerator.org/verify">check</a> bcrypt hashes in your browser.</em></p>
]]></content:encoded></item></channel></rss>