Introduction
Protecting user passwords is a critically important task in modern software development. In an era of constantly growing cyber‑threats, using simple or outdated hash functions such as MD5 or SHA‑1 creates serious security risks. These algorithms are vulnerable to rainbow‑table attacks and have high computation speed, making them exploitable by modern cracking methods.
Bcrypt is an adaptive cryptographic hash function specifically designed for secure password storage. Created in 1999 by Niels Provos and David Mazieres, this algorithm is based on the Blowfish cipher and includes a built‑in salt system, making it extremely resistant to various attack types.
In the Python ecosystem, bcrypt is available via the eponymous library, which is a Python wrapper around the original OpenBSD implementation. This library is used by millions of developers worldwide and is considered the gold standard for password hashing in Python applications.
What Is the bcrypt Library
The bcrypt library for Python is a reliable, battle‑tested tool for cryptographic password hashing. It provides a Python wrapper over the original C implementation of bcrypt, delivering high performance and robustness.
Key Features of bcrypt
Adaptivity: The main strength of bcrypt lies in its adaptive nature. The “cost factor” parameter allows you to increase computational difficulty as hardware gets more powerful, ensuring long‑term protection.
Built‑in Salt: Each hash automatically incorporates a unique salt, eliminating the possibility of rainbow‑table attacks.
Cryptographic Strength: Using the Blowfish algorithm with multiple encryption rounds provides a high level of cryptographic security.
Standardization: Bcrypt follows a standardized format, ensuring compatibility across different implementations and platforms.
Installation and Setup
Installation via pip
Install the bcrypt library in the usual way with the pip package manager:
pip install bcrypt
System Requirements
The bcrypt library has minimal system requirements:
- Python 3.6 or newer
- Supported operating systems: Windows, macOS, Linux
- Automatic compilation of C extensions (requires a compiler)
Verifying the Installation
After installation you can verify that the library works correctly:
import bcrypt
print(bcrypt.__version__)
Basics of Working with bcrypt
How the Algorithm Works
Bcrypt follows this workflow:
- Generate a random salt
- Apply the Blowfish algorithm with the specified number of rounds
- Produce the final hash, which includes the salt and parameters
Output Hash Format
A bcrypt hash has the following structure:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
Where:
$2b$— algorithm version identifier12— cost parameter (number of rounds)R9h/cIPz0gi.URNNX3kh2O— 22‑character saltPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW— 31‑character hash value
Quick Start
Basic example of using bcrypt:
import bcrypt
# Password must be provided as bytes
password = b"supersecret123"
# Generate a hash
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
print(f"Hash: {hashed}")
# Verify the password
if bcrypt.checkpw(password, hashed):
print("Password is correct!")
else:
print("Incorrect password!")
Detailed Description of Methods and Functions
Generating a Salt
The gensalt() function creates a cryptographically strong random salt:
# Default parameters
salt = bcrypt.gensalt()
# Adjust the cost (cost factor)
salt = bcrypt.gensalt(rounds=14)
# Specify the version prefix
salt = bcrypt.gensalt(rounds=12, prefix=b"2b")
Password Hashing
The hashpw() function creates a bcrypt hash for a password:
password = b"mypassword123"
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)
# You can also use an existing hash as the salt source
new_hash = bcrypt.hashpw(b"newpassword", hashed)
Password Verification
The checkpw() function checks whether a password matches a given hash:
password = b"testpassword"
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
# Verify correct password
is_valid = bcrypt.checkpw(password, hashed) # True
# Verify incorrect password
is_invalid = bcrypt.checkpw(b"wrongpassword", hashed) # False
Rounds Parameter and Its Impact on Security
Understanding the Rounds Parameter
The rounds parameter defines the number of hashing iterations as 2^rounds. For example:
- rounds=10 → 1,024 iterations
- rounds=12 → 4,096 iterations
- rounds=14 → 16,384 iterations
Choosing an Optimal Rounds Value
Recommendations for selecting the rounds parameter:
Development & testing: rounds=10‑11 (fast) Production: rounds=12‑14 (balance security & performance) Highly sensitive data: rounds=15+ (maximum protection)
Measuring Execution Time
import time
import bcrypt
password = b"testpassword"
# Test different round values
for rounds in [10, 12, 14, 16]:
start_time = time.time()
salt = bcrypt.gensalt(rounds=rounds)
hashed = bcrypt.hashpw(password, salt)
end_time = time.time()
print(f"Rounds {rounds}: {end_time - start_time:.3f} seconds")
Working with Different Data Types
String Handling
Bcrypt works only with byte data, so strings must be encoded first:
# Correct approach
password_str = "my_password_2024"
password_bytes = password_str.encode('utf-8')
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt())
# Convenience wrapper functions
def hash_password_string(password: str) -> bytes:
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
def verify_password_string(password: str, hashed: bytes) -> bool:
return bcrypt.checkpw(password.encode('utf-8'), hashed)
Unicode Handling
When working with Unicode characters, be sure to use the proper encoding:
# Password containing Unicode symbols
unicode_password = "пароль_с_эмодзи_🔐"
password_bytes = unicode_password.encode('utf-8')
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt())
Integration with Web Frameworks
Flask Integration
from flask import Flask, request, jsonify
import bcrypt
app = Flask(__name__)
class PasswordManager:
@staticmethod
def hash_password(password: str) -> str:
"""Hash a password for storage in the DB"""
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
return hashed.decode('utf-8')
@staticmethod
def verify_password(password: str, hashed: str) -> bool:
"""Verify a password during authentication"""
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
password = data.get('password')
hashed_password = PasswordManager.hash_password(password)
# Store hashed_password in the database
return jsonify({"message": "User registered successfully"})
FastAPI Integration
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import bcrypt
app = FastAPI()
class UserCreate(BaseModel):
username: str
password: str
class UserLogin(BaseModel):
username: str
password: str
def get_password_hash(password: str) -> str:
"""Create a password hash"""
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password"""
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
@app.post("/register")
async def register_user(user: UserCreate):
hashed_password = get_password_hash(user.password)
# Save the user record
return {"message": "User created successfully"}
@app.post("/login")
async def login_user(user: UserLogin):
# Retrieve stored_hash from DB
stored_hash = "retrieve_from_database"
if verify_password(user.password, stored_hash):
return {"message": "Login successful"}
else:
raise HTTPException(status_code=401, detail="Invalid credentials")
Django Integration
from django.contrib.auth.hashers import BasePasswordHasher
import bcrypt
class BCryptPasswordHasher(BasePasswordHasher):
"""
Custom Django password hasher that uses bcrypt
"""
algorithm = "bcrypt"
def encode(self, password, salt):
hash = bcrypt.hashpw(password.encode('utf-8'), salt.encode('utf-8'))
return f"bcrypt${hash.decode('utf-8')}"
def verify(self, password, encoded):
algorithm, hash = encoded.split('$', 1)
assert algorithm == self.algorithm
return bcrypt.checkpw(password.encode('utf-8'), hash.encode('utf-8'))
def safe_summary(self, encoded):
algorithm, hash = encoded.split('$', 1)
return {
'algorithm': algorithm,
'hash': hash[:6] + '...',
}
Table of bcrypt Library Methods and Functions
| Function/Method | Parameters | Return Value | Description |
|---|---|---|---|
bcrypt.gensalt() |
rounds=12, prefix=b"2b" |
bytes |
Generates a cryptographically strong salt for hashing |
bcrypt.hashpw() |
password: bytes, salt: bytes |
bytes |
Creates a bcrypt hash of a password using the supplied salt |
bcrypt.checkpw() |
password: bytes, hashed: bytes |
bool |
Verifies that a password matches an existing hash |
bcrypt.kdf() |
password: bytes, salt: bytes, desired_key_bytes: int, rounds: int |
bytes |
Key Derivation Function (KDF) utility |
Additional Parameters and Constants
| Constant/Parameter | Value | Description |
|---|---|---|
MIN_ROUNDS |
4 | Minimum allowed rounds |
MAX_ROUNDS |
31 | Maximum allowed rounds |
DEFAULT_ROUNDS |
12 | Default rounds value |
MAX_PASSWORD_LENGTH |
72 | Maximum password length in bytes |
SALT_LENGTH |
16 | Salt length in bytes |
Performance Management
Optimization for Different Scenarios
High‑load web applications:
# Cache verification results
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_password_check(password_hash, stored_hash_tuple):
return bcrypt.checkpw(password_hash, bytes(stored_hash_tuple))
Background tasks:
import asyncio
import bcrypt
from concurrent.futures import ThreadPoolExecutor
async def hash_password_async(password: str, rounds: int = 12) -> str:
"""Asynchronous password hashing"""
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
hashed = await loop.run_in_executor(
executor,
lambda: bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=rounds))
)
return hashed.decode('utf-8')
Performance Monitoring
import time
import statistics
import bcrypt
def benchmark_bcrypt(password: str, rounds: int, iterations: int = 10):
"""Benchmark bcrypt performance"""
times = []
for _ in range(iterations):
start = time.perf_counter()
salt = bcrypt.gensalt(rounds=rounds)
bcrypt.hashpw(password.encode('utf-8'), salt)
end = time.perf_counter()
times.append(end - start)
return {
'rounds': rounds,
'mean_time': statistics.mean(times),
'median_time': statistics.median(times),
'min_time': min(times),
'max_time': max(times)
}
Security Best Practices
Core Security Principles
Never compare hashes directly:
# WRONG – vulnerable to timing attacks
def insecure_check(password: str, stored_hash: str) -> bool:
new_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
return new_hash.decode('utf-8') == stored_hash
# RIGHT
def secure_check(password: str, stored_hash: str) -> bool:
return bcrypt.checkpw(password.encode('utf-8'), stored_hash.encode('utf-8'))
Handling long passwords:
import hashlib
def secure_long_password_hash(password: str) -> str:
"""Safely hash passwords longer than 72 bytes"""
password_bytes = password.encode('utf-8')
if len(password_bytes) > 72:
# Pre‑hash long passwords
password_bytes = hashlib.sha256(password_bytes).digest()
return bcrypt.hashpw(password_bytes, bcrypt.gensalt()).decode('utf-8')
Protection Against Timing Attacks
import hmac
def constant_time_compare(a: str, b: str) -> bool:
"""Constant‑time string comparison"""
return hmac.compare_digest(a.encode('utf-8'), b.encode('utf-8'))
Security Auditing
def audit_password_policy(password: str) -> dict:
"""Audit a password against common security policies"""
return {
'length_ok': len(password) >= 8,
'has_upper': any(c.isupper() for c in password),
'has_lower': any(c.islower() for c in password),
'has_digit': any(c.isdigit() for c in password),
'has_special': any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password),
'bcrypt_compatible': len(password.encode('utf-8')) <= 72
}
Hash Migration and Upgrade
Upgrading Old Hashes
def upgrade_hash_if_needed(password: str, current_hash: str, target_rounds: int = 12) -> tuple:
"""Re‑hash a password if the stored hash uses outdated parameters"""
# Extract current rounds from the hash
parts = current_hash.split('$')
if len(parts) >= 3:
current_rounds = int(parts[2])
if current_rounds < target_rounds:
# Verify the existing password first
if bcrypt.checkpw(password.encode('utf-8'), current_hash.encode('utf-8')):
# Create a new hash with the updated rounds
new_hash = bcrypt.hashpw(password.encode('utf-8'),
bcrypt.gensalt(rounds=target_rounds))
return True, new_hash.decode('utf-8')
return False, current_hash
Migrating from Other Algorithms
import hashlib
def migrate_from_sha256(password: str, old_sha256_hash: str) -> str:
"""Migrate from a SHA‑256 hash to bcrypt"""
# Verify the old hash
if hashlib.sha256(password.encode('utf-8')).hexdigest() == old_sha256_hash:
# Create a new bcrypt hash
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
else:
raise ValueError("Invalid password for migration")
Limitations and Characteristics of bcrypt
Main Limitations
Password length restriction: bcrypt processes only the first 72 bytes of a password. Longer passwords are silently truncated.
# Demonstrate length restriction
long_password = "a" * 100 # 100 characters
truncated = "a" * 72 # 72 characters
salt = bcrypt.gensalt()
hash1 = bcrypt.hashpw(long_password.encode('utf-8'), salt)
hash2 = bcrypt.hashpw(truncated.encode('utf-8'), salt)
# Both hashes are identical!
print(hash1 == hash2) # True
No built‑in automatic upgrade: Unlike some newer algorithms, bcrypt lacks a native mechanism for automatically updating hash parameters.
Limited configurability: bcrypt offers only a single tuning parameter (rounds), whereas modern algorithms like Argon2 provide memory and parallelism controls.
Handling These Limitations
import hashlib
def handle_long_password(password: str) -> bytes:
"""Properly process passwords longer than 72 bytes"""
password_bytes = password.encode('utf-8')
if len(password_bytes) > 72:
# Compress the long password with SHA‑256 before bcrypt
return hashlib.sha256(password_bytes).digest()
return password_bytes
# Usage example
long_password = "very_long_password_" * 10
processed = handle_long_password(long_password)
hashed = bcrypt.hashpw(processed, bcrypt.gensalt())
Comparison with Alternative Algorithms
Hashing Algorithm Comparison Table
| Characteristic | bcrypt | Argon2 | PBKDF2 | scrypt |
|---|---|---|---|---|
| Year Created | 1999 | 2015 | 2000 | 2009 |
| Core Algorithm | Blowfish | Blake2 | HMAC‑SHA | Salsa20/8 |
| Configurable Parameters | rounds | memory, time, parallelism | iterations | N, r, p |
| ASIC Resistance | Moderate | High | Low | High |
| Memory Usage | Low | Configurable | Low | High |
| OWASP Recommendation | Yes | Yes (preferred) | Yes | Yes |
| Python Support | Excellent | Good | Built‑in | Good |
When to Use Each Algorithm
Use bcrypt when:
- You need a time‑tested, proven solution
- Broad compatibility is required
- The application does not need configurable memory usage
- Migrating an existing system to bcrypt
Consider Argon2 for:
- New projects with high security demands
- When strong protection against specialized hardware is essential
- Systems that can benefit from tunable memory consumption
Practical Usage Examples
Session‑Based Authentication System
import bcrypt
import secrets
from datetime import datetime, timedelta
class AuthenticationSystem:
def __init__(self):
self.users = {} # In a real app this would be a database
self.sessions = {}
def register_user(self, username: str, password: str) -> bool:
"""Register a new user"""
if username in self.users:
return False
# Validate password strength
if not self._validate_password(password):
raise ValueError("Password does not meet security requirements")
# Hash the password
password_hash = bcrypt.hashpw(password.encode('utf-8'),
bcrypt.gensalt(rounds=12))
self.users[username] = {
'password_hash': password_hash,
'created_at': datetime.now(),
'login_attempts': 0
}
return True
def authenticate_user(self, username: str, password: str) -> str:
"""Authenticate a user and create a session token"""
user = self.users.get(username)
if not user:
return None
# Lockout after too many failed attempts
if user['login_attempts'] >= 5:
raise ValueError("Account locked")
# Verify password
if bcrypt.checkpw(password.encode('utf-8'), user['password_hash']):
# Reset failed attempts
user['login_attempts'] = 0
# Create a session
session_token = secrets.token_urlsafe(32)
self.sessions[session_token] = {
'username': username,
'created_at': datetime.now(),
'expires_at': datetime.now() + timedelta(hours=24)
}
return session_token
else:
user['login_attempts'] += 1
return None
def _validate_password(self, password: str) -> bool:
"""Validate password against security policy"""
return (len(password) >= 8 and
any(c.isupper() for c in password) and
any(c.islower() for c in password) and
any(c.isdigit() for c in password))
Password‑Change API
from flask import Flask, request, jsonify
import bcrypt
import json
app = Flask(__name__)
class PasswordChangeAPI:
def __init__(self):
self.password_history = {} # Store recent password hashes to prevent reuse
def change_password(self, username: str, old_password: str,
new_password: str) -> dict:
"""Change a user's password with security checks"""
try:
# Retrieve current hash from DB
current_hash = self._get_password_hash(username)
if not current_hash:
return {'success': False, 'error': 'User not found'}
# Verify old password
if not bcrypt.checkpw(old_password.encode('utf-8'), current_hash):
return {'success': False, 'error': 'Current password is incorrect'}
# Validate new password
validation = self._validate_new_password(username, new_password)
if not validation['valid']:
return {'success': False, 'error': validation['error']}
# Create new hash
new_hash = bcrypt.hashpw(new_password.encode('utf-8'),
bcrypt.gensalt(rounds=12))
# Save the new hash
self._save_password_hash(username, new_hash)
# Update password history
self._update_password_history(username, new_hash)
return {'success': True, 'message': 'Password changed successfully'}
except Exception as e:
return {'success': False, 'error': str(e)}
def _validate_new_password(self, username: str, password: str) -> dict:
"""Validate the new password"""
if len(password) < 8:
return {'valid': False, 'error': 'Password too short'}
# Prevent reuse of recent passwords
history = self.password_history.get(username, [])
for old_hash in history:
if bcrypt.checkpw(password.encode('utf-8'), old_hash):
return {'valid': False, 'error': 'Cannot reuse recent password'}
return {'valid': True}
def _update_password_history(self, username: str, new_hash: bytes):
"""Keep the last 5 password hashes for a user"""
self.password_history.setdefault(username, []).append(new_hash)
if len(self.password_history[username]) > 5:
self.password_history[username].pop(0)
Password Reset System
import secrets
import smtplib
from email.mime.text import MIMEText
from datetime import datetime, timedelta
class PasswordResetSystem:
def __init__(self):
self.reset_tokens = {}
def request_password_reset(self, email: str) -> bool:
"""Initiate a password reset request"""
# Generate a reset token
reset_token = secrets.token_urlsafe(32)
# Store token with expiration
self.reset_tokens[reset_token] = {
'email': email,
'created_at': datetime.now(),
'expires_at': datetime.now() + timedelta(hours=1),
'used': False
}
# Send email containing the token
return self._send_reset_email(email, reset_token)
def reset_password(self, token: str, new_password: str) -> dict:
"""Reset a password using a valid token"""
token_data = self.reset_tokens.get(token)
if not token_data:
return {'success': False, 'error': 'Invalid token'}
if token_data['used']:
return {'success': False, 'error': 'Token already used'}
if datetime.now() > token_data['expires_at']:
return {'success': False, 'error': 'Token expired'}
# Create a new password hash
new_hash = bcrypt.hashpw(new_password.encode('utf-8'),
bcrypt.gensalt(rounds=12))
# Update the user's password in the DB
email = token_data['email']
self._update_user_password(email, new_hash)
# Mark token as used
token_data['used'] = True
return {'success': True, 'message': 'Password reset successfully'}
Frequently Asked Questions
Why does bcrypt limit passwords to 72 bytes?
This limitation stems from the internal architecture of the Blowfish cipher used by bcrypt. Blowfish operates on 64‑bit blocks and has a maximum key size, so passwords longer than 72 bytes are truncated. For longer inputs, pre‑hash with SHA‑256 before feeding them to bcrypt.
Can bcrypt be used to hash data other than passwords?
Technically yes, but bcrypt is optimized for passwords. For general data hashing, other algorithms (e.g., SHA‑256, Blake2) are more appropriate. Bcrypt’s intentional slowness provides security for passwords but is inefficient for bulk data.
How often should the rounds parameter be updated?
It’s advisable to review the rounds value every 2–3 years, taking into account advances in hardware performance. A good rule of thumb is to aim for a hash time of roughly 100–300 ms on your target infrastructure.
Is it safe to store bcrypt hashes in a database?
Yes. Bcrypt hashes are designed for safe storage; they embed the salt and cost parameters, and even if the database is compromised, cracking requires substantial computational effort.
Can bcrypt be used in multithreaded applications?
Yes, the bcrypt library is thread‑safe. However, because hashing is CPU‑intensive, high‑throughput services should consider thread pools or asynchronous processing to avoid bottlenecks.
What if I forget the rounds value used for a particular hash?
The rounds value is encoded inside the hash itself. You can extract it programmatically:
def extract_rounds_from_hash(hashed: str) -> int:
parts = hashed.split('$')
if len(parts) >= 3:
return int(parts[2])
return None
Is Python bcrypt compatible with other implementations?
Yes. The Python bcrypt library follows the standard format and is interoperable with implementations in PHP, Node.js, Java, and other languages, provided the same algorithm version is used.
Conclusion
Bcrypt remains one of the most reliable and widely adopted tools for password hashing in Python. Its time‑tested architecture, based on the Blowfish cipher, delivers a high level of cryptographic protection while remaining easy to use.
Key advantages of bcrypt include a built‑in salt system, an adaptive cost factor, and extensive cross‑platform support. These qualities make it an excellent choice for a broad range of applications, from simple web apps to complex enterprise systems.
Even with newer algorithms like Argon2 emerging, bcrypt continues to be recommended by many organizations, including OWASP. Its stability, performance, and rich ecosystem provide a solid foundation for building secure authentication systems.
When configured correctly, combined with security best practices and regular audits, bcrypt offers robust protection for user passwords for years to come. Remember its limitations and complement it with additional safeguards such as multi‑factor authentication and monitoring for suspicious activity.
$
The Future of AI in Mathematics and Everyday Life: How Intelligent Agents Are Already Changing the Game
Experts warned about the risks of fake charity with AI
In Russia, universal AI-agent for robots and industrial processes was developed