feat(autofill): Ctrl+Shift+T types the 2FA code

Same workflow as the password-only hotkey: click the 2FA field, press the
combo, the current 6-digit code is typed in.

Reuses the whole autofill pipeline — no new native typing path. The code is
sent in the password slot with an empty username, so Delphi's existing
password-only branch types just the code.

- Bridge: akTotp kind, hotkey id 47, registered/torn down alongside the other
  two autofill combos; an unconfigured combo (vk=0) doesn't count as a
  registration failure.
- Matching filters on totp_secret BEFORE scoring, so the multi-match picker
  never offers an entry that has no code.
- Configurable + synced like the others (autofillHotkeyTotp), collision-checked
  against the other four slots, covered by Reset to defaults and the cheatsheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-25 16:53:00 +01:00
parent 2d6ba98073
commit 7c86f35fce
7 changed files with 121 additions and 18 deletions
+36 -8
View File
@@ -62,7 +62,10 @@ type
// Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the
// "password only" hotkey (Ctrl+Shift+P). The host decides what to type
// based on this kind.
TAutofillKind = (akFull, akPasswordOnly);
// akTotp reuses the password-only typing path: JS sends the generated
// 6-digit code in the password slot with an empty username, so Delphi
// types just the code into the focused 2FA field.
TAutofillKind = (akFull, akPasswordOnly, akTotp);
// Fired when an autofill hotkey is pressed. Args are the foreground
// window HWND and its title (captured before any focus change), plus
@@ -117,6 +120,7 @@ type
FAutofillRegistered: Boolean;
FAutofillFullActive: Boolean;
FAutofillPwdActive: Boolean;
FAutofillTotpActive: Boolean;
FOnAutofillRequest: TAutofillRequestEvent;
FDebugHotkeyRegistered: Boolean;
FOnDebugHotkey: TProc;
@@ -162,8 +166,10 @@ type
// Returns True if both hotkeys registered successfully. If one or both
// failed (clash with another app), best-effort: whichever succeeded
// stays active.
// ATotpVk = 0 means "no 2FA hotkey configured" — it is then skipped and
// does NOT count against the returned all-registered flag.
function SetAutofillHotkeys(AFullMods, AFullVk,
APwdMods, APwdVk: Word): Boolean;
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
// user-chosen combo. True on success.
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
@@ -287,6 +293,7 @@ const
DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel
NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title
QUICK_SEARCH_HOTKEY_ID = 46; // Ctrl+Shift+Q → quick-search-and-fill
AUTOFILL_HOTKEY_ID_TOTP = 47; // Ctrl+Shift+T → type the 2FA code
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT
@@ -895,13 +902,16 @@ begin
else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and
((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY) or
(AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP)) then
begin
// Capture the foreground window BEFORE any focus change, then fire the
// callback so the host can match the title against vault entries.
var LKind: TAutofillKind;
if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then
LKind := akPasswordOnly
else if AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP then
LKind := akTotp
else
LKind := akFull;
var LTarget := GetForegroundWindow;
@@ -920,7 +930,7 @@ end;
// =============================================================================
function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk,
APwdMods, APwdVk: Word): Boolean;
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
begin
// Tear down whatever is currently registered before installing the new
// combos. RegisterHotKey would fail if the same ID is already taken.
@@ -934,6 +944,11 @@ begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
FAutofillPwdActive := False;
end;
if FAutofillTotpActive then
begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
FAutofillTotpActive := False;
end;
// Best-effort registration. A failure (typically MOD_x clash with another
// app's global hotkey) is silent: the other slot can still be live.
@@ -943,9 +958,15 @@ begin
if (APwdVk <> 0) and (APwdMods <> 0) then
FAutofillPwdActive := RegisterHotKey(FMsgWindow,
AUTOFILL_HOTKEY_ID_PWDONLY, APwdMods, APwdVk);
if (ATotpVk <> 0) and (ATotpMods <> 0) then
FAutofillTotpActive := RegisterHotKey(FMsgWindow,
AUTOFILL_HOTKEY_ID_TOTP, ATotpMods, ATotpVk);
FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive;
Result := FAutofillFullActive and FAutofillPwdActive;
FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive
or FAutofillTotpActive;
// An unconfigured 2FA combo (vk=0) must not report a registration failure.
Result := FAutofillFullActive and FAutofillPwdActive
and (FAutofillTotpActive or (ATotpVk = 0));
end;
function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
@@ -1041,10 +1062,12 @@ end;
procedure TPMBridge.RegisterAutofillHotkey;
begin
// Convenience default — Ctrl+Shift+L (full) + Ctrl+Shift+P (password).
// Convenience defaults — Ctrl+Shift+L (full), Ctrl+Shift+P (password),
// Ctrl+Shift+T (2FA code).
// Idempotent: calling twice with the same combos is harmless.
SetAutofillHotkeys(AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('L'),
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P'));
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P'),
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('T'));
end;
procedure TPMBridge.UnregisterAutofillHotkey;
@@ -1059,6 +1082,11 @@ begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
FAutofillPwdActive := False;
end;
if FAutofillTotpActive then
begin
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
FAutofillTotpActive := False;
end;
FAutofillRegistered := False;
end;