實作心得

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

easyvibecoding··22 次閱讀

開發者在做產品展示、錄製教學或參加技術會議時,經常需要操作 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.sendActionSettingsLink@Environment(\.openSettings)——最後發現只有用原生選單樣式(Toggle、Button 作為直接子元素)才能穩定運作。

Settings 視窗更麻煩:menu-bar-only app 沒有主視窗,openSettings() 叫了沒反應。最終用 SettingsWindowController 手動管理 NSWindow,搭配 NSApp.setActivationPolicy(.regular) + orderFrontRegardless() 才讓視窗正常從背景跳出來。

WebSocket 握手後立刻斷線的迴圈

VS Code Extension 連上 Swift Core 的 WebSocket 後,會進入「連線 → 握手 → 立刻斷開 → 重連」的無限迴圈。

原因是 NWProtocolWebSocketreceiveMessage 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。用 TreeWalkerSHOW_TEXT filter)遍歷文字節點,找到匹配的 Key 後替換為帶有 demosafe-mask class 的 <span> 元素。關鍵是會保存原始文字,退出 Demo Mode 時能完整還原,不會破壞頁面內容。

安全設計原則

這個工具的安全紅線很明確:

  • 明文只走一條路:Keychain → ClipboardEngine → NSPasteboard,貼上後立即對明文 Data 執行 resetBytes(zero-fill),不留記憶體殘留
  • IPC 永不傳明文:WebSocket 上只傳 regex pattern、遮蔽後的預覽文字和 Key ID,Extension 從不接觸真正的金鑰值
  • WebSocket 只綁 localhostNWEndpoint.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」的困擾,歡迎試用和貢獻。