fix(clipboard): auto-clear must not wipe content copied since our SetText

Copy a password (30s auto-clear armed), then copy something else from
another app before the timer fires: the tick emptied the clipboard anyway,
destroying the user's newer content. Guard with the Win32 clipboard sequence
number: SetText snapshots GetClipboardSequenceNumber, ClearIfOurs only
empties when it hasn't moved. Applied to the auto-clear timer AND the
clear-on-minimize path (same bug class); the explicit JS clipboard/clear
command stays unconditional (user-initiated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-12 11:20:49 +01:00
parent 72dcc3dd82
commit 3822c21194
+21 -3
View File
@@ -41,6 +41,9 @@ type
TSecureClipboard = class
private
FClearTimer: TTimer;
// Clipboard sequence number right after OUR SetText. If it moved, the
// user copied something else since — that content is theirs, not ours.
FSetSequence: DWORD;
procedure ClearTimerTick(Sender: TObject);
public
constructor Create;
@@ -50,6 +53,10 @@ type
// AClearAfterMs = 0 disables auto-clear; default is 30 seconds.
procedure SetText(const AText: string; AClearAfterMs: Integer = 30000);
procedure Clear;
// Clear only if the clipboard still holds what WE put there (sequence
// unchanged). Auto-clear timer + minimize-to-tray use this so they never
// wipe something the user copied from another app in the meantime.
procedure ClearIfOurs;
end;
// Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the
@@ -341,7 +348,7 @@ end;
procedure TSecureClipboard.ClearTimerTick(Sender: TObject);
begin
FClearTimer.Enabled := False;
Clear;
ClearIfOurs;
end;
procedure TSecureClipboard.SetText(const AText: string; AClearAfterMs: Integer);
@@ -385,6 +392,10 @@ begin
CloseClipboard;
end;
// Snapshot the sequence AFTER our write — any later copy (ours or another
// app's) bumps it, which is exactly the "is it still ours?" signal.
FSetSequence := GetClipboardSequenceNumber;
if AClearAfterMs > 0 then
begin
FClearTimer.Interval := AClearAfterMs;
@@ -402,6 +413,12 @@ begin
end;
end;
procedure TSecureClipboard.ClearIfOurs;
begin
if GetClipboardSequenceNumber = FSetSequence then
Clear;
end;
function TSecureClipboard.ReadText: string;
var
H: THandle;
@@ -581,9 +598,10 @@ begin
// out of sight. SKIPPED when AClearClipboard=False — the quick-search
// copy-then-hide flow deliberately keeps the password on the clipboard
// (the 30s auto-clear timer still guards it) so the user can paste it
// into their target app after we minimise.
// into their target app after we minimise. ClearIfOurs: never wipe
// content the user copied from another app since.
if AClearClipboard then
FSecureClipboard.Clear;
FSecureClipboard.ClearIfOurs;
LFormHwnd := MainFormHWND(FMainForm);
LAppHwnd := FindFMXAppWindow;