"""Durable, one-time WebAuthn challenges and device-bound passkey credentials.""" import hashlib import sqlite3 from dataclasses import dataclass from pathlib import Path from typing import Callable from src.private_state import connect_private_sqlite from src.session_store import SessionStoreError @dataclass(frozen=True) class StoredPasskey: credential_id: bytes public_key: bytes sign_count: int device_label: str management_id: str created_at: int principal_id: int class PasskeyStore: def __init__( self, path: str | Path, *, clock: Callable[[], float], max_challenges: int = 10_000, max_challenges_per_source: int = 10, ) -> None: self.path = Path(path) self.clock = clock self.max_challenges = max(1, max_challenges) self.max_challenges_per_source = max(1, max_challenges_per_source) @staticmethod def _digest(value: bytes | str) -> str: raw = value if isinstance(value, bytes) else value.encode() return hashlib.sha256(raw).hexdigest() def _connect(self) -> sqlite3.Connection: try: connection = connect_private_sqlite(self.path, timeout=0.1) connection.execute( """ CREATE TABLE IF NOT EXISTS passkey_credentials ( credential_id BLOB PRIMARY KEY, public_key BLOB NOT NULL, sign_count INTEGER NOT NULL, device_label TEXT NOT NULL, management_id TEXT NOT NULL UNIQUE, created_at INTEGER NOT NULL, principal_id INTEGER ) """ ) credential_columns = { row[1] for row in connection.execute("PRAGMA table_info(passkey_credentials)") } if "principal_id" not in credential_columns: connection.execute( "ALTER TABLE passkey_credentials ADD COLUMN principal_id INTEGER" ) connection.execute( """ CREATE TABLE IF NOT EXISTS passkey_challenges ( challenge_hash TEXT PRIMARY KEY, session_hash TEXT, source_hash TEXT, purpose TEXT NOT NULL, action TEXT NOT NULL, target TEXT NOT NULL, expires_at INTEGER NOT NULL ) """ ) columns = { row[1] for row in connection.execute("PRAGMA table_info(passkey_challenges)") } if "source_hash" not in columns: connection.execute( "ALTER TABLE passkey_challenges ADD COLUMN source_hash TEXT" ) return connection except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def issue_challenge( self, challenge: bytes, *, session_id: str | None, purpose: str, action: str, target: str, source: str | None = None, ttl_seconds: int = 120, ) -> None: now = int(self.clock()) source_hash = self._digest(source or session_id or f"{purpose}:{action}:{target}") try: with self._connect() as connection: connection.execute("BEGIN IMMEDIATE") connection.execute("DELETE FROM passkey_challenges WHERE expires_at <= ?", (now,)) connection.execute( "INSERT INTO passkey_challenges(" "challenge_hash, session_hash, source_hash, purpose, action, target, expires_at" ") VALUES (?, ?, ?, ?, ?, ?, ?)", ( self._digest(challenge), self._digest(session_id) if session_id else None, source_hash, purpose, action, target, now + max(1, ttl_seconds), ), ) connection.execute( """ DELETE FROM passkey_challenges WHERE source_hash = ? AND challenge_hash NOT IN ( SELECT challenge_hash FROM passkey_challenges WHERE source_hash = ? ORDER BY expires_at DESC, rowid DESC LIMIT ? ) """, (source_hash, source_hash, self.max_challenges_per_source), ) connection.execute( """ DELETE FROM passkey_challenges WHERE challenge_hash NOT IN ( SELECT challenge_hash FROM passkey_challenges ORDER BY expires_at DESC, rowid DESC LIMIT ? ) """, (self.max_challenges,), ) except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def consume_challenge( self, challenge: bytes, *, session_id: str | None, purpose: str, action: str, target: str, ) -> bool: now = int(self.clock()) try: with self._connect() as connection: connection.execute("DELETE FROM passkey_challenges WHERE expires_at <= ?", (now,)) cursor = connection.execute( "DELETE FROM passkey_challenges WHERE challenge_hash = ? " "AND session_hash IS ? AND purpose = ? AND action = ? AND target = ? " "AND expires_at > ?", ( self._digest(challenge), self._digest(session_id) if session_id else None, purpose, action, target, now, ), ) return cursor.rowcount == 1 except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def register( self, *, credential_id: bytes, public_key: bytes, sign_count: int, device_label: str, management_id: str, principal_id: int, ) -> None: try: with self._connect() as connection: connection.execute( "INSERT INTO passkey_credentials(credential_id, public_key, sign_count, " "device_label, management_id, created_at, principal_id) " "VALUES (?, ?, ?, ?, ?, ?, ?)", ( credential_id, public_key, sign_count, device_label, management_id, int(self.clock()), principal_id, ), ) except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def all(self, *, principal_id: int) -> list[StoredPasskey]: try: with self._connect() as connection: rows = connection.execute( "SELECT credential_id, public_key, sign_count, device_label, management_id, " "created_at, principal_id FROM passkey_credentials " "WHERE principal_id = ? ORDER BY created_at DESC", (principal_id,), ).fetchall() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc return [StoredPasskey(*row) for row in rows] def get(self, credential_id: bytes, *, principal_id: int) -> StoredPasskey | None: try: with self._connect() as connection: row = connection.execute( "SELECT credential_id, public_key, sign_count, device_label, management_id, " "created_at, principal_id FROM passkey_credentials " "WHERE credential_id = ? AND principal_id = ?", (credential_id, principal_id), ).fetchone() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc return StoredPasskey(*row) if row else None def get_management_id( self, management_id: str, *, principal_id: int ) -> StoredPasskey | None: try: with self._connect() as connection: row = connection.execute( "SELECT credential_id, public_key, sign_count, device_label, management_id, " "created_at, principal_id FROM passkey_credentials " "WHERE management_id = ? AND principal_id = ?", (management_id, principal_id), ).fetchone() except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc return StoredPasskey(*row) if row else None def advance_counter( self, credential_id: bytes, *, expected: int, new: int ) -> bool: if expected == 0 and new == 0: comparison = "sign_count = 0" elif new > expected: comparison = "sign_count = ?" else: return False try: with self._connect() as connection: parameters = ( (new, credential_id) if expected == 0 and new == 0 else (new, credential_id, expected) ) cursor = connection.execute( f"UPDATE passkey_credentials SET sign_count = ? " f"WHERE credential_id = ? AND {comparison}", parameters, ) return cursor.rowcount == 1 except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def revoke_management_id(self, management_id: str) -> bool: try: with self._connect() as connection: cursor = connection.execute( "DELETE FROM passkey_credentials WHERE management_id = ?", (management_id,) ) return cursor.rowcount == 1 except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def revoke_access( self, management_id: str, *, preserve_session: bool ) -> tuple[bool, bool]: """Atomically remove one passkey and, when requested, its active session.""" try: with self._connect() as connection: credential = connection.execute( "SELECT 1 FROM passkey_credentials WHERE management_id = ?", (management_id,), ).fetchone() if credential is None: return False, False session_revoked = False if not preserve_session: session = connection.execute( "SELECT session_hash FROM active_sessions WHERE management_id = ?", (management_id,), ).fetchone() if session is not None: connection.execute( "DELETE FROM step_up_grants WHERE session_hash = ?", (session[0],) ) cursor = connection.execute( "DELETE FROM active_sessions WHERE management_id = ?", (management_id,), ) session_revoked = cursor.rowcount == 1 cursor = connection.execute( "DELETE FROM passkey_credentials WHERE management_id = ?", (management_id,), ) return cursor.rowcount == 1, session_revoked except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def revoke_all(self) -> None: try: with self._connect() as connection: connection.execute("DELETE FROM passkey_credentials") connection.execute("DELETE FROM passkey_challenges") except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Passkey registry is temporarily unavailable") from exc def revoke_device_access(self, management_id: str, *, principal_id: int) -> bool: """Atomically remove one active session, its grants, and its linked passkey.""" try: with self._connect() as connection: session = connection.execute( "SELECT session_hash FROM active_sessions " "WHERE management_id = ? AND principal_id = ?", (management_id, principal_id), ).fetchone() if session is None: return False connection.execute( "DELETE FROM step_up_grants WHERE session_hash = ?", (session[0],) ) connection.execute( "DELETE FROM passkey_credentials " "WHERE management_id = ? AND principal_id = ?", (management_id, principal_id), ) cursor = connection.execute( "DELETE FROM active_sessions " "WHERE management_id = ? AND principal_id = ?", (management_id, principal_id), ) return cursor.rowcount == 1 except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc def revoke_all_access(self, *, principal_id: int) -> list[str]: """Atomically remove one principal's passkeys, grants, and active sessions.""" try: with self._connect() as connection: management_ids = [ row[0] for row in connection.execute( "SELECT management_id FROM active_sessions " "WHERE principal_id = ? ORDER BY management_id", (principal_id,), ).fetchall() ] connection.execute( "DELETE FROM passkey_credentials WHERE principal_id = ?", (principal_id,), ) connection.execute( "DELETE FROM step_up_grants WHERE session_hash IN (" "SELECT session_hash FROM active_sessions WHERE principal_id = ?)", (principal_id,), ) connection.execute( "DELETE FROM active_sessions WHERE principal_id = ?", (principal_id,), ) return management_ids except (OSError, sqlite3.Error) as exc: raise SessionStoreError("Session registry is temporarily unavailable") from exc