Skip to main content

Modern password hashing for Python — a drop-in replacement for passlib

Project description

hashward

Modern password hashing for Python. A drop-in replacement for passlib, which has been unmaintained since 2020 and is broken on Python 3.13+.

CI PyPI version Python versions License: MIT

Why hashward?

  • passlib is dead. Last release was October 2020. It crashes on Python 3.13+ due to the removed crypt module.
  • Zero required dependencies. Pure Python implementations for all hashlib-based schemes. Optional deps for argon2 and bcrypt.
  • Python 3.9+ only. No Python 2 baggage. Full type hints with py.typed marker.
  • passlib-compatible API. CryptContext, hash identification, and verify_and_update() all work the same way.
  • Secure defaults. Argon2id as the default scheme with safe parameter defaults and timing-safe comparisons.

Installation

pip install hashward

With argon2 support (recommended):

pip install hashward[argon2]

With bcrypt support:

pip install hashward[bcrypt]

With everything:

pip install hashward[all]

Quick Start

Simple hashing and verification

import hashward

# Hash a password (uses argon2id by default)
hashed = hashward.hash("my secret password")
# '$argon2id$v=19$m=65536,t=2,p=2$...'

# Verify a password
hashward.verify("my secret password", hashed)   # True
hashward.verify("wrong password", hashed)        # False

# Use a specific scheme
hashed = hashward.hash("my secret password", scheme="bcrypt")
# '$2b$12$...'

CryptContext for policy management

from hashward import CryptContext

# Create a context with your preferred schemes
ctx = CryptContext(
    schemes=["argon2", "bcrypt", "pbkdf2_sha256"],
    default="argon2",
    deprecated=["pbkdf2_sha256"],
    argon2__time_cost=3,
    argon2__memory_cost=65536,
)

# Hash and verify
hashed = ctx.hash("password")
assert ctx.verify("password", hashed)

# Automatic scheme identification
ctx.identify(hashed)  # "argon2"

# Check if a hash needs upgrading (deprecated scheme or outdated params)
ctx.needs_update(hashed)  # False (it's current)

# Verify and get a new hash if the old one needs upgrading
valid, new_hash = ctx.verify_and_update("password", old_bcrypt_hash)
if valid and new_hash:
    # Save new_hash to your database — it's been upgraded to argon2
    save_to_db(new_hash)

Hash identification

from hashward import identify

identify("$argon2id$v=19$m=65536,t=2,p=2$...")  # "argon2"
identify("$2b$12$...")                            # "bcrypt"
identify("$6$rounds=656000$...")                  # "sha512_crypt"
identify("$1$...")                                # "md5_crypt"
identify("pbkdf2_sha256$600000$...")              # "django_pbkdf2_sha256"

INI configuration (passlib-compatible)

from hashward import CryptContext

# Load from INI-format string
config = """
[hashward]
schemes = argon2, bcrypt, pbkdf2_sha256
default = argon2
deprecated = pbkdf2_sha256
argon2__time_cost = 3
argon2__memory_cost = 65536
"""
ctx = CryptContext.from_string(config)

# Serialize back
print(ctx.to_string())

Supported Schemes

Modern (recommended for new hashes)

Scheme Backend Notes
argon2 argon2-cffi Default. Argon2id, memory-hard. Requires pip install hashward[argon2].
bcrypt bcrypt Industry standard. Requires pip install hashward[bcrypt].
bcrypt_sha256 bcrypt Bcrypt with SHA-256 pre-hash (no 72-byte limit).
scrypt hashlib.scrypt (stdlib) Memory-hard. No extra dependencies.
pbkdf2_sha256 hashlib.pbkdf2_hmac (stdlib) NIST-approved. No extra dependencies.
pbkdf2_sha512 hashlib.pbkdf2_hmac (stdlib) NIST-approved. No extra dependencies.

Legacy (verification and migration only)

Scheme Notes
sha512_crypt Common in /etc/shadow. Pure Python, no crypt module needed.
sha256_crypt Common in /etc/shadow. Pure Python, no crypt module needed.
md5_crypt Insecure. Always reports needs_update.
des_crypt Insecure. Always reports needs_update.

Django compatibility

Scheme Notes
django_pbkdf2_sha256 Django's default hasher format.
django_bcrypt Django bcrypt format.
django_bcrypt_sha256 Django bcrypt+SHA-256 format.
django_argon2 Django argon2 format.
django_scrypt Django scrypt format.

Migrating from passlib

hashward provides a compatibility layer for gradual migration.

Step 1: Replace imports

# Before (passlib):
from passlib.context import CryptContext

# After (hashward — direct):
from hashward import CryptContext

# Or use the compat shim for minimal changes:
from hashward.compat.passlib import CryptContext

Step 2: Update context configuration

# passlib config:
ctx = CryptContext(schemes=["argon2", "bcrypt", "pbkdf2_sha256"])

# hashward config (identical API):
ctx = CryptContext(schemes=["argon2", "bcrypt", "pbkdf2_sha256"])

Step 3: Verify existing hashes still work

hashward can verify hashes generated by passlib. All passlib hash formats are recognized, including bcrypt_sha256 v1 and v2 formats.

from hashward import CryptContext

ctx = CryptContext(schemes=["argon2", "bcrypt", "pbkdf2_sha256"], deprecated=["pbkdf2_sha256"])

# Verify a passlib-generated hash
old_hash = "$pbkdf2-sha256$29000$..."  # generated by passlib
valid = ctx.verify("password", old_hash)

# Automatically upgrade old hashes
valid, new_hash = ctx.verify_and_update("password", old_hash)
if valid and new_hash:
    # new_hash is argon2id — save it
    save_to_db(new_hash)

API Reference

Module-level functions

hashward.hash(secret, scheme="argon2")

Hash a password using the specified scheme (default: argon2).

hashward.verify(secret, hash_string)

Verify a password against a hash string. Automatically identifies the scheme.

hashward.identify(hash_string)

Detect the hashing scheme from a hash string. Returns the scheme name or None.

CryptContext

CryptContext(schemes, default, deprecated, **settings)

Create a policy manager for password hashing.

  • schemes — list of allowed scheme names
  • default — scheme to use for new hashes
  • deprecated — list of schemes that trigger needs_update()
  • min_verify_time — minimum wall-clock seconds for verify() (default: 0, disabled). Useful for preventing timing-based user enumeration.
  • truncate_error — if True, raise PasswordValueError when a bcrypt password exceeds 72 bytes instead of silently truncating (default: False)
  • **settings — per-scheme settings using scheme__param=value syntax

ctx.hash(secret, scheme=None, **settings)

Hash a password. Uses the default scheme unless overridden.

ctx.verify(secret, hash)

Verify a password against a hash. Returns False for unrecognized hashes.

ctx.identify(hash)

Identify the scheme of a hash string. Only returns schemes configured in this context.

ctx.needs_update(hash)

Check if a hash needs re-hashing (deprecated scheme or outdated parameters).

ctx.verify_and_update(secret, hash)

Verify and return (valid, new_hash). new_hash is None if no update is needed.

ctx.using(**overrides)

Return a new CryptContext with overridden settings.

ctx.copy(**overrides)

Alias for ctx.using().

ctx.to_string() / CryptContext.from_string(ini_str)

Serialize to / deserialize from INI-format configuration strings.

Security Considerations

  • Default scheme is argon2id with memory-hard parameters (64 MiB, 2 iterations, 2 threads).
  • Timing-safe comparisons via hmac.compare_digest for all hash verification.
  • No crypt module dependency. All legacy schemes (SHA-crypt, MD5-crypt, DES-crypt) use pure Python implementations.
  • DES-crypt always reports needs_update. Any existing DES-crypt hash will trigger automatic re-hashing on the next verify_and_update() call. Do not use it for new hashes.
  • bcrypt 72-byte limit is handled: passwords are silently truncated. Use bcrypt_sha256 or truncate_error=True in CryptContext for explicit control.

Development

# Clone and set up
git clone https://github.com/agentine/hashward.git
cd hashward
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# Run benchmarks
python benchmarks/bench_hashing.py

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hashward-0.1.0.tar.gz (34.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hashward-0.1.0-py3-none-any.whl (29.4 kB view details)

Uploaded Python 3

File details

Details for the file hashward-0.1.0.tar.gz.

File metadata

  • Download URL: hashward-0.1.0.tar.gz
  • Upload date:
  • Size: 34.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for hashward-0.1.0.tar.gz
Algorithm Hash digest
SHA256 af549cd0d7c64f9e5e54be85d87fa4c0ad896037846b37316eaabb6f28196d42
MD5 0bd1680c6985152d25df93c57ca84bd0
BLAKE2b-256 b4324a9c4f286dab082719dd433da65c97a5e9d26fe445009e60b674abb3a569

See more details on using hashes here.

Provenance

The following attestation bundles were made for hashward-0.1.0.tar.gz:

Publisher: publish.yml on agentine/hashward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hashward-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: hashward-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 29.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for hashward-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5ac6a41126f133364741e1d07160cfd7d71da27905340e7c8e5af7671846985f
MD5 26a711fa635ef5ddf30b6e71ad34bcbe
BLAKE2b-256 86fa55978c91cb46a3838162cede13d7269d39f3fe4ab9fafe56c85f0aa1f043

See more details on using hashes here.

Provenance

The following attestation bundles were made for hashward-0.1.0-py3-none-any.whl:

Publisher: publish.yml on agentine/hashward

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page