金鑰儲存與身分驗證
BitTool Signer 會將每個錢包的助記詞(以及 BIP39 passphrase,如果有設定的話)存放在各自的加密檔案中。開啟檔案需要兩把獨立的金鑰:一把由錢包密碼衍生,另一把由 iOS 保存,且只存在於該裝置上。
本頁以 iPhone 版為準說明。BitTool Signer Android 版採用相同的兩層加密,並使用 Android 自身的金鑰儲存;請參閱 Android 版。
保險庫
每個錢包都是 App 私有的 Library/Application Support/BitToolSigner/ 目錄中的一個 Vaults/<UUID>.bsvault 檔案。建立流程如下:
- 以
SecRandomCopyBytes產生全新的 32 位元組 salt。 - 衍生一把 256 位元的 密碼金鑰:PBKDF2-HMAC-SHA256,600,000 次迭代(CommonCrypto)。
- 以密碼金鑰透過 AES-256-GCM 加密機密資料(助記詞 + passphrase,以 JSON 表示):全新的 96 位元 nonce、128 位元 tag。
- 再以一把隨機的 256 位元 裝置金鑰,使用 AES-256-GCM 對該密文進行第二次加密。
- 以
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly將裝置金鑰存入鑰匙圈,並關閉 iCloud 同步。如果手機未設定裝置密碼,建立錢包就會失敗;不存在任何較弱的備援方案。 - 以原子操作寫入檔案,並套用完整檔案保護(complete file protection)、僅限擁有者的權限,以及排除於備份之外的設定。
vault file
├── header (not secret, but authenticated)
│ format version 2 · KDF parameters · salt · wallet ID · name · created at
└── AES-GCM(device key,
AES-GCM(PBKDF2-SHA256(password, salt, 600 000),
{ mnemonic, passphrase }))
兩層 AES-GCM 都會將儲存的標頭位元組作為關聯資料(associated data)進行驗證,並在前面加上領域標籤(com.bittool.signer.vault.v2/<layer>)。編輯、重新編碼或重新排列任何標頭位元組,都會導致解密失敗。
錢包密碼 用於加密本機保險庫。它不是 BIP39 passphrase,也不會影響地址。規則:8–64 個字元,至少包含大寫字母、小寫字母、數字與符號中的三種,不得包含控制字元;完全依照輸入內容使用(不修剪、不正規化)。
裝置金鑰從不離開手機,且不包含在備份中,因此複製出去的保險庫檔案無法在其他地方開啟。遺失手機、刪除 App 或移除裝置密碼,都可能使保險庫無法讀取。抄寫下來的助記詞(以及 passphrase)是唯一的備份。
為什麼使用 PBKDF2 而不是 Argon2id?
Argon2id 具備記憶體困難(memory-hard)特性,作為密碼 KDF 會更強。此版本使用 CommonCrypto 的 PBKDF2(採用目前 OWASP 針對 HMAC-SHA256 建議的 600,000 次迭代),以避免加入原生的密碼學相依套件。KDF 參數存放在帶有版本號的標頭中,因此未來遷移時不需要新的檔案配置。
Opus 5 的安全審查將這項選擇評為資訊等級(BTS-OP5-03),而非弱點:由於還有以裝置金鑰加密的第二層,只複製到保險庫檔案的人根本無法開始搜尋密碼。該審查建議未來的保險庫格式改用 Argon2id,作為縱深防禦,以因應裝置金鑰也遭到入侵的情況。
密碼嘗試
失敗的密碼嘗試次數會依錢包分別記錄在一個經過加密與驗證的 .attempts 檔案中。計數會在緩慢的金鑰衍生 之前 儲存,因此強制結束 App 並不會重置計數。
| 失敗次數 | 下次嘗試前的等待時間 |
|---|---|
| 1–4 | 無 |
| 5 | 2 秒 |
| 6 | 4 秒 |
| n | min(2n−4 秒, 5 分鐘) |
輸入正確密碼會重置計數器。App 絕不會因為密碼錯誤而刪除錢包。這是應用程式層級的控制,而非硬體計數器:能夠回滾 App 檔案的人就能重置它。真正的密碼學保護來自 KDF。
無需密碼即可使用的公開資料
地址、配對 QR Code 與活動紀錄不需要密碼就能使用,因為它們只需要公鑰。建立或匯入錢包時,引擎會衍生一次 公開索引,並將它儲存在保險庫之外:
- 六個 Bitcoin 帳戶節點(BIP84、BIP86、BIP49、BIP44,以及 BIP48 native 與 nested 多重簽名)
- EVM 與 TRON 帳戶節點,以及 TRON 地址 0–99
- 三個 Solana 公鑰(請參閱衍生路徑)
公開索引檔案帶有一個以另一把隨機鑰匙圈金鑰計算的 HMAC-SHA256。如果驗證失敗(有人編輯過它,或它屬於另一個錢包),App 會將它視為不存在,並要求輸入一次密碼來重建。因此,能寫入 App 檔案的攻擊者也無法換入自己的地址。
授權
- 沒有「已解鎖」狀態,也沒有全域工作階段。每一項簽名工作都會要求輸入該錢包的密碼,顯示助記詞、變更密碼與刪除錢包也是如此。
- App Lock(App 鎖定,選用,使用 Face ID)只保護 App 的存取,從不授權簽名。開啟 App Lock 後,App 會在冷啟動時以及進入背景 120 秒後鎖定;計時所用的時鐘在手機休眠時也會持續計時。
- 當 App 進入背景時,會立即清除任何已輸入的密碼、風險確認與顯示中的助記詞。隱私遮罩會在 App 切換器中以及螢幕錄影期間遮住畫面。
記憶體衛生
BIP39 種子在原生程式碼中計算,暫存緩衝區會以 memset_s 覆寫。每個接觸機密或不受信任輸入的操作,都在 全新的 JavaScriptCore 環境 中執行,並在完成後捨棄。這縮短了機密在記憶體中的存留時間,但無論是 Swift 或 JavaScript 字串都無法保證會被抹除,因此 App 並不宣稱能完全清零。
Android 版
保險庫採用相同的兩層加密與相同的經驗證標頭。不同之處在於裝置金鑰存放的位置,以及平台如何保護這款 App。
- 密碼層。 PBKDF2-HMAC-SHA256,600,000 次迭代,在原生程式碼中計算(處理器支援時使用 ARMv8 SHA-256 指令),接著進行 AES-256-GCM 加密,每次加密都使用新的 12 位元組隨機數與新的 32 位元組鹽值。
- 裝置層。 每個錢包一把在 Android Keystore 內部產生、永遠無法匯出的 AES-GCM 金鑰,手機支援時位於 StrongBox 中。保險庫要求設定螢幕鎖定。
- 檔案。 保存在 App 的私有儲存空間中,以原子方式寫入,不納入雲端備份與裝置間轉移。
- 密碼嘗試。 與 iPhone 版一樣,在耗時的金鑰衍生之前依錢包計數;計數器損壞時會拒絕所有嘗試。
- 授權。 每一次簽名都需要錢包密碼。App 鎖(選用,指紋或臉部辨識)永遠不能授權簽名。所有視窗都不會出現在截圖、錄影與最近使用的應用程式中;在 Android 12 以上版本會隱藏其他 App 的覆蓋層;當有其他視窗遮住時,「簽名」按鈕與風險確認會拒絕點按。
- 記憶體。 機密保存在用完即關閉並抹除的容器中,即使操作被取消也是如此;每個涉及機密的操作都在全新的 QuickJS 執行環境中進行。助記詞透過 App 自帶的鍵盤輸入,任何輸入法都看不到。JVM 無法保證每個副本都被抹除,因此 App 在這裡同樣不宣稱能完全清零。
Opus 5.5 審查列出了與這一層相關的兩項低風險發現事項:在 Android 8–11 上發生 Keystore 錯誤後,用來驗證公開錢包資料的金鑰可能被重新產生(BTS-13);兩把輔助金鑰沒有檢查螢幕鎖定(BTS-14)。請參閱安全審查。