1# PCH-02-010 — Clipboard auto-clear verifies the parked hash before clearing23**Severity:** Informational4**Target files:** `src/background/clipboard.ts:124-178` (`clearClipboardNow`),5`src/background/offscreen.ts`, `manifest.json` (⚠ permission), comment in6`clipboard.ts:18-19`7**Report:** peach-code-review.md, Finding PCH-02-01089## ⚠ Integration gate: this fix requires the `clipboardRead` permission1011Verified constraints of the only available read surfaces:1213- `navigator.clipboard.readText()` in an offscreen document rejects with14 *"Document is not focused"* — offscreen documents are never focused. Not15 reliable.16- The workable offscreen read path is `document.execCommand("paste")` into a17 textarea (the documented offscreen-clipboard technique), which **requires18 adding `"clipboardRead"` to `manifest.json` permissions** (the manifest19 currently grants `clipboardWrite` only).2021`clipboardRead` surfaces in the Chrome Web Store as **"Read data you copy and22paste"** — a visible permission expansion for a password manager, and a23store re-review trigger. The current unconditional clear is conservative and24costs only a minor UX surprise (the review's own framing). **Decision for the25integrator:**2627- **Option A (no new permission):** land only the comment fix below — the28 parked hash is *not* dead bookkeeping (PCH-01-021 made it the clear-owed29 flag); state explicitly that match-before-clear was evaluated and rejected30 because the required `clipboardRead` grant is a worse privacy posture than31 the UX cost it saves. This satisfies the brief's fallback ("document this32 and clear unconditionally … acceptable behavior").33- **Option B (full fix, below):** hash-match before clearing, with the34 permission addition flagged in the release notes. The hash comparison is35 designed so plaintext never leaves the offscreen document.3637Recommendation: **Option A** for v4.0.1; hold Option B in this draft until a38release where the permission expansion is worth bundling. Both are complete39below — whichever lands, the parked-hash comment stops reading as dead code.4041---4243## Option A — comment-only edit (clipboard.ts:18-19)4445```typescript464748495051525354555657585960616263```6465Option A save message:66`docs: PCH-02-010 document unconditional clipboard clear as deliberate — hash-match requires clipboardRead permission, implementation parked`6768---6970## Option B — full hash-match implementation7172### B1. manifest.json — permission7374```json7576 "clipboardRead",77```7879(`manifest.firefox.json` is NOT changed — the Firefox path has no offscreen80surface and keeps the unconditional clear; see B3.)8182### B2. offscreen.ts — hash the live clipboard locally, return only the hash8384Append a second listener (same file, after the CLEAR_CLIPBOARD listener).85The plaintext never crosses the message channel — the offscreen document86hashes in place and replies with hex SHA-256 only:8788```typescript899091929394chrome.runtime.onMessage.addListener(95 (msg: unknown, _sender, sendResponse: (r: unknown) => void) => {96 const type = (msg as { type?: string } | null)?.type;97 if (type !== "GET_CLIPBOARD_HASH") return undefined;98 void (async () => {99 try {100 let text: string;101 try {102 text = await navigator.clipboard.readText();103 } catch {104 105 106 107 const ta = document.createElement("textarea");108 document.body.appendChild(ta);109 ta.focus();110 const ok = document.execCommand("paste");111 text = ta.value;112 ta.remove();113 if (!ok) throw new Error("paste_failed");114 }115 const digest = await crypto.subtle.digest(116 "SHA-256",117 new TextEncoder().encode(text),118 );119 const hash = Array.from(new Uint8Array(digest))120 .map((b) => b.toString(16).padStart(2, "0"))121 .join("");122 sendResponse({ ok: true, hash });123 } catch {124 sendResponse({ ok: false });125 }126 })();127 return true; 128 },129);130```131132### B3. clipboard.ts — compare before clearing in `clearClipboardNow`133134Replace the Chrome/offscreen branch (clipboard.ts:124-155). The offscreen135document is created once and reused for both the hash check and the clear:136137```typescript138export async function clearClipboardNow(): Promise<void> {139 let cleared = false;140 let skipClear = false;141142 143 if (chrome.offscreen?.createDocument) {144 try {145 await chrome.offscreen.createDocument({146 147 148 149 url: "src/background/offscreen.html",150 reasons: [chrome.offscreen.Reason.CLIPBOARD],151 justification: "Clear sensitive clipboard content after timeout",152 });153 } catch {154 155 }156157 158 159 160 161 162 163 try {164 const stored = await chrome.storage.session.get(CLEAR_HASH_KEY);165 const parked = stored?.[CLEAR_HASH_KEY];166 if (typeof parked === "string") {167 const res = (await chrome.runtime.sendMessage({168 type: "GET_CLIPBOARD_HASH",169 })) as { ok?: boolean; hash?: string } | undefined;170 if (res?.ok === true && typeof res.hash === "string" && res.hash !== parked) {171 skipClear = true;172 }173 }174 } catch {175 176 }177178 if (!skipClear) {179 try {180 const res = (await chrome.runtime.sendMessage({181 type: "CLEAR_CLIPBOARD",182 })) as { ok?: boolean } | undefined;183 cleared = res?.ok === true;184 } catch {185 cleared = false;186 }187 }188 189 try {190 await chrome.offscreen.closeDocument();191 } catch {192 193 }194 }195196 if (skipClear) {197 198 await chrome.storage.session.remove(CLEAR_HASH_KEY).catch(() => undefined);199 await chrome.alarms.clear(CLIPBOARD_CLEAR_ALARM).catch(() => false);200 return;201 }202203 204 205 if (!cleared) {206 try {207 await writeToClipboard("");208 cleared = true;209 } catch {210 cleared = false;211 }212 }213214 if (cleared) {215 await chrome.storage.session.remove(CLEAR_HASH_KEY).catch(() => undefined);216 await chrome.alarms.clear(CLIPBOARD_CLEAR_ALARM).catch(() => false);217 } else {218 219 220 221 await chrome.alarms.create(CLIPBOARD_CLEAR_ALARM, {222 delayInMinutes: MIN_ALARM_MINUTES,223 });224 }225}226```227228Behavioural notes (all verified against the current file):229- The fast-path `setTimeout` and the alarm backstop both call230 `clearClipboardNow`, so both honour the match check — no second wiring231 point.232- If the parked hash is missing (bookkeeping write failed on copy), the check233 is skipped and the clear proceeds — identical to today.234- Deviation from the brief's sketch: the brief hashes with BLAKE3; the parked235 hash is and stays SHA-256 (`hashForVerification`, clipboard.ts:61-69) — the236 comparison must use the hash that is actually parked.237- Deviation from the brief's sketch: the brief returns `true` ("clear") when238 the parked hash is absent; the existing PCH-01-021 alarm handler already239 treats absent-hash as "nothing owed" and skips. This patch preserves that:240 absence short-circuits in the ALARM handler (unchanged), and in241 `clearClipboardNow` absence just means "no comparison possible → clear",242 which only the fast-path timer reaches.243244### B4. Tests (Option B only)245246`clipboard.hash-match.test.ts` (in this folder) — 4 tests: matching hash →247CLEAR_CLIPBOARD sent + bookkeeping torn down; mismatching hash → clear248skipped + bookkeeping torn down; hash-read failure → cleared anyway249(conservative); Firefox path (no chrome.offscreen) → unconditional clear250unchanged. Destination: `src/background/clipboard.hash-match.test.ts`.251252Option B save message:253`fix: PCH-02-010 clipboard auto-clear verifies parked-hash match via offscreen SHA-256 before clearing — adds clipboardRead permission (Chrome only)`