95 lines
2.4 KiB
Python
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)
|