feat: passkey 作為全站基礎功能,明網使用、本機加密儲存、離線可解
- 主要言語
- JavaScript
- スター
- 47
- フォーク
- 7
- 平均マージ
- 2時間 42分
- マージ済み PR(30日)
- 291
説明
## 目標
讓讀者在明網的 anoni.net 用 passkey 保護存在瀏覽器裡的個人資料。資料留在讀者的裝置上,站方讀不到,斷網時照樣解得開。passkey 存在讀者自己的密碼管理員或裝置的平台驗證器裡,站上維持沒有帳號、沒有登入、沒有伺服器端驗證。
## 機制
WebAuthn 的 PRF 擴充讓 passkey 內部多藏一把秘密,永遠不離開驗證器。網頁每次驗證附一段輸入,讀者通過指紋或 PIN 之後,驗證器回傳固定 32 位元組的輸出。同一把 passkey 配同一段輸入,每次得到同一段輸出。
typage 已經把它做成 age 的收件人類型,程式在 #424 vendor 進來的 `utils/vendor/age/age-encryption/dist/webauthn.js`,不需要新的第三方程式碼:
1. 加密時產生 16 位元組的隨機 nonce,把標籤 `age-encryption.org/fido2prf` 與 nonce 組成兩段輸入交給 passkey 算 PRF。
2. 兩段輸出接起來經 HKDF 得到包裝金鑰,用 ChaCha20-Poly1305 包住 age 的 file key,nonce 明碼放在 stanza 參數裡。
3. 解密時用 stanza 裡的 nonce 重算一次,得到同一把包裝金鑰。
typage 刻意把一次驗證能算的兩段輸出都用掉才湊成一把金鑰,所以一次指紋只能解開一個檔案。
## 硬限制
這幾條要原樣寫進頁面,讀者看完再決定要不要用:
- passkey 綁在 `anoni.net` 這個 origin。鏡像站、onion、localhost 的 origin 不同,用不了。
- Tor Browser 整個關閉 WebAuthn。`tor-browser-153.2.0esr-16.0-1` 分支的 `001-base-profile.js` 仍把 `security.webauth.webauthn` 設為 false,追蹤的 issue 26614 從 2018 開到現在,2026 年 1 月還有更新。
- passkey 遺失,資料永久打不開,沒有密語可以退回。
- 瀏覽器的儲存空間會在空間壓力下被回收,讀者按「清除網站資料」也會一起清。passkey 還在,資料沒了。
- 用手機當電腦的驗證器要掃 QR,需要網路與藍牙,離線時只有本機已解鎖的密碼管理員、平台驗證器與 USB 安全金鑰能用。
- Linux 桌機沒有平台驗證器,要靠密碼管理員擴充或 USB 安全金鑰。
離線本身沒有問題。文件站裝成離線 App 後,從 service worker 快取載入的頁面 origin 仍是 anoni.net,驗證整個在裝置上完成。網域被封鎖也一樣,快取還在就能用。
## 必要條件
下面四項是功能的一部分,少一項就不上線:
1. 匯出要脫離網域。提供「匯出成密語保護的 .age 檔」,流程是用 passkey 解開再用密語重新加密。匯出的檔案用 age CLI 在任何地方都能開,資料才不會跟 anoni.net 這個網域綁在一起。
2. 建立時強制備援收件人。同一份資料同時加密給 passkey 與一把 X25519 備援金鑰,備援金鑰讓讀者下載或抄寫一次,或改用第二把 passkey。age 規定 scrypt stanza 必須單獨存在,所以備援沒辦法是密語。
3. 請求持久儲存。呼叫 `navigator.storage.persist()`,並把結果顯示給讀者,被拒絕時要說清楚資料隨時可能被回收。
4. 改寫站上的承諾。「什麼都不存」要改成「存在你的裝置上,我們讀不到」,兩者是不同的話,指紋示範頁與 `utils/index.md` 的說法都要跟著對齊。
## 架構
- 一站一把 32 位元組的 vault key,存成 age 檔頭的形式放在 IndexedDB:一個 passkey stanza 加一個 X25519 備援 stanza。
- 解鎖時跳一次指紋取回 vault key,只放記憶體,關分頁就消失,可加閒置逾時。
- 各小工具透過共用模組 `js/vault.js` 存取,紀錄用 vault key 以 ChaCha20-Poly1305 加密,noble 的實作已在 vendor 裡。
- RP ID 一開始就明確指定為 `anoni.net`,文件站將來搬到子網域也不會斷。
- 只在讀者按下按鈕時才碰 `navigator.credentials`,頁面載入不探測,跟指紋示範頁的承諾一致。
- 在本機檔案加密小工具裡,passkey 是第三種收件人,排在公鑰模式之後。
## 支援面
2026 年 3 月的狀況,上線前要在真機重測一輪:
| 環境 | 狀況 |
|---|---|
| macOS 15 以上的 Safari 18、Chrome 132、Firefox 139 | 支援 |
| iOS 18.4 以上 | 支援,18.0 到 18.3 有資料遺失的 bug |
| Android Chrome 配 Google 密碼管理員 | 支援,Firefox Android 不支援 |
| Windows 11 配 2026 年 2 月更新,Chrome/Edge 147、Firefox 148 | 支援,Windows 10 全部不支援 |
| 1Password、Bitwarden、Dashlane | 支援 PRF |
| Proton Pass | 未查到說明 |
| Tor Browser | 不支援 |
## 保護範圍
本機加密擋的是拿走裝置、複製瀏覽器資料夾、在共用電腦上翻資料的人,也讓站方永遠讀不到內容。它擋不了解鎖狀態下同一台機器上的惡意程式,也擋不了文件站本身被換掉 JS。後者跟站上其他小工具的信任前提相同,就是讀者信任 anoni.net 送出的程式碼。
## 不做
- 登入、帳號、伺服器端驗證。
- 用 passkey 取代密語模式。密語模式與公鑰模式維持,passkey 是第三種選擇。
- Tor Browser 與 onion 的支援,等上游開放 WebAuthn 再評估。
## 測試
- vault 檔頭的包與解包:用 Node 內建的 HMAC 模擬 PRF 輸出,驗證 stanza 的形狀與 typage 一致,X25519 備援 stanza 用獨立實作解得開。
- 原始碼掃描:vault key 與 PRF 輸出不進 storage、不進網路、不進分析事件。
- WebAuthn 本身沒辦法在 CI 執行,用 DOM 替身照 `check_qrstream_ui.mjs` 的做法走完建立、解鎖、匯出、清除四條路。
- vendor 檔案清單、import map 與 `offline_assets` 的三邊比對沿用 `test_agecrypt.mjs`。
## 先後順序
1. #421 的公鑰模式。
2. 本機檔案加密小工具加 passkey 收件人,含強制備援與匯出。
3. `js/vault.js` 共用模組與第一個使用它的功能。vault 要裝什麼還沒定,威脅模型清單的答案、筆記、讀者自己的檔案都是候選,決定後再拆子任務。
## 相關
- #421 本機檔案加密採 age 格式
- #424 密語模式與「什麼是 age」介紹頁
- typage README 與 `webauthn.js`:
- PRF 支援面整理:
- Tor Browser 的 WebAuthn 追蹤:
🤖 Generated with [Claude Code](https://claude.com/claude-code)
コントリビューションガイド
調査の方向性
Start by reading #421 and #424, then inspect utils/vendor/age/age-encryption/dist/webauthn.js, js/vault.js, and the existing offline asset checks. Run test_agecrypt.mjs and follow the check_qrstream_ui.mjs DOM-substitution approach to understand the stated test paths. Done requires all four listed prerequisites, the passkey file-tool flow, shared vault integration, aligned wording, and the listed tests and scans.
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- javascript
- 領域
- cryptography, documentation, frontend, security
- issue の種類
- 機能追加
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 活発さ
- 活発
- 明瞭さ
- 説明が足りない
- 初心者へのやさしさ
- 30/100