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:
2026-07-24 01:21:39 +00:00
parent 04417cf05c
commit 56b200d233
28 changed files with 4900 additions and 82 deletions
+83
View File
@@ -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)