05/18 Enhanced codes and functionalities 5

This commit is contained in:
2026-05-18 18:27:24 -04:00
parent 09d3bbdc15
commit 46765b9448
11 changed files with 1042 additions and 4 deletions
+74
View File
@@ -0,0 +1,74 @@
"""
app/models/webauthn_credential.py — WebAuthn (passkey) credential storage.
Each user can register multiple passkeys (phone, laptop, YubiKey, etc.).
Credentials are used for server authentication only — the vault key is
still derived from the master password client-side (zero-knowledge preserved).
Zero-knowledge note:
WebAuthn replaces TOTP as a second factor OR replaces the password-based
server auth (passwordless flow). In both cases the master password is still
required client-side to derive the vault key.
"""
import json
from datetime import datetime, timezone
from sqlalchemy.dialects.mysql import INTEGER
from app import db
class WebAuthnCredential(db.Model):
__tablename__ = 'webauthn_credentials'
id = db.Column(INTEGER(unsigned=True), autoincrement=True, primary_key=True)
user_id = db.Column(
INTEGER(unsigned=True),
db.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True,
)
# Base64url-encoded credential ID from the authenticator.
credential_id = db.Column(db.String(512), nullable=False, unique=True)
# COSE-encoded public key bytes, base64url.
public_key = db.Column(db.Text, nullable=False)
# Monotonically increasing counter — used to detect cloned authenticators.
sign_count = db.Column(db.BigInteger, nullable=False, default=0)
# JSON list of transport hints e.g. '["internal", "hybrid"]'
transports = db.Column(db.String(255), nullable=True)
# Authenticator AAGUID (UUID string) from attestation.
aaguid = db.Column(db.String(64), nullable=True)
# User-assigned friendly name shown in the UI.
name = db.Column(db.String(128), nullable=False, default='Passkey')
created_at = db.Column(
db.DateTime,
nullable=False,
default=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
)
last_used_at = db.Column(db.DateTime, nullable=True)
def get_transports(self) -> list:
"""Return transport hints as a Python list (empty list if none)."""
if not self.transports:
return []
try:
return json.loads(self.transports)
except (ValueError, TypeError):
return []
def set_transports(self, transports: list) -> None:
self.transports = json.dumps(transports) if transports else None
def to_dict(self) -> dict:
return {
'id': self.id,
'credential_id': self.credential_id,
'name': self.name,
'aaguid': self.aaguid,
'transports': self.get_transports(),
'created_at': self.created_at.isoformat() if self.created_at else None,
'last_used_at': self.last_used_at.isoformat() if self.last_used_at else None,
}
def __repr__(self):
return f'<WebAuthnCredential user_id={self.user_id} name={self.name!r}>'