Password hashing utilities — argon2id with scrypt fallback.
Hashes passwords using the best available algorithm:
- argon2id via
argon2-cffi(preferred,pip install chirp[auth]) - scrypt via stdlib
hashlib(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
Check if argon2-cffi is available.
Hash password with scrypt, returning a PHC-format string.
Verify password against a scrypt PHC-format hash.
Build aPasswordHasherwith explicit, pinned argon2id cost params.
Cost factors follow RFC 9106 §4 (second recommended option) and are pinned to argon2-cffi's current …
Hash password with argon2id via argon2-cffi.
Verify password against an argon2 hash.
Fails closed: any argon2-cffi failure — a wrong password
(VerificationError, an Argon2Errorsubclass) or a malformed/corrupt…
Hash a password using the best available algorithm.
Uses argon2id ifargon2-cffi is installed (pip install chirp[auth]), otherwise falls back to scrypt…
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…
ReturnTrueif a scrypt hash's embedded cost is below current params.
Parsesn (and r) from the PHC string and compares to…
Report whether a stored hash should be replaced on the next login.
Parameter staleness is reported by default: an argon2 hash whose embedded cost…
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 …
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…
Verify a login credential while resisting user-enumeration via timing.
For a known user, behaves likeverify_password(). For an unknown
user (phc_hash is…
_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