security.passwords

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Password hashing utilities — argon2id with scrypt fallback.

Hashes passwords using the best available algorithm:

  1. argon2id viaargon2-cffi (preferred, pip install chirp[auth])
  2. scrypt via stdlibhashlib(fallback, always available)

Both produce…

Password hashing utilities — argon2id with scrypt fallback.

Hashes passwords using the best available algorithm:

  1. argon2id viaargon2-cffi (preferred, pip install chirp[auth])
  2. scrypt via stdlibhashlib(fallback, always available)

Both produce PHC-format strings.verify_passwordauto-detects the algorithm from the hash prefix, so hashes are forward-compatible if the default changes.

Usage::

from chirp.security.passwords import hash_password, verify_password

hashed = hash_password("my-password")
ok = verify_password("my-password", hashed)

security.passwords

Name Type Default Description
type
qualified_name
element_type
description
source_file
line_number
is_autodoc
autodoc_element
_autodoc_template
_autodoc_url_path
_autodoc_page_type
title
doc_content_hash

Symbols on this page

function _has_argon2

Check if argon2-cffi is available.

Jump to symbol
function _hash_scrypt

Hash password with scrypt, returning a PHC-format string.

Jump to symbol
function _verify_scrypt

Verify password against a scrypt PHC-format hash.

Jump to symbol
function _argon2_hasher

Build aPasswordHasherwith explicit, pinned argon2id cost params.

Cost factors follow RFC 9106 §4 (second recommended option) and are pinned to argon2-cffi's current …

Jump to symbol
function _hash_argon2

Hash password with argon2id via argon2-cffi.

Jump to symbol
function _verify_argon2

Verify password against an argon2 hash.

Fails closed: any argon2-cffi failure — a wrong password (VerificationError, an Argon2Errorsubclass) or a malformed/corrupt…

Jump to symbol
function hash_password

Hash a password using the best available algorithm.

Uses argon2id ifargon2-cffi is installed (pip install chirp[auth]), otherwise falls back to scrypt…

Jump to symbol
function verify_password

Verify a password against a PHC-format hash.

Auto-detects the algorithm from the hash prefix. This means hashes created with argon2 can be verified even…

Jump to symbol
function _scrypt_needs_rehash

ReturnTrueif a scrypt hash's embedded cost is below current params.

Parsesn (and r) from the PHC string and compares to…

Jump to symbol
function needs_rehash

Report whether a stored hash should be replaced on the next login.

Parameter staleness is reported by default: an argon2 hash whose embedded cost…

Jump to symbol
function verify_and_upgrade

Verify a password and opportunistically return an upgraded hash.

Returns(ok, new_hash_or_None). new_hashis a freshly computed hash only when the password verified …

Jump to symbol
function _decoy_hash

Return the process-wide decoy hash, computing it once (thread-safe).

Double-checked locking: the common (already-initialized) path reads the published value without taking the lock; only…

Jump to symbol
function verify_login

Verify a login credential while resisting user-enumeration via timing.

For a known user, behaves likeverify_password(). For an unknown user (phc_hash is…

Jump to symbol
_has_argon2
function
def _has_argon2() -> bool

Check if argon2-cffi is available.

No parameters.

_hash_scrypt
function
def _hash_scrypt(password: str) -> str

Hash password with scrypt, returning a PHC-format string.

Parameters

Name Type Default Description
password str
_verify_scrypt
function
def _verify_scrypt(password: str, phc_hash: str) -> bool

Verify password against a scrypt PHC-format hash.

Parameters

Name Type Default Description
password str
phc_hash str
_argon2_hasher
function
def _argon2_hasher()

Build aPasswordHasherwith explicit, pinned argon2id cost params.

Cost factors follow RFC 9106 §4 (second recommended option) and are pinned to argon2-cffi's currentPasswordHasher()defaults so existing hashes still verify. The same construction is used for hashing and verifying so the two paths always agree.

No parameters.

_hash_argon2
function
def _hash_argon2(password: str) -> str

Hash password with argon2id via argon2-cffi.

Parameters

Name Type Default Description
password str
_verify_argon2
function
def _verify_argon2(password: str, phc_hash: str) -> bool

Verify password against an argon2 hash.

Fails closed: any argon2-cffi failure — a wrong password (VerificationError, an Argon2Errorsubclass) or a malformed/corrupt hash string (InvalidHashError, which derives from ValueError— NOT fromArgon2Error) — returns Falseinstead of propagating. Both are caught so any malformed-input failure fails closed, mirroring the scrypt path which returnsFalsefor unparseable hashes.

Parameters

Name Type Default Description
password str
phc_hash str
hash_password
function
def hash_password(password: str) -> str

Hash a password using the best available algorithm.

Uses argon2id ifargon2-cffi is installed (pip install chirp[auth]), otherwise falls back to scrypt (stdlib).

Returns a PHC-format string safe for database storage.

Parameters

Name Type Default Description
password str The plaintext password to hash.
verify_password
function
def verify_password(password: str, phc_hash: str) -> bool

Verify a password against a PHC-format hash.

Auto-detects the algorithm from the hash prefix. This means hashes created with argon2 can be verified even if the default algorithm later changes (and vice versa).

Parameters

Name Type Default Description
password str The plaintext password to check.
phc_hash str The stored hash (from ``hash_password``).
_scrypt_needs_rehash
function
def _scrypt_needs_rehash(phc_hash: str) -> bool

ReturnTrueif a scrypt hash's embedded cost is below current params.

Parsesn (and r) from the PHC string and compares to the current _SCRYPT_*constants. A malformed/unparseable hash is treated as stale (True) so a corrupt stored hash is re-derived on the next successful login rather than being trusted indefinitely.

Parameters

Name Type Default Description
phc_hash str
needs_rehash
function
def needs_rehash(phc_hash: str, *, upgrade_algorithm: bool = False) -> bool

Report whether a stored hash should be replaced on the next login.

Parameter staleness is reported by default: an argon2 hash whose embedded cost factors are below the current pinned params (per argon2-cffi's PasswordHasher.check_needs_rehash), or a scrypt hash whose embedded n/r is below the current _SCRYPT_*constants.

The algorithm upgrade clause — flagging a scrypt hash as stale only because argon2 is now installed and is the new default — is gated behind upgrade_algorithm and is off by default. Installing the auth extra must not silently mark every existing scrypt hash stale: on non-deterministic / autoscaled hosts that would trigger a fleet-wide rehash-write storm the first time each host serves a login. Opt in explicitly (e.g. a controlled migration window) when you want algorithm upgrades.

Parameters

Name Type Default Description
phc_hash str The stored PHC-format hash to inspect.
upgrade_algorithm bool False When ``True``, a scrypt hash is also considered stale if argon2 is available (algorithm upgrade). Default ``False``.
verify_and_upgrade
function
def verify_and_upgrade(password: str, phc_hash: str, *, upgrade_algorithm: bool = False) -> tuple[bool, str | None]

Verify a password and opportunistically return an upgraded hash.

Returns(ok, new_hash_or_None). new_hashis a freshly computed hash only when the password verified andneeds_rehash() reports the stored hash is stale.upgrade_algorithmis forwarded to needs_rehash() and defaults to False(parameter staleness only — scrypt→argon2 algorithm upgrades require an explicit opt-in). A hash is never re-derived for a wrong password, so a failed guess can never cause a database write.

Typical use on login (parameter upgrades only)::

ok, new_hash = verify_and_upgrade(password, user.password_hash)
if not ok:
    return reject()
if new_hash is not None:
    user.password_hash = new_hash  # persist the upgrade

Controlled scrypt→argon2 migration window::

ok, new_hash = verify_and_upgrade(
    password, user.password_hash, upgrade_algorithm=True
)
if not ok:
    return reject()
if new_hash is not None:
    user.password_hash = new_hash  # persist the upgrade

Parameters

Name Type Default Description
password str The plaintext password to check.
phc_hash str The stored PHC-format hash.
upgrade_algorithm bool False When ``True``, also rehash a current-cost scrypt hash to argon2 when argon2 is available. Default ``False`` (storm-
_decoy_hash
function
def _decoy_hash() -> str

Return the process-wide decoy hash, computing it once (thread-safe).

Double-checked locking: the common (already-initialized) path reads the published value without taking the lock; only the first caller(s) contend on_DECOY_LOCKand exactly one performs the (expensive) hash.

No parameters.

verify_login
function
def verify_login(password: str, phc_hash: str | None) -> bool

Verify a login credential while resisting user-enumeration via timing.

For a known user, behaves likeverify_password(). For an unknown user (phc_hash is None) it still runs a full hash verification against a module-level decoy hash before returningFalse, so the unknown-user and wrong-password code paths take comparable time and an attacker cannot tell "no such user" from "wrong password" by latency.

Residual: the timing equivalence is approximate, not unconditional. In a mixed-algorithm corpus — legacy scrypt hashes stored while argon2 is now the default — the decoy is computed with the current default algorithm, so its verify cost differs from a stored scrypt verify. The defence removes the coarse known-vs-unknown signal; it does not promise byte-constant timing across algorithms. Re-deriving stale hashes (seeverify_and_upgrade()) converges the corpus toward a single algorithm over time.

Parameters

Name Type Default Description
password str The plaintext password supplied at login.
phc_hash str | None The stored hash for the looked-up user, or ``None`` when no such user exists.

View source · /home/runner/work/chirp/chirp/site/../src/chirp/security/passwords.py:1