# 打造 Demo-safe：讓直播展示時 API Key 徹底隱形的開源工具

> 作者：easyvibecoding · 發佈：2026-03-14

開發者在做產品展示、錄製教學或參加技術會議時，經常需要操作 API Key——產生金鑰、貼入設定檔、部署服務。但螢幕上閃過的明文 Key，可能就這樣被幾千位觀眾看見。

手動模糊影片？太慢。用環境變數？破壞展示流程。祈禱沒人注意到？太天真。

所以我做了 **Demo-safe API Key Manager**——一款 macOS 系統級工具，讓 API Key 從進入工作流程的那一刻起就徹底隱形。螢幕永遠只顯示 `sk-proj-****...****`，但剪貼簿裡是完整的金鑰。

## 三層遮蔽架構

```text
[VS Code Extension] <-> [Core Engine (Swift)] <-> [Chrome Extension]
                              |
                      [macOS Keychain]
```

整個系統分三層，各司其職：

**Swift Core Engine（Menu Bar App）** 是大腦。Key 儲存在 macOS Keychain（`kSecAttrAccessibleWhenUnlockedThisDeviceOnly` 保護等級），透過 localhost WebSocket 和各 Extension 即時同步遮蔽狀態。切換 Demo Mode、管理金鑰庫、設定情境模式，都在 Menu Bar 完成。整個 Swift Core **零外部依賴**——Package.swift 沒有任何 third-party package，完全用 Apple 原生框架實現。

**VS Code Extension** 負責編輯器內的遮蔽。打開含有 API Key 的檔案時，Decoration API 會即時將匹配的 Key 替換為遮蔽文字。原始文字依然存在——只是你在螢幕上看不到。Status Bar 即時顯示連線狀態、當前模式和遮蔽數量，一目了然。

**Chrome Extension** 則守住瀏覽器。Content Script 自動注入 10 個主流 API 管理平台：

- OpenAI Console、Anthropic Dashboard
- AWS Console、Google Cloud Console、Azure Portal
- Stripe Dashboard
- GitHub Token Settings、Hugging Face Token Settings
- SendGrid、Slack API

透過 `MutationObserver` 搭配 200ms debounce 監聽 DOM 變動，即使是 SPA 動態載入的內容也不會漏掉。

## 16 種內建辨識模式

系統內建了 16 種 API Key pattern，依前綴自動辨識：

| 服務 | 前綴 | 說明 |
|------|------|------|
| OpenAI | `sk-proj-` | Project API Key |
| Anthropic | `sk-ant-` | Claude API Key |
| AWS Access Key | `AKIA` | IAM Access Key ID |
| AWS Session Token | `ASIA` | 臨時安全憑證 |
| Stripe | `sk_live_` / `sk_test_` | Live / Test Secret Key |
| Google Cloud | `AIza` | API Key |
| GitHub | `ghp_` / `github_pat_` | PAT / Fine-grained PAT |
| GitLab | `glpat-` | Personal Access Token |
| Slack | `xoxb-` / `xoxp-` | Bot / User Token |
| SendGrid | `SG.` | API Key |
| Hugging Face | `hf_` | Access Token |

每個 pattern 都有對應的遮蔽格式（prefix + `****...****` + suffix），長度自動對齊原始 Key，讓排版完全不會跑掉。也可以自訂新的辨識模式。

## 離線遮蔽：斷線也不怕

一般的即時同步架構，伺服器斷線就失效。但 Demo-safe 的 VS Code Extension 有 **Pattern Cache** 機制——所有辨識模式會持久化到 VS Code 的 `globalState`，即使 Swift Core 當掉或電腦休眠喚醒後連線中斷，已快取的 pattern 依然有效，編輯器內的遮蔽不會消失。

Status Bar 會即時反映狀態：正常連線顯示「Demo-safe」，斷線顯示「(Offline)」，沒有快取則顯示「(No Cache)」提醒你需要重新連線。

## 開發過程的幾個關鍵挑戰

### MenuBarExtra 的按鈕點擊問題

macOS 的 `MenuBarExtra` 看起來很方便，但自訂 VStack 布局裡的按鈕點擊區域會莫名失效。試了好幾種方案——`NSApp.sendAction`、`SettingsLink`、`@Environment(\.openSettings)`——最後發現只有用**原生選單樣式**（Toggle、Button 作為直接子元素）才能穩定運作。

Settings 視窗更麻煩：menu-bar-only app 沒有主視窗，`openSettings()` 叫了沒反應。最終用 `SettingsWindowController` 手動管理 NSWindow，搭配 `NSApp.setActivationPolicy(.regular)` + `orderFrontRegardless()` 才讓視窗正常從背景跳出來。

### WebSocket 握手後立刻斷線的迴圈

VS Code Extension 連上 Swift Core 的 WebSocket 後，會進入「連線 → 握手 → 立刻斷開 → 重連」的無限迴圈。

原因是 `NWProtocolWebSocket` 的 `receiveMessage` callback 裡，`isComplete` 對 WebSocket 來說是**每條訊息完成**，不是連線結束。把「isComplete 就關閉連線」的邏輯改為「只在錯誤或收到 .close opcode 時關閉」，問題就解決了。

### Editor Decoration 文字擠壓

遮蔽文字 `sk-****...****` 只有 14 個字元，但原始 Key 有 49 個字元。直接覆蓋上去，後面的程式碼全部擠在一起。

解法是雙管齊下：原始文字用 `opacity: '0'` + `letterSpacing: '-1em'` 讓它視覺寬度歸零，然後遮蔽文字用 CSS `after` pseudo-element 顯示，並填充 `*` 到與原始 Key 等長。Hover 時還會顯示鎖頭圖示和所屬 Service 名稱。Overview Ruler 右側也會標記遮蔽位置，方便在長檔案中快速定位。

### Chrome Extension 的 Content Script 狀態同步

Content Script 注入頁面時，不知道現在是不是 Demo Mode——它只會在收到 `state_changed` 事件時更新。如果使用者先開了 Demo Mode 再開網頁，Key 就不會被遮蔽。

修復很簡單：Content Script 載入時主動向 Background Service Worker 發 `get_state` 請求，取得當前狀態後立刻掃描。

### Chrome 的 DOM 遮蔽與還原

Chrome Extension 的遮蔽方式和 VS Code 不同——它直接操作 DOM。用 `TreeWalker`（`SHOW_TEXT` filter）遍歷文字節點，找到匹配的 Key 後替換為帶有 `demosafe-mask` class 的 `<span>` 元素。關鍵是會保存原始文字，退出 Demo Mode 時能完整還原，不會破壞頁面內容。

## 安全設計原則

這個工具的安全紅線很明確：

- **明文只走一條路**：Keychain → ClipboardEngine → NSPasteboard，貼上後立即對明文 Data 執行 `resetBytes`（zero-fill），不留記憶體殘留
- **IPC 永不傳明文**：WebSocket 上只傳 regex pattern、遮蔽後的預覽文字和 Key ID，Extension 從不接觸真正的金鑰值
- **WebSocket 只綁 localhost**：`NWEndpoint.hostPort(host: .ipv4(.loopback), port: 0)`，hard-coded 寫死，外部無法連入
- **Handshake Token**：每次 Core 重啟都用 `SecRandomCopyBytes`（32 bytes → 64-char hex）重新產生，舊 token 立即失效
- **ipc.json 權限 600**：只有當前使用者可讀，其他使用者無法取得連線資訊
- **Keychain 保護等級**：`kSecAttrAccessibleWhenUnlockedThisDeviceOnly`——裝置鎖定時無法存取，且不會同步到 iCloud
- **Vault 原子寫入**：每次更新 vault.json 前先備份到 vault.json.backup，防止寫入中斷導致資料損毀

## 情境模式

不同場景需要不同安全等級：

| 情境 | 遮蔽等級 | 剪貼簿自動清除 |
|------|---------|--------------|
| Livestream | 全遮蔽 | 30 秒 |
| Tutorial Recording | 全遮蔽 | 10 秒 |
| Internal Demo | 部分遮蔽 | 不清除 |
| Development | 不遮蔽 | 不清除 |

一鍵切換（快捷鍵 `Ctrl+Opt+Cmd+D`），所有已連線的 Extension 即時同步。每個情境還可以設定 `activeServiceIds` 白名單，只遮蔽特定服務的 Key。

## 全域快捷鍵

| 快捷鍵 | 功能 |
|-------|------|
| `Ctrl+Opt+Cmd+D` | 切換 Demo Mode |
| `Ctrl+Opt+Space` | 呼出 Key 選取器（QuickPick） |
| `Ctrl+Opt+[1-9]` | 依索引直接貼上 Key |
| `Ctrl+Opt+Cmd+V` | 擷取剪貼簿中的 Key |

快捷鍵透過 `CGEvent.tapCreate` 實現系統級監聽（需要 Accessibility 權限），在任何應用程式中都能使用。

## 技術選型

| 元件 | 技術 |
|------|------|
| Menu Bar App | Swift 5.9 + SwiftUI + AppKit（macOS 14+） |
| Key 儲存 | macOS Keychain (Security.framework) |
| IPC Server | Network.framework (NWListener + NWProtocolWebSocket) |
| VS Code Extension | TypeScript + Decoration API + ws + esbuild |
| Chrome Extension | Manifest V3 + Content Scripts + MutationObserver |
| 辨識模式 | 16 種內建 regex pattern + 自訂擴充 |

選擇 Network.framework 而非第三方 WebSocket 套件，是因為它原生支援 WebSocket protocol，不需要額外依賴，而且和 macOS 系統整合最好。整個 Swift Core 的 Package.swift 沒有任何外部依賴——這在 macOS 開發中很少見，但也代表維護成本極低。

## 開源

專案以 **Apache License 2.0** 開源，歡迎貢獻：

[GitHub: easyvibecoding/demosafe](https://github.com/easyvibecoding/demosafe)

目前已完成核心功能（Swift Core + VS Code + Chrome Extension），還有幾個有趣的功能在 roadmap 上：

- **Floating Toolbox HUD**：按住快捷鍵呼出搜尋框，放開即貼上
- **Terminal Masking**：用 node-pty 代理終端，在輸出到達螢幕前就替換敏感資訊
- **System-wide Masking**：透過 Accessibility API 實現全系統遮蔽
- **Import/Export**：Vault 匯入匯出，方便團隊共享設定

如果你也有「展示時不小心曝光 Key」的困擾，歡迎試用和貢獻。

