feat: add auth subsystem with WebAuthn passkeys support
New modules: lib/auth, lib/auth_users, lib/db, lib/db_sqlite, lib/password, lib/webauthn, daemon/handlers/auth, scripts/bootstrap_auth, tests/test_auth Frontend: webui/api/auth, hoover/components/auth, pages/login, passkeys, users Updates: daemon/iface and server, lib/common and nginx, pyproject.toml deps, install script, server.py, app.js, and websocket/api clients
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""Password hashing with Argon2id.
|
||||
|
||||
Handles user password storage for the auth DB. Uses argon2-cffi (C
|
||||
implementation of the Argon2id memory-hard KDF). Random 16-byte salt is
|
||||
generated per hash by the library.
|
||||
|
||||
Argon2id is used for auth user passwords ONLY. Nginx basic-auth htpasswd
|
||||
files continue to use sha256_crypt (passlib) — that's a separate concern
|
||||
with different constraints (htpasswd format is standardized).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import InvalidHashError, VerifyMismatchError
|
||||
from argon2.low_level import Type
|
||||
|
||||
# Argon2id: OWASP recommended parameters
|
||||
# 64 MiB memory, 3 iterations, 4 parallel threads
|
||||
_PH = PasswordHasher(
|
||||
time_cost=3,
|
||||
memory_cost=65536, # 64 MiB
|
||||
parallelism=4,
|
||||
hash_len=32,
|
||||
salt_len=16,
|
||||
type=Type.ID,
|
||||
)
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Generate an Argon2id hash of *password*.
|
||||
|
||||
A random 16-byte salt is generated automatically by argon2.
|
||||
The resulting hash string starts with ``$argon2id$`` and encodes
|
||||
the algorithm version, parameters, salt, and hash output.
|
||||
|
||||
Args:
|
||||
password: Plain-text password string.
|
||||
|
||||
Returns:
|
||||
Full Argon2id hash string (e.g. ``$argon2id$v=19$m=65536,t=3,p=4$...``).
|
||||
|
||||
Raises:
|
||||
TypeError: If *password* is not a string or contains NUL bytes.
|
||||
"""
|
||||
return _PH.hash(password)
|
||||
|
||||
|
||||
def verify_password(password: str, hash_string: str) -> bool:
|
||||
"""Verify *password* against an Argon2id *hash_string*.
|
||||
|
||||
The hash string must have been produced by :func:`hash_password`.
|
||||
|
||||
Args:
|
||||
password: Plain-text password to verify.
|
||||
hash_string: Argon2id hash string to compare against.
|
||||
|
||||
Returns:
|
||||
``True`` if the password matches, ``False`` otherwise.
|
||||
|
||||
Raises:
|
||||
TypeError: If *hash_string* is not a valid Argon2id hash.
|
||||
"""
|
||||
try:
|
||||
_PH.verify(hash_string, password)
|
||||
return True
|
||||
except (InvalidHashError, VerifyMismatchError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def needs_rehash(hash_string: str) -> bool:
|
||||
"""Check if *hash_string* needs to be rehashed with updated parameters.
|
||||
|
||||
Returns True if the hash was not produced with the current parameters
|
||||
of the hasher, indicating it should be rehashed on next login.
|
||||
|
||||
Args:
|
||||
hash_string: Argon2id hash string to check.
|
||||
|
||||
Returns:
|
||||
``True`` if the hash parameters should be upgraded.
|
||||
"""
|
||||
return _PH.check_needs_rehash(hash_string)
|
||||
Reference in New Issue
Block a user