| name | bcrypt-5-0-0 |
| description | A skill for password hashing and key derivation using bcrypt 5.0 in Python. Use when implementing secure password storage, verifying passwords, or deriving cryptographic keys with bcrypt_pbkdf. |
bcrypt 5.0
Overview
bcrypt is a password-hashing library for Python that provides an acceptable level of security for storing passwords. It implements the bcrypt adaptive hashing algorithm and bcrypt_pbkdf key derivation function. Version 5.0 is implemented in Rust (via PyO3), supports Python 3.8+ including free-threaded Python 3.14, and runs on Linux, macOS, Windows (including ARM).
The library exposes four functions: hashpw, checkpw, gensalt, and kdf. All inputs and outputs are bytes objects — there is no string API.
When to Use
- Hashing passwords for user authentication systems
- Verifying a plaintext password against a stored bcrypt hash
- Deriving cryptographic keys using bcrypt_pbkdf (used by OpenSSH for key encryption)
- Migrating legacy password hashes to bcrypt format
- Any scenario requiring memory-hard, adaptive-cost password hashing
Consider argon2id or scrypt instead if you need the strongest available algorithm. The library authors themselves recommend those alternatives for new projects.
Core Concepts
The bcrypt Algorithm
bcrypt is based on the Blowfish cipher with an Eksblowfish setup. It incorporates a salt to prevent rainbow table attacks and uses a configurable work factor (cost) that doubles computation time with each increment. The algorithm is deliberately slow — that is its purpose.
Key properties:
- Salted: Each hash includes a unique 16-byte salt embedded in the output
- Adaptive cost: Work factor from 4 to 31, defaulting to 12 (2^12 = 4096 iterations)
- 72-byte password limit: Passwords longer than 72 bytes raise
ValueError in v5.0 (previously silently truncated)
- Constant-time comparison:
checkpw uses timing-safe comparison to prevent side-channel attacks
Hash Format
A bcrypt hash is a single bytes string encoding the algorithm version, cost, salt, and derived key:
$2b$12$LJ3m4ysyJWS5a36aPCZMteXV0h0CkFDbTQM8l0PpKPF0WJwHsO.7G
│ │ │ │
│ │ │ └─ 31-char base64-encoded hash
│ │ └─ 22-char base64-encoded salt (16 bytes)
│ └─ Cost factor (log2 of iterations, e.g. 12 = 4096)
└─ Algorithm identifier ($2b$, $2a$, or $2y$)
The hash is self-contained — it carries all information needed for verification. You store the full hash string in your database.
bcrypt_pbkdf (KDF)
bcrypt_pbkdf is a key derivation function used by OpenSSH for encrypting private keys. It uses the same Eksblowfish setup as bcrypt but operates in a KDF mode, producing arbitrary-length output keys from a password and salt with configurable rounds.
API Reference
bcrypt.hashpw(password, salt) -> bytes
Hash a password using bcrypt. Takes a password and salt as bytes, returns the full bcrypt hash as bytes.
import bcrypt
password = b"user_secret_password"
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password, salt)
Parameters:
password (bytes): The password to hash. Maximum 72 bytes. Raises ValueError if longer.
salt (bytes): A salt generated by gensalt() or a previously stored bcrypt hash (the salt is extracted from it).
Returns: bytes — the full bcrypt hash string.
Raises:
ValueError — password exceeds 72 bytes, or salt is invalid.
bcrypt.checkpw(password, hashed_password) -> bool
Verify a password against a stored bcrypt hash. Uses constant-time comparison.
import bcrypt
password = b"user_secret_password"
stored_hash = b'$2b$12$LJ3m4ysyJWS5a36aPCZMteXV0h0CkFDbTQM8l0PpKPF0WJwHsO.7G'
if bcrypt.checkpw(password, stored_hash):
print("Password is correct")
else:
print("Password is incorrect")
Parameters:
password (bytes): The plaintext password to verify.
hashed_password (bytes): A previously-computed bcrypt hash (output of hashpw).
Returns: bool — True if the password matches, False otherwise.
Raises:
ValueError — hashed_password is not a valid bcrypt hash.
bcrypt.gensalt(rounds=12, prefix=b"2b") -> bytes
Generate a random salt for use with hashpw. The salt is embedded in the returned bytes string.
import bcrypt
salt = bcrypt.gensalt()
salt_strong = bcrypt.gensalt(rounds=14)
salt_compat = bcrypt.gensalt(prefix=b"2a")
Parameters:
rounds (int, default 12): The logarithmic cost factor. Each increment doubles the computation time. Valid range: 4–31. Recommended minimum: 12 for modern hardware.
prefix (bytes, default b"2b"): The algorithm prefix. Options:
b"2b" — standard bcrypt (default, recommended)
b"2a" — older variant, use for compatibility with existing hashes
Returns: bytes — a bcrypt salt string like b'$2b$12$randomcharactershere...'
bcrypt.kdf(password, salt, desired_key_bytes, rounds, ignore_few_rounds=False) -> bytes
Derive a cryptographic key using bcrypt_pbkdf. This is the KDF used in OpenSSH's newer private key format.
import bcrypt
key = bcrypt.kdf(
password=b"user_password",
salt=b"random_salt_16_bytes",
desired_key_bytes=32,
rounds=100,
)
Parameters:
password (bytes): The input password or passphrase.
salt (bytes): A random salt value. Should be at least 16 bytes for security.
desired_key_bytes (int): Number of key bytes to produce (e.g., 32 for AES-256).
rounds (int): Number of bcrypt_pbkdf iterations. OpenSSH uses 16 by default. Use at least 100 for general-purpose KDF.
ignore_few_rounds (bool, default False): If True, suppresses the warning when rounds < 50.
Returns: bytes — the derived key of desired_key_bytes length.
Raises:
ValueError — rounds is too low (unless ignore_few_rounds=True).
Usage Examples
Basic Password Storage Flow
The standard pattern for user registration and login:
import bcrypt
def register_user(username: str, password: str) -> dict:
"""Register a new user with a bcrypt-hashed password."""
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
return {"username": username, "password_hash": hashed.decode("ascii")}
def authenticate_user(username: str, password: str, stored_hash: str) -> bool:
"""Verify user credentials against stored hash."""
return bcrypt.checkpw(
password.encode("utf-8"),
stored_hash.encode("ascii")
)
Handling Long Passwords
Since bcrypt has a 72-byte limit, hash long passwords with SHA-256 first:
import bcrypt
import hashlib
import base64
def hash_long_password(password: bytes) -> bytes:
"""Hash a password of any length using SHA-256 + bcrypt."""
digest = hashlib.sha256(password).digest()
encoded = base64.b64encode(digest)
return bcrypt.hashpw(encoded, bcrypt.gensalt())
Re-hashing on Login (Cost Adjustment)
Gradually increase the work factor as hardware improves:
import bcrypt
import re
def needs_rehash(stored_hash: bytes) -> bool:
"""Check if a stored hash uses a cost below our minimum."""
match = re.match(rb'\$2b\$(\d+)\$', stored_hash)
if not match:
return True
return int(match.group(1)) < 14
def authenticate_and_upgrade(password: bytes, stored_hash: bytes):
"""Authenticate and re-hash with higher cost if needed."""
if bcrypt.checkpw(password, stored_hash):
if needs_rehash(stored_hash):
new_hash = bcrypt.hashpw(password, bcrypt.gensalt(rounds=14))
return True, new_hash
return True, stored_hash
return False, stored_hash
Key Derivation for Encryption
Derive an encryption key from a passphrase:
import bcrypt
import os
def derive_encryption_key(passphrase: str, key_size: int = 32) -> tuple[bytes, bytes]:
"""Derive an encryption key from a passphrase.
Returns (key, salt) — store the salt alongside encrypted data.
"""
salt = os.urandom(16)
key = bcrypt.kdf(
password=passphrase.encode("utf-8"),
salt=salt,
desired_key_bytes=key_size,
rounds=100,
)
return key, salt
Security Considerations
Work Factor Selection
The default cost of 12 (4096 iterations) is a reasonable baseline. Adjust based on your server's capability — aim for hashing to take approximately 250-500ms per password. On fast servers, use 14 or higher.
Benchmarking:
import time
import bcrypt
start = time.perf_counter()
for _ in range(10):
bcrypt.hashpw(b"test", bcrypt.gensalt(rounds=14))
elapsed = time.perf_counter() - start
print(f"Average: {elapsed / 10 * 1000:.0f}ms per hash at cost 14")
Password Length Limit
The 72-byte limit is a property of the bcrypt algorithm itself, not just this library. In bcrypt 5.0, passing a password longer than 72 bytes raises ValueError instead of silently truncating. If your application accepts long passwords or passphrases, pre-hash with SHA-256 as shown above.
Salt Quality
Always use bcrypt.gensalt() to generate salts — it uses a cryptographically secure random number generator. Never reuse salts across passwords.
Prefix Selection
Use b"2b" (the default) for new hashes. The $2a$ prefix is maintained for compatibility with existing hashes but should not be used for new hashing operations.
Version Notes
bcrypt 5.0 Changes
- Implemented in Rust via PyO3 (since v4.0)
- Added support for Python 3.14 and free-threaded Python 3.14
- Added support for Windows on ARM
- Passwords longer than 72 bytes now raise
ValueError (previously silently truncated)
- Minimum supported Rust version: 1.74
Compatibility
- Python 3.8+ (including free-threaded builds) and PyPy 3
- Compatible with hashes from py-bcrypt and OpenBSD bcrypt
- Hash format is portable across all bcrypt implementations
- NUL bytes are allowed in inputs (since v4.0)
Alternatives
The library authors recommend considering these alternatives for new projects:
- argon2id (via
argon2-cffi) — winner of the Password Hashing Competition, supports memory hardness
- scrypt (via
hashlib.scrypt in the standard library) — NIST-recommended, memory-hard
bcrypt remains an acceptable and widely-deployed choice, but argon2id provides stronger resistance against GPU-based attacks.