bcrypt - password recreation

онлайн тренажер по питону
Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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:

  1. Generate a random salt
  2. Apply the Blowfish algorithm with the specified number of rounds
  3. 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 identifier
  • 12 — cost parameter (number of rounds)
  • R9h/cIPz0gi.URNNX3kh2O — 22‑character salt
  • PST9/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.

$

News