打造 Demo-safe:讓直播展示時 API Key 徹底隱形的開源工具
開發者在做產品展示、錄製教學或參加技術會議時,經常需要操作 API Key——產生金鑰、貼入設定檔、部署服務。但螢幕上閃過的明文 Key,可能就這樣被幾千位觀眾看見。
手動模糊影片?太慢。用環境變數?破壞展示流程。祈禱沒人注意到?太天真。
所以我做了 Demo-safe API Key Manager——一款 macOS 系統級工具,讓 API Key 從進入工作流程的那一刻起就徹底隱形。螢幕永遠只顯示 sk-proj-****...****,但剪貼簿裡是完整的金鑰。
三層遮蔽架構
[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/SafeApiKeyManager
目前已完成核心功能(Swift Core + VS Code + Chrome Extension),還有幾個有趣的功能在 roadmap 上:
- Floating Toolbox HUD:按住快捷鍵呼出搜尋框,放開即貼上
- Terminal Masking:用 node-pty 代理終端,在輸出到達螢幕前就替換敏感資訊
- System-wide Masking:透過 Accessibility API 實現全系統遮蔽
- Import/Export:Vault 匯入匯出,方便團隊共享設定
如果你也有「展示時不小心曝光 Key」的困擾,歡迎試用和貢獻。