端對端加密,金鑰在你自己的裝置上
Happier 讓同一個程式設計工作階段,在它執行的那台電腦和你用來觀看它的每一台裝置之間保持同步,而中間坐著一台中繼伺服器。這一頁就是那句話底下的架構:哪一把金鑰在哪裡產生、中繼站實際持有什麼、它仍然看得到什麼,以及在一台被設定為關閉加密的伺服器上會有什麼改變。
一則訊息在你的手機和電腦之間是怎麼加密的
你從手機送出的一則訊息,畫在它經過的三個節點上。代理程式回覆的一切都以相反方向走同一條路徑,在離開你的電腦之前就先封起來。
- 你的手機
- 在這裡封起來. 訊息在你輸入它的裝置上加密,用的是只屬於該工作階段的金鑰 — 建立工作階段時抽取的 32 個隨機位元組,AES-256-GCM,每則訊息一個新的 nonce。接著該工作階段金鑰會透過一次臨時的 X25519 交換封裝到你帳號自己的金鑰上,而交給中繼站的正是這份封裝後的副本。
- 中繼站
- 封著被傳送. 伺服器寫下的是一列形如 { t: "encrypted", c: "…" } 的資料 — base64,加上一個它沒有秘密金鑰的封裝金鑰信封。它可以路由那一列、計數它、保存它並交還它。它沒有任何東西可以打開它,也沒有任何端點要求它這麼做。
- 你的電腦
- 在這裡打開. CLI 用你連結終端機時取得的帳號金鑰打開封裝的工作階段金鑰,解密,然後把明文傳給 Claude Code、Codex、OpenCode 或正在執行的任何代理程式。回覆以相反方向走同一趟路,在離開之前先封起來。
中繼伺服器儲存什麼
加密是一項關於內容的主張,而關於內容的主張只是答案的一半。這裡是另一半,詳細程度相同:營運這個中繼站的伺服器在沒有金鑰的情況下能讀到哪些欄位。
- 密文,以及一個封裝的金鑰信封
- 每則訊息一列,裡面只有 base64,加上該工作階段被封裝的資料金鑰。封裝是在裝置上完成的;沒有任何伺服器路由會產生一個。
- 識別碼與形狀
- 帳號、裝置和工作階段的 id,每個工作階段一個隨機標籤、序號、時間戳和大小。這是任何會同步的系統上加密都涵蓋不到的部分,值得直說:中繼站知道你送了多少、什麼時候送的。
- 每則訊息來自哪一側
- 每則訊息都帶著一個角色 — user、agent、event 或 unknown — 由用戶端隨密文一起提供。它是一個欄位,好讓對話紀錄可以分頁和計數而不必被打開。在加密的資料列上,伺服器無法推導出它;它只能儲存用戶端申報的內容。
- 一份刻意做小的活動投影
- 工作階段是否未讀、有多少權限請求在等待、代理程式問了幾個問題、它是不是在思考、上一個回合怎麼結束。這其中每一項都是衍生狀態 — 一個計數器、一個旗標,或用戶端在工作階段推進時寫下的一個時間戳 — 從來不是任何一個字的內容。它存在的目的,是讓一支關了一小時的手機在你打開的那一刻顯示正確的標記,而計數就是伺服器被賦予用來做這件事的最大權限。
- 你輸入了什麼,以及代理程式回了什麼
- 對話紀錄就是密文。沒有第二份副本,沒有伺服器端在它之上的索引,也沒有需要索引的摘要步驟。
- 工作階段在哪裡執行
- 專案路徑、你的主機名稱、你的家目錄和 CLI 版本都是工作階段中繼資料,而中繼資料在工作階段建立之前就用同一把工作階段金鑰加密。它們是封裝資料塊裡的欄位,不是伺服器可以排序的資料表欄位。
- 你的供應商憑證
- 已連結的帳號 — 工作階段所使用的 Claude 或 ChatGPT 憑證 — 在上傳之前就在裝置上封起來,用的是從你的帳號機密為這一個用途衍生出來的金鑰。中繼站儲存的是一個信封,並把它交還給你自己的裝置。
- 你的儲存庫
- 你的儲存庫從不會被同步或鏡射。代理程式在它執行的那台電腦上讀寫檔案,而那台電腦仍是唯一的完整副本。當你要求一個檔案、一個資料夾或一份差異時,它會走和其他東西一樣的封裝路徑 — 因為你要求了,而不是因為 Happier 留了一份副本。
你的加密金鑰是在你自己的裝置上產生的
你的帳號根部有一個機密,而它是在裝置上建立的 — 不是由伺服器發放。其餘的一切都由一棵金鑰樹從它衍生出來:一個 HMAC-SHA-512 的根,然後每一個標記路徑元素做一次 HMAC。用來打開封裝工作階段金鑰的金鑰對,來自那棵樹中標記為 "content" 的位置,其他每一種儲存資料塊也各有自己的一把金鑰,所以沒有任何一把金鑰身兼兩職。
這就是為什麼一個全新的瀏覽器可以登入你的帳號,卻仍然看不到上週的任何東西。登入是向中繼站證明你是誰。讀取舊的工作階段需要金鑰,而金鑰不在中繼站上,無從送出。還原裝置才是搬動它的那一步 — 從一台已經有它的裝置,或從你保存下來的秘密金鑰。
這也代表遺失的情況是真的,值得說一次:如果每一台登入過的裝置都不見了,而秘密金鑰也隨之不見,那些加密的工作階段無法由我們或任何人復原。一個能把它們交還的中繼站,就是一個能讀取它們的中繼站。
連結一台新裝置,以及你的金鑰如何送到它上面
新增裝置是金鑰材料移動的時刻,所以也是最值得理解的時刻。掃描 QR code 會發出一個請求,而它做的就只有這樣:核准發生在一台已經登入的裝置上,而當兩個畫面都顯示一組簡短的確認碼時,你應該在核准前先比對它們。
核准實際做的事,是把內容金鑰封裝到提出請求的裝置的公開金鑰上,放在一個帶著臨時寄件者金鑰和新 nonce 的信封裡。那個信封和其他東西一樣經過中繼站。連結終端機則在上面多加一層綁定:酬載會對著一個隨 QR code 傳遞、而非經由伺服器傳遞的配對機密進行驗證,所以一個調換酬載的中繼站無法產生相符的標籤。
儲存政策:端對端加密是預設值
以上描述的都是預設值,而這個預設不是用戶端可以說服伺服器改變的建議。全新的中繼站設定為要求端對端加密儲存:明文寫入會以政策錯誤被拒絕,而不是靜靜地被接受。用戶端在建立工作階段之前會從伺服器自己的 features 端點讀取政策,所以一個工作階段處於哪一種模式,是在它的第一則訊息之前就談好的。
伺服器維運者還有另外兩種設定可用。其中一種可以讓帳號自行選擇,於是加密和明文工作階段在同一個中繼站上並存 — 這需要同時開啟選用式政策和退出許可,而許可在維運者開啟之前是關閉的。另一種要求明文,給那些在基礎架構層管理加密、且需要伺服器能夠讀取和處理工作階段內容的組織。最後那一種是一個真實的取捨,值得不加修飾地說出來:在明文工作階段上,伺服器可以讀取對話紀錄。那正是這個設定的用意。
模式在工作階段建立時就固定,所以一個工作階段永遠不會一半是這個一半是那個,而改變一個帳號的模式,改變的是新工作階段會發生什麼,不會回頭影響舊的。沒有東西會被追溯解密,也沒有東西會被追溯加密。
設定這三項的變數、它們底下的靜態資料選項,以及環繞它們的身分控制,是屬於維運者的那一半,它們在給團隊的自行架設頁面上。
推播通知是由你自己的電腦送出的
推播通知是唯一一件跳出這個模型的事,因為它必須抵達一支什麼都沒開著的手機。Happier 的答案是由你自己的電腦送出它們。伺服器儲存你的推播權杖;你自己電腦上的 CLI 把它們讀回來,並自行送出通知,這讓中繼站不必進入一條它原本得被告知內容的路徑。
酬載攜帶的內容刻意很薄:工具或請求的類型,以及在有幫助時的一小段提示 — 像是 src/file.ts 這樣的檔案、一個指令名稱、像「3 questions」這樣的計數。原始的工具輸入不會被放進去。足以讓你決定要不要拿起手機,而且是在任何站在你背後的人都讀得到的螢幕上。
自行架設中繼站:中繼資料也留在你自己的硬體上
以上所有內容在一個不是你執行的中繼站上同樣成立。要自己執行的理由是上面關於識別碼與形狀的那一列:加密不會隱藏誰在和誰對話、頻率多高,而在你自己的中繼站上,那本帳也是你的。一道指令就把中繼站放到你自己的硬體上,路徑於是變成你的裝置、你的中繼站、你的電腦,裡面沒有任何一樣不是你部署的。
自己讀那份加密程式碼
這一頁上沒有任何認證,因為根本沒有可報告的認證,而一個對自己沒有的法遵含糊比劃的資安頁面,比一個完全不提的還糟。這裡放的是上面每一句話所依據的程式碼,MIT 授權,你在執行之前就能讀:packages/protocol/src/crypto 裡的基本元件、apps/cli/src/api/encryption.ts 裡的工作階段加密、伺服器在其 features 端點強制執行的儲存政策,以及能確切顯示哪些欄位是明文的 schema。
這也是關於你該怎麼看待這一頁的誠實答案:把它當成一張指出去哪裡看的地圖,而不是一個要你憑信任接受的承諾。讀那四個檔案,或者自己執行中繼站,讀它向用戶端通告了什麼。
加密模型參考以操作程序的角度涵蓋同樣的範圍 — 還原會要求你做什麼、每一種儲存模式對一個帳號代表什麼、當某台裝置還讀不到某個工作階段時該用哪個流程。

