錯誤密碼必須以關閉方式失敗
排程中閱讀時間 7 分鐘 作者 NT²
解鎖不是對你所有資料的盡力解密。NT² 先檢查一小段本機驗證碼。錯誤密碼會以關閉方式失敗:保管箱維持鎖定,介面也保持誠實。
錯誤密碼必須以關閉方式失敗
主張:解鎖是閘門,不是希望
NT² Vault 把錯誤的主密碼當成關著的門,而不是柔性失敗。
解鎖時,裝置不會打開每一筆加密項目「看看看起來對不對」。它不會解密一則筆記樣本,再從亂碼 UTF-8 猜測。它也不會讓丟出的 DOMException 變成空白畫面,卻可能留下半初始化的工作階段狀態。解鎖是一道刻意的閘門:用你輸入的密碼派生金鑰材料,驗證一小段本機檢查值,然後才把保管箱帶進已解鎖工作階段。
這道閘門必須以關閉方式失敗。密碼錯誤 → 保管箱維持鎖定。驗證碼損壞 → 保管箱維持鎖定。任何無法通過完整性驗證的解密 → 視為操作失敗,顯示清楚錯誤,並讓密文保持不可讀。產品可以顯得固執,但絕不能顯得神秘。
「以關閉方式失敗」說起來容易,違反起來也容易。有些產品會做「樂觀」解鎖,一路往前走到看起來不對勁為止。框架把密碼學錯誤變成未處理的 rejection。客服工具在狀態已被錯誤密碼部分改動時,卻叫使用者「重新整理再試」。每個捷徑都把清晰的安全性質,換成更軟的失敗模式——而軟失敗模式,正是鎖定保管箱變成混亂保管箱的路徑。
NT² 把界線畫在前面:解鎖的驗證,必須先於內容的可用性。
約束:以開放方式失敗的密碼學,會變成不安全的 UX
以密碼為基礎的保管箱落在尷尬的邊界上。主密碼是人的祕密;密文是機器材料;介面必須把密碼學結果轉成使用者能採取行動的語言。許多設計正是在這段翻譯裡,漂向以開放方式失敗。
想想「先解密看看」代表什麼。AES-GCM 是帶認證的加密:錯誤金鑰不應在通過完整性標籤的情況下產出看似合理的明文。這是一份禮物。它讓 Web Crypto 能在受控檢查中拒絕錯誤密碼,而不是發明可讀的垃圾。如果產品忽略這份禮物——沒有捕捉、捕捉太晚,或在 unwrap 失敗後仍繼續——使用者體驗就變成了安全模型:
- 未處理的解密錯誤可能在解鎖流程中途崩潰,留下看起來「幾乎已解鎖」的金鑰、旗標或路由狀態。
- 半開工作階段可能露出列表外殼、標題或空白編輯器,而承載內容仍拒絕開啟——教使用者「鎖定」是可以協商的。
- 以崩潰當 UX 的路徑,會訓練人們一直重新整理、強制結束、或重試密碼直到「有東西動起來」。攻擊者喜歡這種環境式重試文化;正當使用者也會學到:錯誤只是雜訊。
成功路徑上也有相關陷阱。若只有在第一個項目解密成功時才算解鎖,保管箱就沒有獨立的密碼檢查。單一物件損壞會偽裝成錯誤密碼。反過來說,若在任何驗證完成前就把使用者丟進應用程式,錯誤密碼就變成導覽問題,而不是存取控制問題。
以關閉方式失敗,代表在這些歧義出現之前就選好失敗模式。系統需要一項不依賴打開整份檔案庫的專用解鎖檢查。它需要把認證失敗當成預期輸入、而不是例外崩潰的解密路徑。它也需要能說「密碼錯誤」或「無法解鎖」的介面文案,而不暗示保管箱在壞掉的外殼底下可能仍可讀。
一旦有人複製了本機解鎖材料,離線猜測仍然可能。以關閉方式失敗的解鎖,並不會為被偷走的裝置設定檔發明線上速率限制。它做的是讓應用程式對邊界保持誠實:在驗證碼通過之前,沒有已解鎖的保管箱——只有鎖定的保管箱,以及一次被拒絕的嘗試。
設計:本機驗證碼,然後才是密封解密
解鎖檢查刻意保持很小。
建立保管箱時,裝置會在本機保管箱設定檔中存放密碼驗證碼:一段以 AES-GCM 密封的已知明文密文,金鑰材料由主密碼與保管箱本機 KDF 鹽值派生。驗證碼不是提示。它不是密碼的第二份副本。它不會上傳讓伺服器為猜測打分。它的工作很窄——只在裝置上回答一個問題:這個由密碼派生的金鑰,打不開這段檢查值?
解鎖接著是一段短序列:
flowchart TD
A[輸入主密碼] --> B[在本機派生金鑰材料]
B --> C[以 AES-GCM 解密密碼驗證碼]
C -->|認證失敗 / 丟出例外| D[以關閉方式失敗:維持鎖定]
C -->|已知明文正確| E[繼續解鎖因子]
E --> F[記憶體中的已解鎖工作階段]
F --> G[僅在需要時解密項目與附件]
G -->|任何解密失敗| H[捕捉:拒絕明文 / 顯示錯誤]
有幾項性質比圖中的方塊更重要。
已知明文是刻意的。 驗證碼加密的是產品定義的固定檢查值——不是使用者筆記、不是標題、也不是難以定義「成功」的隨機 blob。AES-GCM 認證通過後,明文還必須符合保管箱的預期。錯誤密碼會讓認證失敗,或讓比對失敗。無論哪一種,解鎖都會停下。
驗證碼與本機解鎖材料放在一起。 鹽值與驗證碼留在裝置上的保管箱設定檔中。這與把 KDF 鹽值留在邊緣之外是同一套保管故事:雲端不會變成密碼檢查櫃台。本機解鎖可以離線成功。若使用雲端同步,仍需要另一次密碼學身份證明;它不能取代驗證碼。
每一條解密路徑都把 Web Crypto 包在 try/catch 裡。 解鎖是第一個消費者,但不是唯一一個。開啟項目、解開每個物件的內容金鑰、讀取附件信封、驗證共用套件——這些操作都可能因正當理由失敗:金鑰錯誤、blob 截斷、位元翻轉、版本不符。防禦規則是一致的:捕捉失敗,拒絕把位元組當明文,呈現使用者可理解的錯誤,並讓工作階段其餘部分保持一致。單一壞物件不應以未處理例外拖垮已解鎖外殼。閘門上的錯誤密碼,也不該變成使用者必須解讀的堆疊追蹤。
以關閉方式失敗也是工作階段規則。 若驗證失敗,就不會把保管箱 AES 金鑰裝進工作階段記憶體,不會讓「已解鎖」路由意外勝出,也不會留下看起來像成功的部分因子狀態。之後鎖定會清除即時金鑰;失敗的解鎖本來就不該安裝它們。
這套設計與信封加密、可攜密文形狀相配合。驗證碼決定保管箱金鑰路徑能否繼續。每個物件的內容金鑰決定之後哪些密封承載可開啟。結構化欄位離開 RAM 時是密封 blob。驗證碼並不取代那些層;它把它們留在一扇不會因打錯字而半開的門後面。
取捨:誠實會犧牲較軟的「也許」
以關閉方式失敗有產品成本。
打錯一長串密碼的使用者會看到硬拒絕,而不是一個「點點看也許還能用」的半載保管箱。這是正確的安全溝通,短期感覺卻更差。客服無法偷看密文,告訴某人哪個字打錯。本機驗證碼損壞,從外面看可能像錯誤密碼——因為兩者都敗在同一道閘門。區分「祕密錯誤」與「設定檔損壞」需要謹慎文案,以及使用者已持有的還原工具,而不是伺服器神諭。
工程紀律也是成本。每一個新的解密點,都可能有人忘記 try/catch、把原始密碼學錯誤丟回 UI 框架,或記錄敏感的失敗細節。以關閉方式失敗是習慣,不是單一函式。測試必須覆蓋錯誤密碼解鎖,而不只是快樂路徑。錯誤字串必須刻意無聊:它們不該變成側通道,說出「鹽值缺失」、「第三份份額失敗」或「第 47 項可讀」。
我們接受這份摩擦,因為替代方案更糟。有時解鎖進破碎世界的保管箱,會教使用者忽略鎖定狀態。在錯誤密碼時崩潰的保管箱,會教使用者把安全錯誤當成程式錯誤。這兩種教訓都不該出現在存放登入憑證、筆記與身份文件的產品裡。
因此取捨很具體:更尖銳的解鎖失敗、更謹慎的錯誤處理、更少神奇的還原劇場——換來「鎖定」就代表鎖定的工作階段模型。
我們拒絕打造的東西
我們拒絕靠抽樣內容解鎖。 打開隨機項目來判斷密碼是否正確,會把解鎖耦合到資料健康,並邀請模糊的部分成功。
我們拒絕把預期的密碼學失敗當成崩潰 UX。 錯誤密碼與認證失敗是正常結果。它們必須變成清楚的介面狀態,而不是未處理例外。
我們拒絕半解鎖工作階段。 記憶體中沒有保管箱金鑰、沒有「你已進入」的外殼、也沒有蓋在從未於閘門通過認證的密文上的可編輯介面。
我們拒絕上傳驗證碼讓客服檢查猜測。 一旦本機檢查值變成雲端神諭,它就不再是保管邊界。
我們拒絕「設法解密」的靜默後備。 沒有因為舊版曾經如此就略過認證的舊軟失敗路徑。沒有在 AES-GCM 拒絕標籤後仍盡力解碼明文。
我們拒絕把介面韌性當成可選拋光。 解密周圍的 try/catch 是安全邊界的一部分:它讓失敗模式保持以關閉方式失敗,而不是以壯觀方式失敗。
結語:不會半開的門
密碼驗證碼是一小段密文,卻承擔很大的工作。它讓裝置在保管箱假裝開啟之前,就能拒絕錯誤的主密碼。以關閉方式失敗的解密處理,則在金鑰、blob 或信封與現實不符時,讓之後的每一次操作保持誠實。兩者一起把「零知識」從文宣用語,變成日常解鎖體驗:在檢查通過之前,鎖定就維持鎖定。
這項檢查與把 KDF 鹽值留在裝置上、以及拒絕由服務提供者重設密碼落在同一條邊界上。它也補完金鑰不得在重新整理後倖存:即便密碼正確,也只為仍可被清除的工作階段安裝即時金鑰控制代碼。一旦解鎖,內容仍一次開啟一個密封物件——每個物件的信封加密,而結構化欄位離開 RAM 時成為 CBF。
最後更新 2026-09-26
相關故事
- KDF 鹽值留在你的裝置上
閱讀時間 8 分鐘
- 別處 XSS 偷工作階段;這裡金鑰不可匯出
閱讀時間 7 分鐘
- 自動鎖定是記憶體衛生問題
閱讀時間 7 分鐘