Files
Password-Manager/delphi-backend/Handlers
Zaki cf94f67488 feat(2fa): TOTP secret storage + live 6-digit code generation
Adds RFC 6238 TOTP (Google Authenticator-style) support to every entry.
The secret is encrypted client-side with the same AES-GCM key as the
password — the server stores opaque ciphertext and never sees the
plaintext base32 secret.

Schema
======
vault_entries.totp_secret TEXT  -- AES-GCM ciphertext, base64
vault_entries.totp_iv     TEXT  -- 12-byte IV, base64
Both NULL when the entry has no 2FA configured. Added via
ApplyMigrations.AddColumnIfMissing so existing vaults migrate cleanly.

Backend
=======
HandleListEntries: includes totp_secret + totp_iv in the response (or
JSON null when not configured).
HandleCreateEntry / HandleUpdateEntry: accept both fields; empty string
in the body → server stores NULL. Clearing the secret removes 2FA
from the entry.

Frontend
========
TOTP primitives (pure crypto.subtle, no external lib):
 - base32Decode(s)         — RFC 4648, tolerates spaces / lowercase
 - generateTOTP(secret)    — HMAC-SHA1 + RFC 4226 dynamic truncation
 - parseOtpAuthUri(raw)    — extracts ?secret from otpauth:// URIs

UI in the slide-over (the canonical entry detail view):
 - New "Two-factor (TOTP)" field below the password row.
 - Input is password-masked by default with eye-toggle to reveal.
 - Pasting a full otpauth:// URI auto-extracts the secret param so the
   user can copy directly from a QR-code scanner without manual cleanup.
 - X button clears the secret (= removes 2FA on next save).
 - Live code panel below: large monospace "123 456" + Copy button
   (routes through Bridge.copySecure → secure clipboard + 30s auto-clear).
 - Linear progress bar drains over the 30s window, turns red < 5s.
 - Refresh tick runs once per second while the slide-over is open;
   stops on closeSlideOver to avoid background work.

Entry card meta now shows a "2FA" chip when totp_secret is non-null —
quick visual scan for which accounts have 2FA configured without
opening the slide-over.

Validation
==========
soSave calls base32Decode(secret) before encrypting to refuse obviously
broken input. Otherwise garbled base32 would save fine and only fail
in the code panel next time.

Migration interaction (KDF 100k→600k)
=====================================
KNOWN MINOR ISSUE: /migrate-kdf only re-encrypts encrypted_password+iv,
not totp_secret+totp_iv. In practice this is harmless because:
  1) KDF migration runs immediately after login on legacy accounts —
     before the user has a chance to add a TOTP secret.
  2) New accounts start at 600k iterations, no migration ever needed.
A legacy user who somehow added a TOTP between login and the
background migration completing would end up with a TOTP encrypted
under the old key. The fix (extend /migrate-kdf to re-encrypt TOTP
fields too) is a one-line follow-up if anyone hits the edge case.
2026-05-23 05:13:50 +01:00
..