Skip to content

Cluster 3 — TOTP / 2FA generator (RFC 6238 + QR import) [v2.0]#45

Merged
pexatar merged 1 commit into
mainfrom
feat/2.0/main
May 18, 2026
Merged

Cluster 3 — TOTP / 2FA generator (RFC 6238 + QR import) [v2.0]#45
pexatar merged 1 commit into
mainfrom
feat/2.0/main

Conversation

@pexatar
Copy link
Copy Markdown
Owner

@pexatar pexatar commented May 18, 2026

Third cluster of PassKey 2.0 — the first new-feature cluster (clusters 1 & 2 were refactor + security). See `piano-revisione-correzione-miglioramento.md` for the full plan.

Summary

PassKey can now store TOTP / 2FA seeds alongside password entries and render a live 6-digit code with a 30-second countdown directly inside the password detail panel. The implementation follows RFC 6238 and is interoperable with Google Authenticator, Microsoft Authenticator, Authy, Bitwarden, 1Password, and every mainstream 2FA-enabled service.

Core (`PassKey.Core`)

  • New NuGet: Otp.NET 1.4.1 (MIT) — RFC 6238 + Base32 primitives.
  • `PasswordEntry` gains four optional fields: `TotpSecret`, `TotpAlgorithm`, `TotpDigits`, `TotpPeriod`. Defaults preserve full v1.x backward compatibility — when the JSON property is absent, the TOTP section is hidden in the UI.
  • `ITotpService` / `TotpService`:
    • `GenerateCode(entry)` — current code, empty when no secret
    • `RemainingSeconds(entry)` — countdown in `[1, period]`
    • `ParseOtpAuthUri(uri)` — full `otpauth://totp/...` parser (issuer/account split, algorithm/digits/period coercion, defensive fallbacks)
    • `IsValidBase32(secret)` — RFC 4648 alphabet check tolerant to whitespace, lowercase, and `=` padding
  • `BitwardenImporter` and `OnePuxImporter` now optionally receive `ITotpService` and promote the Bitwarden `login.totp` field / the 1Password 1pux one-time-password section fields into structured `TotpSecret` (with `otpauth://` URI detection) instead of dropping them into the notes blob.

Desktop (`PassKey.Desktop`)

  • New NuGet: ZXing.Net 0.16.11 (Apache 2.0). Platform-binding subpackages were skipped because the current one requires SkiaSharp 3.x — conflicts with our `2.88.9` pin for the CVE override (`GHSA-j7hp-h8jx-5ppr`). QR decoding reads BGRA8 pixels from `Windows.Graphics.Imaging.BitmapDecoder` and feeds `RGBLuminanceSource` directly into ZXing's `QRCodeReader` / `HybridBinarizer` pipeline.
  • `PasswordDetailViewModel` exposes `TotpSecret` / `TotpAlgorithm` / `TotpDigits` / `TotpPeriod` plus derived `CurrentTotpCode` / `TotpRemainingSeconds`, with helpers `RefreshTotpDisplay` / `GetCurrentTotpCodeRaw` / `ApplyOtpAuthUri` / `ApplyManualSeed` / `RemoveTotp`.
  • `PasswordDetailView` gains a new "Codice 2FA (TOTP)" section above Notes:
    • Empty state — three buttons: `Scansiona QR` (file picker → ZXing), `Incolla URI` (clipboard otpauth://), `Inserisci seed` (manual Base32 dialog).
    • Filled state — formatted `xxx xxx` live code, ProgressRing countdown with seconds-remaining numeric overlay, copy / show-secret / remove buttons.
  • A `DispatcherQueueTimer` (1 s interval) is started in `SetViewModel` and stopped in `Unloaded` — no handler leaks, no work while the view is detached.
  • "Copia codice" routes through the existing `IClipboardService` so the 30 s auto-clear and clipboard-history suppression apply to TOTP codes too.
  • DI: `TotpService` registered as singleton; importers consume it.
  • Localization: 5 new `x:Uid` keys × 6 cultures (`it-IT`, `en-GB`, `fr-FR`, `de-DE`, `es-ES`, `pt-PT`) — 30 strings total.

Backward compatibility

  • v1.x vaults open unchanged — `TotpSecret == null` suppresses the TOTP section.
  • Backups in the v0x02 Argon2id format (cluster 2) carry the new fields end-to-end without further migration.
  • The `.pkbak` legacy v0x01 PBKDF2 format still restores (unrelated to this cluster, but unaffected).

Test plan

  • `dotnet test` → 199 / 199 (was 182; +17 `TotpServiceTests`)
  • Local installer build via `scripts/build-installer.ps1`
  • T3.GATE smoke test with real GitHub 2FA setup:
    • QR code scanned from a PNG screenshot via "Scansiona QR" → entry transitions to filled state with formatted code + active countdown
    • Code matched the corresponding 6 digits shown by an authenticator app on the user's phone, confirming RFC 6238 conformance against a known-good third-party implementation

PassKey 2.0 now stores TOTP seeds alongside password entries and renders a
live 6-digit code with a 30-second countdown directly inside the password
detail panel. The implementation follows RFC 6238 (Time-Based One-Time Password
Algorithm), defaulting to SHA1 / 6 digits / 30s window for compatibility with
Google Authenticator, Microsoft Authenticator, Authy, and every mainstream
2FA-enabled web service.

Core (PassKey.Core):
- New NuGet: Otp.NET 1.4.1 (MIT) for RFC 6238 / Base32 primitives.
- PasswordEntry gains four optional fields (TotpSecret, TotpAlgorithm,
  TotpDigits, TotpPeriod) with safe defaults so v1.x vaults open unchanged —
  the JSON properties simply remain absent when no seed is configured.
- ITotpService / TotpService:
    * GenerateCode(entry)        — current 6-10 digit code (empty when no secret)
    * RemainingSeconds(entry)    — countdown in [1, period]
    * ParseOtpAuthUri(uri)       — full otpauth://totp/... parser including
                                   issuer/account label split, algorithm
                                   coercion to SHA1/256/512, digits/period
                                   validation, defensive fallbacks
    * IsValidBase32(secret)      — RFC 4648 alphabet check ignoring whitespace,
                                   lowercase, and "=" padding

Desktop (PassKey.Desktop):
- New NuGet: ZXing.Net 0.16.11 (Apache 2.0). The platform binding subpackage
  was avoided because the only current one requires SkiaSharp 3.x which
  conflicts with our 2.88.9 pin override for the SkiaSharp CVE
  (GHSA-j7hp-h8jx-5ppr). QR decoding instead reads raw BGRA8 pixels from
  Windows.Graphics.Imaging.BitmapDecoder and feeds RGBLuminanceSource
  directly into ZXing's QRCodeReader/HybridBinarizer pipeline.
- PasswordDetailViewModel exposes TotpSecret/Algorithm/Digits/Period plus
  derived CurrentTotpCode and TotpRemainingSeconds, with public helpers
  RefreshTotpDisplay / GetCurrentTotpCodeRaw / ApplyOtpAuthUri /
  ApplyManualSeed / RemoveTotp used by the View.
- PasswordDetailView gains a new "Codice 2FA (TOTP)" section above the notes:
    * Empty state — three buttons (Scansiona QR, Incolla URI, Inserisci seed).
    * Filled state — formatted "xxx xxx" code, ProgressRing countdown ring
      with seconds-remaining numeric overlay, copy/show-secret/remove buttons.
- A DispatcherQueueTimer (1 s interval) is started in SetViewModel and
  stopped in Unloaded so the code refreshes live without leaking handlers.
- The "Copia codice" button routes through the existing IClipboardService so
  the 30-second clipboard auto-clear and Windows clipboard-history suppression
  apply to TOTP codes just like to passwords.
- DI registers TotpService as a singleton; BitwardenImporter / OnePuxImporter
  now optionally receive ITotpService and use it to promote the
  Bitwarden "login.totp" field and the 1Password 1pux one-time-password
  section fields into structured TotpSecret values (with otpauth:// URI
  detection) instead of dropping them into the notes blob.
- Localization: 5 new x:Uid keys (FieldTotp, TotpScanQrLabel, TotpPasteUriLabel,
  TotpEnterSecretLabel, TotpCodeLabel) translated across all 6 supported
  cultures (it-IT, en-GB, fr-FR, de-DE, es-ES, pt-PT).

Tests: 199 passed (was 182).
- TotpServiceTests: 17 cases — otpauth URI minimal/full parameter parsing,
  unsupported-algorithm fallback, non-TOTP scheme rejection, invalid-Base32
  rejection, IsValidBase32 alphabet checks, code-generation shape & numeric,
  same-period idempotency, RemainingSeconds bounds, end-to-end URI→code round
  trip.

Verified manually (user T3.GATE smoke test):
- Real GitHub 2FA setup QR scanned from a PNG screenshot via "Scansiona QR".
- Live code rendered ("461 408") and countdown ring active.
- Code matches the corresponding code shown by an authenticator app on the
  user's phone, confirming RFC 6238 conformance against a known-good
  implementation.

Backward compatibility: vaults written by v1.x deserialise unchanged
(TotpSecret == null suppresses the TOTP section in the UI). Backups in the
v0x02 (Argon2id) format introduced in Cluster 2 carry the new fields end-to-end
without further migration.
@pexatar pexatar merged commit b55c8fa into main May 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant