Files
Moon-Household-Budget/auth.py
2026-06-06 19:12:43 -05:00

95 lines
2.4 KiB
Python

"""Passkey (WebAuthn) credential storage + config.
One "household" identity, many enrolled devices (each device = one credential).
Credentials live in passkeys.json on the volume (public keys only — there is no
shared password to leak). RP config + the AUTH_ENABLED gate come from env so the
hostname can change (mkcert host, Tailscale ts.net name, etc.) without code edits.
"""
import os
import secrets
from storage import DATA_DIR, load_json, save_json
PASSKEYS_FILE = os.path.join(DATA_DIR, "passkeys.json")
# ---- config (env-driven) ----
def rp_id():
return os.environ.get("WEBAUTHN_RP_ID", "budget.nastynas.xyz")
def rp_name():
return os.environ.get("WEBAUTHN_RP_NAME", "Moon Household Budget")
def origin():
return os.environ.get("WEBAUTHN_ORIGIN", f"https://{rp_id()}")
def auth_enabled():
return os.environ.get("AUTH_ENABLED", "0").lower() in ("1", "true", "yes", "on")
def enrollment_code():
return os.environ.get("PASSKEY_ENROLLMENT_CODE", "")
# ---- credential store ----
def _data():
d = load_json(PASSKEYS_FILE, {}) or {}
changed = False
if "credentials" not in d:
d["credentials"] = []
changed = True
if not d.get("user_handle"):
d["user_handle"] = secrets.token_urlsafe(16)
changed = True
if changed:
save_json(PASSKEYS_FILE, d)
return d
def user_handle_bytes():
return _data()["user_handle"].encode()
def list_credentials():
return _data()["credentials"]
def has_credentials():
return len(_data()["credentials"]) > 0
def get_credential(cred_id_b64):
for c in _data()["credentials"]:
if c["id"] == cred_id_b64:
return c
return None
def add_credential(cred_id_b64, public_key_b64, sign_count, label, transports=None):
d = _data()
d["credentials"].append({
"id": cred_id_b64,
"public_key": public_key_b64,
"sign_count": sign_count,
"label": label or "Device",
"transports": transports or [],
})
save_json(PASSKEYS_FILE, d)
def update_sign_count(cred_id_b64, new_count):
d = _data()
for c in d["credentials"]:
if c["id"] == cred_id_b64:
c["sign_count"] = new_count
save_json(PASSKEYS_FILE, d)
def remove_credential(cred_id_b64):
d = _data()
d["credentials"] = [c for c in d["credentials"] if c["id"] != cred_id_b64]
save_json(PASSKEYS_FILE, d)