Staging — 僅供部落格預覽。
跳至主要內容

CBF:結構化欄位離開記憶體的方式

排程中

閱讀時間 8 分鐘 作者 NT²

編輯時,一筆憑證是欄位對應表。一旦這些欄位必須熬過鎖定、重新啟動或複本同步,它們需要的是可攜式密文形狀——而不是一份等著下一位讀取者打開的 JSON 傾印。

CBF:結構化欄位離開記憶體的方式

主張:結構化欄位需要可攜式密文形狀

保管箱解鎖時,項目以「結構」的形式才好用。憑證有使用者名稱、密碼、URL,或許還有一次性驗證碼。筆記有帶型別的文字。銀行或文件範本可能承載數十個具名欄位位置。介面編輯的是欄位對應表;領域邏輯驗證的也是這張表。因為使用者主動要求,記憶體可以暫時持有明文。

但靜態儲存時,這張表必須不再是一張表。

NT² Vault 不會把項目內容以可讀 JSON 的形式與標題並列儲存,也不會為每個可能承載項目的通道——今天是 SQLite、明天是備份檔、之後是複本批次——各自發明一套序列化。敏感欄位離開記憶體時,會變成 CBF:一種精簡、可攜的密文容器。明文層之內是 payload blob(內容資料塊)——依穩定欄位登錄表編碼的結構化欄位;資料塊之外是 AES-GCM 的驗證加密,並可在封裝前選擇性使用 zstd 壓縮。

產品主張很窄、也很刻意:CBF 就是結構化欄位離開記憶體的方式。 信封加密決定「用哪一把金鑰」封裝物件;CBF 決定「那把金鑰封裝的是哪些位元組」,以及這些位元組如何在儲存與傳輸之間保持可攜,卻不會變成別人眼中的明文結構描述。

限制:靜態的 JSON 不是隱私邊界

結構化保管箱很容易誘惑人走捷徑:把欄位對應表編碼成 JSON、存進某個欄位、把欄位叫做 payload,「之後再加密」,或者用保管箱範圍的金鑰加密整列,把中間格式當成實作細節。

這條捷徑會同時在好幾處失敗。

可讀的中間形態會誘發意外揭露。 若持久表示仍然是欄位對應表——只是包了 base64、只是 gzip、只是「放在加密資料庫檔案裡」——每一個備份工具、同步除錯器與支援傾印,都可能變成明文外洩點。零知識不是「我們有加密資料庫檔案」的行銷形容詞,而是關於保管箱鎖定時磁碟上究竟存在什麼的規則。

對 JSON 做臨時加密很脆弱。 字串化、AES-GCM、存密文與 IV——這對單一應用版本可行。下一個版本新增欄位、重新命名鍵、巢狀複合值,或需要共用欄位子集時,若沒有版本化配置與欄位登錄表,每個消費者都會重造解析;若沒有驗證框架,「密文」仍可能依賴關於字元編碼、鍵順序、哪些鍵是機密的未記錄約定。

通道一多,序列化器就增生。 本機 SQLite 要一種形狀,備份要另一種,深層連結與共用套件又要第三種。若每條路徑各自發明 JSON 方言,完整性檢查會分歧、遷移會分叉,某一通道的錯誤也可能讓另一通道對「同一個項目」持有不同想像。

大小與 CPU 重要,但不該喧賓奪主。 結構化內容常是重複文字。素樸 JSON 膨脹很快。加密後再壓縮幾乎無用;加密前壓縮可能有幫助,但格式必須標明是否曾壓縮,並在不一致時以失敗收場。

NT² Vault 需要一個答案回應:「項目內容不再位於記憶體時,它究竟是什麼?」這個答案必須對範本夠結構化、對鎖定儲存夠不透明,也要對備份與盲目同步夠可攜。

設計:封裝盒裡的內容資料塊

CBF 有兩層互相配合。用白話命名就夠了;重點是每一層擁有的邊界。

第一層——內容資料塊(payload blob)

編輯時,應用程式以範本驅動的欄位對應表運作:位置鍵與值。加密之前,這張表會編碼成二進位 內容資料塊

資料塊不是「鍵比較短的 JSON」。它記錄:

  • 這份內容屬於哪一種(例如保管箱項目,或共用快照);
  • 項目屬於哪個類別範本;
  • 以共享登錄表中的穩定欄位索引定址的欄位值,而不是把自由字串鍵散落在密文裡;以及
  • 可選擴充,例如快照需要點名相關檔案、卻不必內嵌檔案位元組時的附件清單。

用於清單篩選與全文搜尋的標題與類別,刻意留在資料塊之外,作為明文中繼資料。敏感值——密碼、帳號、筆記本文、TOTP 祕密——只存在於即將封裝的編碼內容裡。

第二層——封裝盒

內容資料塊接著進入 可攜式密文容器。概念上:

  1. 當大小啟發式認為值得壓縮時,選擇性以 zstd 壓縮資料塊。
  2. 產生新的初始化向量。
  3. 以物件的內容加密金鑰,透過 AES-GCM 加密。
  4. 用版本化標頭框住結果:內容種類、金鑰種類、密碼套件、旗標(含是否使用 zstd)、IV,以及密文長度。

AES-GCM 為內部位元組提供機密性與完整性。被截斷或置換的資料塊應在驗證時失敗,而不是解碼成看起來合理的錯誤項目。IV 與密文一併儲存;它不是祕密,但在同一把金鑰下每次加密都必須唯一。

flowchart LR
    Fields[記憶體中的結構化欄位] --> Blob[內容資料塊]
    Blob -->|可選 zstd| Plain[待封裝位元組]
    Plain -->|AES-GCM| Box[CBF 密文盒]
    Box --> Disk[SQLite/備份/同步]

封裝盒放在哪裡

對一般保管箱項目而言,本機資料庫把封裝盒存為項目的加密內容。同一族位元組也可以進入備份與複本套件,不必再發明第二套「匯出方言」。盲目同步可以搬運不透明密文,因為邊緣從不需要理解欄位索引——只需要儲存與轉送位元組。

這坐落在本系列稍早描述的信封模型之下。每個項目仍有自己的內容加密金鑰;保管箱金鑰包裝該 CEK。CBF 是 CEK 所加密內容的形狀。附件檔案是另一件事:大型二進位密文留在 SQLite 之外的 BlobStore,而項目欄位離開記憶體時成為 CBF。中繼資料與密文放置是分開的決策;兩者都拒絕靜態明文。

解鎖後開啟項目,路徑反向進行:

  1. 載入封裝盒與經包裝的 CEK。
  2. 以保管箱金鑰解開 CEK。
  3. 開啟封裝盒:驗證、解密,若有旗標則解壓縮。
  4. 把內容資料塊解碼成介面用的欄位對應表。
  5. 明文只留在作用中工作階段的記憶體。

AES-GCM 由瀏覽器中的 Web Crypto 執行。主密碼不會跟著資料塊一起走。格式可攜;金鑰不可。

取捨:選擇格式,而不是傾印

CBF 付出的代價,是每種持久格式都會付出的代價。

作者與工具必須懂得配置。 封裝盒的十六進位傾印不是支援主控台。除錯需要在解鎖後走有意的開啟路徑,而不是「把欄位印出來」。比起在資料庫 GUI 瀏覽 JSON,這比較不方便——而這種不方便,某種程度上正是重點。

欄位演化需要登錄表。 穩定索引與配置版本,取代在密文裡臨時改名鍵。新增範本欄位是產品發行議題:編碼與解碼必須一致。未知擴充需要明確規則,讓較舊的用戶端安全失敗或安全略過,而不是自行發明意義。

壓縮是可選的,不是裝飾。 AES-GCM 之前的 zstd 可以縮小重複文字,但會多一條分支:壓或不壓、設旗標、開啟時解壓、拒絕不一致的信封。極小的內容可能略過壓縮。啟發式存在,是為了誠實交換 CPU 與大小;壓縮永遠不能取代加密。

兩則密碼學故事容易混淆。 信封加密回答金鑰範圍;CBF 回答內容形狀。把它們併成一句——「我們用保管箱金鑰加密 JSON」——兩則故事都會消失。分開保存,代表列上有更多欄位(經包裝的 CEK、IV、封裝盒),以及在失敗時更清楚的推理。

我們接受這份簿記,因為替代方案更糟:看起來已加密、磁碟上卻仍以明文結構思考的保管箱;或一群幾乎彼此同意、卻逐漸漂移的通道專用序列化器。

我們拒絕什麼

我們拒絕靜態儲存明文結構化內容。 解鎖的編輯器可以在記憶體中持有欄位對應表;鎖定儲存持有的是封裝盒。重新整理、閒置鎖定與行程結束會清除工作金鑰;它們不會留下一份親切的 JSON 紀念品。

我們拒絕為每個通道發明新的線上形狀。 本機持久化、備份與複本傳輸,應對項目內容承載同一族可攜密文——而不是 SQLite 方言、JSON 壓縮包方言,以及幾乎同意的同步方言。

我們拒絕把壓縮當成機密性。 zstd 縮小體積;AES-GCM 提供祕密性與真實性。旗標記錄發生過什麼;它們不能取代封裝。

我們拒絕在持久密文裡使用自由字串作為機密鍵名。 欄位索引與範本脈絡,讓內容可演化,而不必把長得像密碼的字串鍵散落在每份儲存資料塊裡。

我們拒絕要求邊緣解讀欄位。 可選同步可以儲存或轉送不透明的封裝盒。盲目複本不必知道哪個索引是密碼。若伺服器必須解析你的筆記才能「幫忙」,那就不再是零知識邊界。

我們拒絕用一把保管箱範圍的內容金鑰,取代格式紀律。 即便有了 CBF,每個物件仍保有自己的 CEK。可攜資料塊若落在同一把共享內容金鑰之下,仍會以錯誤方式耦合輪替、共用與爆炸半徑。

同樣的欄位,唯有密文得以持久

結構化保管箱在記憶體中證明價值:範本、驗證、專注的編輯器,以及封裝之外可搜尋的標題。它在靜態時履行承諾的方式,是拒絕讓結構隨意躺在那裡。

CBF 是這兩種模式之間的樞紐。結構化欄位變成內容資料塊;資料塊變成可選擇性先壓縮、再以 AES-GCM 封裝的盒子。那個盒子才是鎖定後仍能存活的東西。信封加密決定哪一把金鑰開啟哪個物件;本機鹽值,以及拒絕由服務提供者重設密碼,決定究竟誰能從頭派生保管箱金鑰。

關於每個物件周圍的金鑰階層,請讀每個物件一把金鑰:NT² Vault 內的信封加密。關於解鎖材料為何不會變成雲端猜測套件,請讀KDF 鹽值留在你的裝置上。關於我們為何刻意無法重設主密碼,請讀無法重設密碼,是刻意的設計。關於大型檔案如何把密文留在關聯式目錄之外,請讀中繼資料在 SQLite,密文在 BlobStore

若你想要一座敏感欄位只以可攜密文離開記憶體的保管箱,可以試試 NT² Vault,或到 nt2.me 了解更多。

最後更新 2026-09-23

相關故事