passlib bcrypt hashing with a cryptography HMAC pepper: working versions (tested 2026-10-15)
Hash and verify passwords with passlib's CryptContext bcrypt scheme, prefixing each password with a cryptography HMAC-SHA256 pepper so bcrypt's 72 byte limit cannot silently truncate, and cover identify, unknown hashes and work factor rehash detection offline.
passed5 passed · 0 failed2026-10-15 15:57 UTC- Status
- passed
- Last passed
- 2026-10-15 15:57 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions4
passlib@1.7.4bcrypt@4.0.1cryptography@50.0.2pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38764699834
The code
Exactly the files that passed. Your agent can pull them with get_recipe("cryptography-passlib-bcrypt") (install), or see them on GitHub.
requirements.txt
passlib==1.7.4 bcrypt==4.0.1 cryptography==50.0.2 pytest==9.1.1
passwords.py
import base64
from cryptography.hazmat.primitives import hashes, hmac
from passlib.context import CryptContext
bcrypt_ctx = CryptContext(schemes=["bcrypt"], bcrypt__rounds=12)
def pepper(password: str, key: bytes) -> bytes:
h = hmac.HMAC(key, hashes.SHA256())
h.update(password.encode("utf-8"))
return base64.b64encode(h.finalize())
def hash_password(password: str, key: bytes) -> str:
return bcrypt_ctx.hash(pepper(password, key))
def verify_password(password: str, stored_hash: str, key: bytes) -> bool:
return bcrypt_ctx.verify(pepper(password, key), stored_hash)
def needs_rehash(stored_hash: str) -> bool:
return bcrypt_ctx.needs_update(stored_hash)
def hash_scheme(stored_hash: str) -> str | None:
return bcrypt_ctx.identify(stored_hash)
tests/test_passwords.py
import pytest
from passlib.context import CryptContext
from passlib.exc import UnknownHashError
from passwords import (
bcrypt_ctx,
hash_password,
hash_scheme,
needs_rehash,
pepper,
verify_password,
)
KEY = bytes(range(32))
def test_roundtrip_salts_each_hash():
first = hash_password("correct horse battery staple", KEY)
second = hash_password("correct horse battery staple", KEY)
assert first != second
assert verify_password("correct horse battery staple", first, KEY)
assert not verify_password("wrong password", first, KEY)
assert hash_scheme(first) == "bcrypt"
def test_pepper_avoids_the_72_byte_truncation_collision():
base = "a" * 72
left = hash_password(base + "LEFT", KEY)
assert not verify_password(base + "RIGHT", left, KEY)
raw = bcrypt_ctx.hash(base + "LEFT")
assert bcrypt_ctx.verify(base + "RIGHT", raw)
def test_pepper_key_is_required_and_deterministic():
stored = hash_password("hunter2", KEY)
assert verify_password("hunter2", stored, KEY)
assert not verify_password("hunter2", stored, bytes(32))
assert pepper("hunter2", KEY) == pepper("hunter2", KEY)
assert len(pepper("x" * 1000, KEY)) < 72
def test_verify_raises_for_unidentifiable_hashes():
forged = "$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$abcdef"
assert hash_scheme(forged) is None
with pytest.raises(UnknownHashError):
verify_password("hunter2", forged, KEY)
def test_needs_rehash_tracks_work_factor():
cheap_ctx = CryptContext(schemes=["bcrypt"], bcrypt__rounds=4)
cheap = cheap_ctx.hash(pepper("hunter2", KEY))
current = hash_password("hunter2", KEY)
assert needs_rehash(cheap)
assert not needs_rehash(current)
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.