feat: PIN unlock + table column picker + edit-position chooser + UX

- PIN unlock: device-local 4-12 digit shortcut, DPAPI-wrapped vault
  key. Three modes (state.unlockMode): pw / pin / pw+pin. PIN
  derives a wrap key via PBKDF2(pin, salt, 100k) and unwraps the
  stored vault key (mirrors the Quick Unlock blob shape).
  Anti-brute-force: 5 wrong attempts wipes the blob. Setup gated by
  master-pw reauth so an unattended unlocked laptop can't be
  backdoored. Master pw rotation clears the PIN blob (key drift).
  loadServerSettings post-sync demotes pin/both -> pw when the local
  blob is missing, so a wiped device re-syncs the correct mode up.
  New unit PM.PinUnlock.pas + cmd://pin/{store,get,clear,status}.
- Table column picker: ⚙ in topbar (table view only), checkbox menu
  for Site/Username/Folder/Updated. Site also drives showSiteOnCards
  so the existing "Show site / URL" toggle in Settings stays in
  sync. NAME column auto-widths (180px min, content max, +32px
  right padding) so column hugs the next one without truncating.
- Editor position chooser (Appearance setting): Slide-over right /
  left / Centered modal. Scoped to #slideover + #settingsPanel so
  the click-outside / pointer-events logic doesn't accidentally
  trap the modal-style empty viewport.
- Confirm before discarding unsaved edits: state.confirmOnUnsaved
  setting (default ON), prompts on X / Esc / click-outside / switch-
  to-other-entry. Also gates Lock vault / Sign out actions when the
  editor is dirty; auto-lock and system-lock paths bypass to avoid
  blocking on an unattended machine.
- Open-in-browser button added to the actions cell of the table
  view (was card-only).
- Entry templates pass folder customization + template id through
  duplicate / export / import / auto-backup roundtrips.
- Folder color + icon now persisted across export/import: payload.
  folders carries name/color/icon; import creates missing folders
  additively (existing local customisation kept).
- Bulk move-to-folder, batch add-tag, single add-tag now re-ship
  the full entry payload so partial PUTs don't silently wipe
  TOTP / custom_fields / kind / template.
- FireDAC: switched ftString -> ftMemo for icon_b64 / custom_fields
  / TOTP / template params and replaced .AsString with .Value so a
  large (~200 KB) DeepSeek favicon no longer gets truncated at the
  default ANSI 4000-char cap.
- Unicode filenames: attachment INSERT now uses ftWideString +
  .AsWideString so non-ANSI filenames round-trip instead of being
  mangled to "?".
- HandleSetEntryIcon cap raised 256 KB -> 512 KB chars to accept
  base64 data URIs produced by max-raw favicon fetches.
- promptDialog + askReauth support inline `error` line + retry-
  with-count loops on doExport reauth and auto-backup password
  setup (5 attempts cap before bailing).
- Recently used moved from Tools to Vault section in the sidebar.
- Auth screen passkey button hidden (Delphi backend stubs WebAuthn).
- Sensitive cmd://favicon/refresh-style buttons in Settings now
  stopPropagation so the document-level "close panel" handler
  doesn't dismiss Settings mid-async during DOM reparenting.
- TEST_PLAN.md: +PIN unlock section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-06-29 04:41:39 +01:00
parent 1f56a03492
commit b00da43ab0
11 changed files with 1417 additions and 71 deletions
+46 -1
View File
@@ -32,7 +32,7 @@ uses
{$IFDEF USE_EDGE_BROWSER}
FMX.TMSFNCEdgeWebBrowser,
{$ENDIF}
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart,
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.PinUnlock, PM.UserPrefs, PM.AutoStart,
PM.Favicon,
FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check
const
@@ -635,6 +635,51 @@ begin
BoolToStr(PM.QuickUnlock.HasQuickUnlock, True).ToLower + ')');
end
// ---- PIN unlock (DPAPI blob, PIN-derived wrap of the vault key) ----
// Same wire model as quickunlock — opaque base64 payload in / out.
else if ACmd = 'pin/store' then
begin
LText := GetParam('data');
if LText = '' then Exit;
var LBytes := TNetEncoding.Base64.DecodeStringToBytes(LText);
if PM.PinUnlock.StorePinUnlock(LBytes) then
LogLine(Format('PIN blob stored (%d bytes)', [Length(LBytes)]))
else
LogLine('PIN blob store FAILED (DPAPI error)');
end
else if ACmd = 'pin/get' then
begin
var LBytes: TBytes;
if PM.PinUnlock.LoadPinUnlock(LBytes) and (Length(LBytes) > 0) then
begin
var LB64 := TNetEncoding.Base64.EncodeBytesToString(LBytes);
LB64 := StringReplace(LB64, #13, '', [rfReplaceAll]);
LB64 := StringReplace(LB64, #10, '', [rfReplaceAll]);
WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&Bridge.onPinResult)' +
'Bridge.onPinResult("' + LB64 + '")');
LogLine('PIN blob served');
end
else
WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&Bridge.onPinResult)Bridge.onPinResult(null)');
end
else if ACmd = 'pin/clear' then
begin
PM.PinUnlock.ClearPinUnlock;
LogLine('PIN blob cleared');
end
else if ACmd = 'pin/status' then
begin
WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&Bridge.onPinStatus)' +
'Bridge.onPinStatus(' +
BoolToStr(PM.PinUnlock.HasPinUnlock, True).ToLower + ')');
end
// ---- Autofill --------------------------------------------------------
// configure: JS calls this on page load / settings change to sync the
// hotkey registration state with the user's localStorage preference.