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:
@@ -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;
|
||||
|
||||
|
||||
@@ -895,10 +895,14 @@ begin
|
||||
var LFullVk := Word(StrToIntDef(GetParam('full_vk'), Ord('L')));
|
||||
var LPwdMods := Word(StrToIntDef(GetParam('pwd_mods'), 6));
|
||||
var LPwdVk := Word(StrToIntDef(GetParam('pwd_vk'), Ord('P')));
|
||||
// 2FA combo — vk 0 when the user cleared it, which skips registration.
|
||||
var LTotpMods := Word(StrToIntDef(GetParam('totp_mods'), 6));
|
||||
var LTotpVk := Word(StrToIntDef(GetParam('totp_vk'), Ord('T')));
|
||||
var LAllOk := FBridge.SetAutofillHotkeys(LFullMods, LFullVk,
|
||||
LPwdMods, LPwdVk);
|
||||
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d (all_ok=%s)',
|
||||
[LFullMods, LFullVk, LPwdMods, LPwdVk, BoolToStr(LAllOk, True)]));
|
||||
LPwdMods, LPwdVk,
|
||||
LTotpMods, LTotpVk);
|
||||
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d totp=mods:%d vk:%d (all_ok=%s)',
|
||||
[LFullMods, LFullVk, LPwdMods, LPwdVk, LTotpMods, LTotpVk, BoolToStr(LAllOk, True)]));
|
||||
// Notify JS of the result so the UI can flag a failed-to-register combo
|
||||
// (typically a clash with another app's global hotkey).
|
||||
WebBrowser.ExecuteJavaScript(
|
||||
@@ -1542,7 +1546,11 @@ begin
|
||||
LTitle := LTitle.Replace('\', '\\');
|
||||
LTitle := LTitle.Replace('"', '\"');
|
||||
|
||||
if AKind = akPasswordOnly then LKind := 'password' else LKind := 'full';
|
||||
case AKind of
|
||||
akPasswordOnly: LKind := 'password';
|
||||
akTotp: LKind := 'totp';
|
||||
else LKind := 'full';
|
||||
end;
|
||||
|
||||
WebBrowser.ExecuteJavaScript(
|
||||
'if(window.Bridge&&typeof Bridge.onAutofillRequest==="function")' +
|
||||
|
||||
Reference in New Issue
Block a user