從醫療資訊到個人理財:我做 Kaizei 學到的零知識加密

我做完 ClinCalc / ExClinCalc 之後跨領域做了 Kaizei 個人理財桌面應用。這個跨領域不是換主題,是把醫療系統的安全思維帶過去 —— 並順便學到「零知識」是什麼意思。

#kaizei#zero-knowledge#encryption#electron#cross-domain#security

TL;DR

我從醫療資訊系統跨足做個人理財桌面應用 Kaizei。表面是換主題,實質是把同一套「資料隱私先行」的思維帶到不同領域。在這個過程中我具體實作了一套零知識加密 vault ── server 物理上無法解密使用者資料 ── 並理解這個架構的真實代價。


為什麼跨領域

很多人問我:「你已經有兩個醫療系統,為什麼還要去做個人理財?」

短答:因為我想驗證一個假設 ── 「資料隱私先行」是不是只在醫療場景有意義?

長答要回到我做 ExClinCalc 時的觀察。當我用 PostgreSQL RLS 把權限下放到資料庫層、寫了 那篇關於 RLS 的 blog,我意識到這個思維可以推廣:

不只醫療資料 ── 任何「server 不需要看到原始內容」的資料,都應該在 server 層級就保證它讀不到。

個人理財的 API key、交易紀錄、隱私備註 ── 都符合這個條件。


從 RLS 到零知識的差距

ExClinCalc 的 RLS 解的是「多角色之間互相隔離」 ── 醫師 A 看不到醫師 B 的病人。但 Supabase 自己的 service_role 仍能看所有資料。我相信 Supabase 不會亂看,但**「我必須相信 vendor」這個假設本身就是缺陷**。

零知識架構的目標是把這個假設拿掉:

RLS 模型(ExClinCalc):
  使用者間隔離 ✅
  Server / vendor 看得到全部 ⚠️

零知識模型(Kaizei):
  使用者間隔離 ✅
  Server 物理上無法解密 ✅

「物理上無法」這四個字是關鍵。不是 server 答應不看 ── 是 server 即使想看,沒有解密金鑰,也讀不出來。


怎麼實作

步驟 1:雙金鑰衍生

使用者輸入主密碼。透過 PBKDF2(310,000 iterations,OWASP 2023 建議下限)+ 兩組獨立 salt 衍生兩把金鑰:

const authKey = await PBKDF2(password, salt_auth, 310_000)
const encKey = await PBKDF2(password, salt_enc, 310_000)

兩把金鑰雖然來自同一密碼,但因為 salt 不同,互相無法推導

步驟 2:authKey 上 server,encKey 留本機

// 上 server
await fetch('/auth/login', {
  body: JSON.stringify({
    email,
    authKey,  // 給 server 驗身份用
  })
})

// encKey:永遠留在裝置 RAM 內,不寫盤、不送網路

Server 收到 authKey,再做 SHA-256(authKey + server pepper) 後存入 DB。即使 DB 整個被脫庫,攻擊者拿到的也只是 hash + pepper,要 brute force 主密碼幾乎不可能。

步驟 3:本機 AES-256-GCM 加密 vault

const ciphertext = await AES_GCM_encrypt(vaultData, encKey)
await fetch('/vault', {
  method: 'PUT',
  body: ciphertext  // 送出去的是密文
})

Server 收到的是已加密的 binary blob。Server 看到的是 random bytes。沒有 encKey,server 即使想解也解不開

步驟 4:取回時反過來

const ciphertext = await fetch('/vault').then(r => r.arrayBuffer())
const vaultData = await AES_GCM_decrypt(ciphertext, encKey)
// encKey 仍在本機 RAM 中

整個流程 server 只看過密文。


真實代價:「無後門」就是無後門

零知識架構聽起來很美,但代價是真實的:

代價 1:忘記主密碼 = 資料無法救回

我前述設計沒有「忘記密碼」恢復路徑。如果使用者忘記主密碼,Kaizei 的 server 也救不了他(server 沒有 decrypt key)。

對使用者教育要做足。Kaizei 在註冊時強制使用者:

  • 設定強密碼
  • 寫下備份(紙本 / 1Password)
  • 「你忘記密碼 = 資料無法救回」要連續確認 3 次

代價 2:每次解鎖延遲 200–400ms

PBKDF2 310,000 iterations 在桌面端跑大約 200–400ms。這是設計如此 ── 越慢的 KDF 越難 brute force。

對使用者體驗的影響:開啟 app 後等 0.3 秒才能用。可接受,但要 UX 上表現「正在解鎖」而不是裝呆。

代價 3:跨機器同步衝突解決受限

Server 看不懂內容 → 看不懂哪個欄位被誰改了 → 沒辦法做欄位級 merge。只能整包覆蓋(last-write-wins)

對使用者:A 機器加了一筆交易、B 機器加了另一筆,如果不及時 sync,可能會丟其中一筆。

我的對策:

  • 每次寫入前先 fetch server 版本,做本機 merge 後再寫回
  • 寫入時帶 optimistic lock(version number),衝突時 fail back
  • 顯眼提示「有版本衝突,請手動處理」

代價 4:無法分析使用行為

Server 看不懂內容 → 沒辦法做「最常用功能 / 用戶旅程」之類分析。

對 Kaizei 我認為這代價值得付。對其他應用未必:消費級社交 / 內容平台靠分析做產品決策,零知識會讓它們失能。


跟「終端對終端加密」的差別

很多人會問:「這跟 Signal / WhatsApp 的終端對終端加密(E2EE)有什麼不一樣?」

兩者是相關但不同的概念:

概念解決什麼
零知識(Zero Knowledge)Server 不能讀內容(單一使用者場景)
終端對終端加密(E2EE)任何中間節點不能讀內容(多使用者通訊場景)

Kaizei 是單一使用者多裝置同步,所以零知識是對的模型。Signal 是兩個使用者通訊,需要 E2EE + Forward Secrecy + Diffie-Hellman 等更複雜協定。

我做的不是 E2EE。這個誠實要說清楚


跨領域帶來的副效果

做完 Kaizei 後回頭看醫療作品,有幾個有意思的影響:

影響 1:對「server 知道太多」更敏感

Kaizei 之後我重新檢查 ClinCalc 的隱私架構。發現:

  • ClinCalc 把使用者體檢數值傳給 Cloudflare Worker、Worker 再呼叫 Gemini API
  • 雖然「規則先跑、LLM 只看結構化結果」已經降低風險
  • 但 ClinCalc 的 Worker server 仍看到原始數值

有沒有可能 ClinCalc 也走零知識路線? ── 體檢數值在使用者瀏覽器加密後再上傳?技術上可行(Web Crypto),但會犧牲影像 OCR(影像必須交給 Gemini 解碼)。不是純好的折衷

這個議題沒有完美解,但 Kaizei 讓我意識到:我把「server 必看到原始」當成預設了,而這個預設本身值得質疑

影響 2:理解「規範強制」與「技術強制」的差別

醫療場域有 HIPAA、GDPR、台灣個資法強制資料保護 ── 但這是規範,不是技術。違反了會被罰,但 server 仍然技術上看得到。

零知識是技術強制。即使 server 想違反,也物理上做不到。這兩個層級的保護是疊加的,不是互相取代

我做完 Kaizei 後在讀 ExClinCalc 的稽核機制:稽核能告訴你「誰看了什麼」,但無法防止「能看的人看了不該看的」。真正的隱私保護要把「能看到」這件事本身拿掉


開放問題

問題 1:醫療系統能走零知識嗎?

部分可以。例如儲存的歷史記錄可以零知識化。但「即時診斷」需要 server 看數值才能算 ── 不能完全零知識

或許答案是「分區零知識」:高頻寫入的歷史 vault 走零知識、即時計算用標準加密 + RLS。這是個值得設計的中間地帶

問題 2:忘記密碼恢復可以怎麼做而不破壞零知識?

幾個方向:

  • Shamir Secret Sharing 三選二恢復:把主密碼拆 3 份,2 份夠救
  • 社交恢復(social recovery):指定 3 個朋友,2 個 attestation 後解鎖
  • 硬體 token + 主密碼 雙因素:丟其中一個還活著

每個都有自己的安全 / UX 取捨。我目前的看法:純零知識的 Kaizei 留給高安全需求使用者,恢復機制是另一個產品階段的設計題


給跨領域的人

跨領域不是換主題,是把同一套思維框架套到新場域驗證。我的「資料隱私先行」思維在醫療上是 RLS,在個人理財上是零知識加密。核心觀念相同,技術實現不同

如果你也在跨領域中:

  1. 找你「離不開」的一條核心思維 ── 對我是隱私 / 安全。對你可能是性能 / 可解釋 / 可組合
  2. 把那個思維套到新場域 ── 看會發生什麼有趣事情
  3. 新場域回頭批判舊場域 ── Kaizei 之後我才看出 ClinCalc 預設的 server-trust 假設

跨領域的價值不在「會兩件事」,在「用一件事看另一件事」。


延伸閱讀