From 9363629ea2d5736e174038bf259995ebb0ca6a82 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 19 Sep 2026 23:06:02 -0400 Subject: [PATCH] feat(security): recovery key leaves the clipboard after 60 s, visibly (#156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useSensitiveCopy: the recovery key's Copy button becomes 'Copied · clears in 60 s' and counts down; at zero the clipboard is cleared only if it still holds the key (readText() where permitted — if the browser refuses to read, nothing is wiped rather than risk eating something else). Any other copy made in the app cancels the timer. No setting. Verified headless with a fake clock: countdown ticks, clipboard emptied at 0; copying something else mid-countdown cancels and leaves that content untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .../components/DeviceVerificationSetup.tsx | 13 ++-- src/app/hooks/useSensitiveCopy.ts | 70 +++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 src/app/hooks/useSensitiveCopy.ts diff --git a/src/app/components/DeviceVerificationSetup.tsx b/src/app/components/DeviceVerificationSetup.tsx index 0d02cc347..b0308b866 100644 --- a/src/app/components/DeviceVerificationSetup.tsx +++ b/src/app/components/DeviceVerificationSetup.tsx @@ -19,7 +19,7 @@ import { useSaveFile } from '../hooks/useSaveFile'; import { useModalStyle } from '../hooks/useModalStyle'; import { PasswordInput } from './password-input'; import { ContainerColor } from '../styles/ContainerColor.css'; -import { copyToClipboard } from '../utils/dom'; +import { useSensitiveCopy } from '../hooks/useSensitiveCopy'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { clearSecretStorageKeys } from '../../client/secretStorageKeys'; import { ActionUIA, ActionUIAFlowsLoader } from './ActionUIA'; @@ -232,9 +232,8 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) { const [show, setShow] = useState(false); const saveFile = useSaveFile(); - const handleCopy = () => { - copyToClipboard(recoveryKey); - }; + // [Gitea #156] The key leaves the clipboard again after 60 s, visibly. + const { copy: handleCopy, secondsLeft } = useSensitiveCopy(recoveryKey); const handleDownload = () => { const blob = new Blob([recoveryKey], { @@ -272,8 +271,10 @@ function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) { -