# Bcrypt 5 0 0

> 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.

- Skill: `tangledgroup/bcrypt-5-0-0` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tangledgroup/bcrypt-5-0-0`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tangledgroup/bcrypt-5-0-0/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tangledgroup (https://skillmd.com/u/tangledgroup)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tangledgroup/bcrypt-5-0-0

---


# 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.

```python
import bcrypt

password = b"user_secret_password"
salt = bcrypt.gensalt()        # generates random salt with default cost 12
hashed = bcrypt.hashpw(password, salt)
# hashed is like: b'$2b$12$LJ3m4ysyJWS5a36aPCZMteXV0h0CkFDbTQM8l0PpKPF0WJwHsO.7G'
```

**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.

```python
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.

```python
import bcrypt

# Default: cost 12, prefix $2b$
salt = bcrypt.gensalt()

# Higher cost for more security (slower)
salt_strong = bcrypt.gensalt(rounds=14)

# Compatibility with older systems
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.

```python
import bcrypt

key = bcrypt.kdf(
    password=b"user_password",
    salt=b"random_salt_16_bytes",
    desired_key_bytes=32,   # 256-bit key
    rounds=100,
)
# key is 32 bytes of derived key material
```

**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:

```python
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())
    # Store in database:
    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:

```python
import bcrypt
import hashlib
import base64

def hash_long_password(password: bytes) -> bytes:
    """Hash a password of any length using SHA-256 + bcrypt."""
    # SHA-256 the password, then base64-encode to avoid NULL byte issues
    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:

```python
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  # your desired minimum

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))
            # Update stored_hash in database to new_hash
            return True, new_hash
        return True, stored_hash
    return False, stored_hash
```

### Key Derivation for Encryption

Derive an encryption key from a passphrase:

```python
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:
```python
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.

