同一套保管箱 UI:瀏覽器、PWA 與桌面/行動殼層
排程中閱讀時間 7 分鐘 作者 NT²
解鎖、列表、搜尋與設定不該裂成三個產品。NT² Vault 維持一套保管箱 UI, 讓每個殼層只負責平台必須擁有的事:耐用檔案如何落在該裝置上。
同一套保管箱 UI:瀏覽器、PWA 與桌面/行動殼層
主張很直接:NT² Vault 以同一套保管箱 UI,運行於瀏覽器分頁、已安裝的 PWA,以及把該 UI 嵌進系統 WebView 的桌面與行動殼層。
這不是「寫一次就假裝到處都是原生」。這是產品邊界。保管箱體驗——建立、解鎖、為列表分頁、開啟項目、附加檔案、再次鎖定——屬於同一個應用。殼層只在平台強迫分歧之處分歧:耐用資料庫檔與附件密文如何落盤,以及網頁層能透過薄橋接呼叫哪些 OS 能力(生物辨識、系統共用表、視窗生命週期)。
無論你在分頁開啟 se.nt2.me、安裝成 PWA,或啟動封裝的桌面/行動殼層,你應該認出同一個保管箱。不是換了一棵設定樹的遠房親戚。就是同一個保管箱。
限制:三套 UI 等於三個安全產品
結構化、零知識的保管箱不是包在 frame 裡的型錄站。解鎖在客戶端衍生金鑰。工作階段金鑰活在記憶體中,必須在閒置、重新整理與鎖定時清除。項目負載與附件位元組在碰觸耐用儲存之前就已密封。列表從真正的 SQLite 資料庫分頁,而不是灌成巨大的記憶體內陣列。多標籤行為選出單一 Writer。這些規則與 UI 糾纏:每一次儲存、搜尋或附加的按鈕,都是通往密碼學與儲存的路徑。
團隊常靠分叉介面來「解決」跨平台。
- 手機用 Swift 或 Kotlin 應用。
- 桌面用 Electron 或 Qt 樹。
- 瀏覽器另做一套網頁客戶端。
每個分叉都以好意開始——原生元件、商店規範、「在該平台感覺像家」。成本稍後才到。加密語意漂移。解鎖邊界案例在一個殼層修好、在另一個被遺忘。附件分塊、信封解開、失敗即關閉的主密碼驗證,變成三套實作與三份錯誤史。行銷承諾「每個裝置上同一個保管箱」。工程交付三個剛好共用一個標誌的產品。
還有更軟的陷阱:瀏覽器維持一套 UI,然後出一個「原生」殼層,只重做看起來難的畫面——解鎖與列表——而把設定、備份、共用流程留在半套 WebView 路徑上。使用者看得到接縫。審查者看得到接縫。安全審查者會注意到:當外殼一變,密碼學邊界也跟著動了。
因此限制不是「WebView 比原生 UI 好看」。而是保管箱的信任模型就是產品。複製 UI 樹,就複製攻擊面與回歸面。對一個本機優先、已在瀏覽器上押注網頁平台做密碼學與儲存的保管箱而言,誠實的做法是守住這個賭注並包住它——而不是重建三次。
設計:一個保管箱應用、三個殼層、兩種儲存形狀
NT² Vault 的主客戶端是單一的網頁保管箱應用。在瀏覽器與已安裝 PWA 中,就是該應用本身。在桌面與行動殼層中,同一份建置跑在系統 WebView 裡。進入、解鎖、建立、還原、儀表板、收件匣與設定的路由維持共用。Svelte 元件維持共用。加密、解密、分頁與附加的領域規則維持共用。
改變的是同一契約下的儲存轉接層。
在瀏覽器(分頁或 PWA)中,保管箱的 SQLite 資料庫與附件密文落在 Origin Private File System(OPFS):每個保管箱一個真實資料庫檔,旁邊是加密的附件影格。IndexedDB 只留下裝置層級的小型索引供保管箱選擇器使用——不是關聯式閣樓。SQLite 本身以 WebAssembly 跑在專用 Web Worker,讓 UI 執行緒能專注於捲動與輸入。
在桌面與行動殼層上,相同的邏輯配置使用應用私有資料目錄下的原生檔案:每個保管箱一個 SQLite 檔,以及平行的 attachments 目錄存放加密 .bin 影格。中繼資料仍在 SQLite。密文仍在資料庫之外。當主機檔案系統改變時,產品規則不變。
flowchart TB
subgraph shells [殼層]
Tab[瀏覽器分頁]
PWA[已安裝 PWA]
Desk[桌面殼層]
Mob[行動殼層]
end
UI[同一套保管箱 UI]
subgraph storage [儲存轉接層]
OPFS[OPFS:vault.sqlite + 附件影格]
Native[原生檔案:保管箱 DB + 附件影格]
end
Tab --> UI
PWA --> UI
Desk --> UI
Mob --> UI
UI --> OPFS
UI --> Native
殼層也暴露一小段橋接,承接開放網頁無法單獨擁有的能力:作業系統允許時的生物辨識提示、系統共用表、觸覺回饋,以及強化失焦鎖定或關閉鎖定政策的視窗生命週期掛鉤。這座橋刻意保持精薄。它不重做保管箱。它不持有主密碼。它不成爲第二套密碼學堆疊。
結果是三個宿主、一個產品:
| 宿主 | 使用者看到什麼 | 耐用保管箱位元組落在哪裡 |
|---|---|---|
| 瀏覽器分頁 | 同一套保管箱 UI | OPFS 資料庫 + 附件檔 |
| 已安裝 PWA | 同一套保管箱 UI,可安裝外觀 | 相同的 OPFS 路徑 |
| 桌面/行動殼層 | WebView 中的同一套保管箱 UI | 原生的每保管箱檔案 |
可選的雲端同步(啟用 Premium 時)仍是疊在該本機真相之上的複本路徑。殼層選擇不改變零知識規則:邊緣仍只見密文與帳戶中繼資料,不見明文負載或 KDF salt。無論你在分頁或封裝應用中,解鎖都維持本機。
取捨:一套 UI 意味著接受 WebView 的界限
透過 WebView 運送同一套保管箱 UI 是取捨,不是口號。
原生外觀不是免費的。 封裝、權限與商店審查仍須遵守平台規範。生物辨識與共用表需要謹慎橋接。有些 OS 元件永遠不會與完全原生的列表長得一模一樣。想用 SwiftUI 優先重做每一個畫面的工程師,會發現架構故意固執。
能力矩陣是真實的。 OPFS 可用性、Worker 行為與檔案系統配額因瀏覽器而異。桌面與行動殼層繼承宿主 OS 的 WebView 怪癖。功能偵測屬於儲存與能力邊界——不是靜默分叉,為「這個平台」發明第二條解鎖路徑。
訊息延遲與轉接層紀律仍在。 瀏覽器路徑仍要為 SQLite 周圍的 Worker RPC 付費。殼層仍需要履行相同開啟/讀取/寫入/串流契約的附件密文轉接。圖方便的捷徑——「在行動上乾脆把 blob 丟進 SQLite」——會摧毀讓關聯式引擎保持快速的中繼資料/密文分離。
發行節奏把殼層耦合到保管箱應用。 解鎖的錯誤修正會落到 UI 運送之處。這正是重點。這也是紀律:殼層發行必須追蹤保管箱建置,而不是漂成「下季再說」的私有 UI 分叉。
我們接受這些成本,因為另一條路——三套 UI——代價是信任。在手機與桌面用不同方式解鎖的保管箱,不是「平台原生」。那是圖示比較好看、卻未完成的密碼學。
換回來的是:可以在單一處推理工作階段記憶體、信封加密、分頁列表與附件 BlobStore 規則。審查者可以問一個問題——「這條 UI 路徑有沒有守住本機優先邊界?」——而不是三個。使用者可以從瀏覽器的一天走到桌面的一天,不必再學第二個產品。
我們拒絕什麼
架構在拒絕寫清楚時更清楚。
我們拒絕為保管箱維護平行的原生 UI 樹。 桌面與行動殼層嵌進保管箱應用。它們可以加薄的 OS 橋接。它們不成爲第二套設定、列表與解鎖實作。
我們拒絕每個殼層一套不同的密碼學堆疊。 PBKDF2 衍生、AES-GCM 信封、不可擷取金鑰,以及失敗即關閉的本機驗證,在各處都是客戶端規則。殼層不會為了方便而得到一套削弱這些規則的「更簡單」解鎖。
我們拒絕「因為原生檔案很煩」就把附件密文塞回 SQLite。 中繼資料維持可在資料庫查詢。加密影格維持在檔案 BlobStore——瀏覽器是 OPFS,殼層是原生檔案——如此大型二進位不會把每一次列表查詢變成倉庫掃描。
我們拒絕讓殼層成為真相來源。 裝置上的保管箱資料庫與附件存放,仍是日常使用的權威。雲端是可選的複本傳輸。把應用封裝起來,不代表伺服器突然擁有解鎖。
我們拒絕把靜默產品分叉偽裝成漸進增強。 若某平台無法滿足安全本機保管箱所需的儲存或 Worker 要件,那是支援邊界——不是授權在同一品牌背後發明更弱的主執行緒或伺服器後備變體。
這些拒絕守住標題中的主張。同一套 UI 不是建置口號。它是一套信任模型如何在三個宿主上存活。
一個產品,三扇門
本機優先保管箱,無論從分頁開啟、釘成 PWA,或啟動桌面/行動殼層,都應感覺像同一間私人閣樓。閣樓的家具——路由、列表行為、加密、鎖定衛生——不該在門框改變時重新排列。改變的只有地板:瀏覽器上的 OPFS,以及殼層提供私有資料目錄時的原生檔案。
這個故事與本機優先堆疊的其餘部分並列。若想了解為什麼產品能在關鍵路徑上不靠沉重伺服器運作,請讀為什麼選擇 PWA 本地優先、零伺服器的 Vault。關於瀏覽器資料庫檔邊界,見為什麼我們將保管箱的 SQLite 資料庫放在 OPFS,而非 IndexedDB。關於附件密文為何留在關聯式引擎之外,見中繼資料在 SQLite,密文在 BlobStore。關於在瀏覽器中讓引擎離開 UI 執行緒,見SQLite 放在 Web Worker 裡,不是選配。
最後更新 2026-10-07
相關故事
- 來自一萬筆項目 OPFS 保管箱的基準測試
閱讀時間 7 分鐘
- 結構描述 v60 與漸進式遷移
閱讀時間 7 分鐘
- SQLite 放在 Web Worker 裡,不是選配
閱讀時間 7 分鐘