種子產生與隨機性
錢包的強度取決於建立它的隨機性。BitTool Signer 的所有隨機性都取自作業系統的密碼學安全產生器,從不使用 JavaScript 內建的偽隨機函式。
本頁以 iPhone 版為準說明。BitTool Signer Android 版使用 Android 中的對應機制取得隨機數,請參閱 Android 版。
熵的來源
新的助記詞起始於 Apple SecRandomCopyBytes(kSecRandomDefault, …) 所產生的隨機位元組,這是 iOS 以硬體熵作為種子的系統 CSPRNG。此呼叫在原生 Swift 程式碼中進行:
enum SecureRandom {
static func bytes(count: Int) throws -> Data {
guard (1...65_536).contains(count) else { throw VaultError.randomFailure }
var data = Data(count: count)
let status = data.withUnsafeMutableBytes { buffer in
guard let address = buffer.baseAddress else { return errSecAllocate }
return SecRandomCopyBytes(kSecRandomDefault, count, address)
}
guard status == errSecSuccess else { throw VaultError.randomFailure }
return data
}
static func entropyHex(wordCount: Int = 24) throws -> String {
guard [12, 15, 18, 21, 24].contains(wordCount) else { throw VaultError.invalidSecret }
return try bytes(count: wordCount / 3 * 4).map { String(format: "%02x", $0) }.joined()
}
}
如果產生器回報任何錯誤,建立錢包的流程就會以錯誤中止。不會退回使用較弱的來源。
從熵到單字
App 提供 24 個單字(預設,256 位元熵)或 12 個單字(128 位元)。這些位元組會以標準 BIP39 演算法轉換為單字:
- 取得熵:24 個單字需要 32 個位元組,12 個單字需要 16 個位元組。
- 將
SHA-256(entropy)的前entropy bits ÷ 32個位元附加在後作為校驗碼(24 個單字為 8 位元,12 個單字為 4 位元)。 - 將結果分割為每組 11 位元。每一組都是 2,048 字英文單字表中的一個索引。
轉換在內建引擎(@scure/bip39)中執行,引擎會從原生程式碼以 hex 形式接收熵:
// engine/src/keys.js
export function mnemonic(entropy) {
const b = hexToBytes(entropy, 32);
check([16, 20, 24, 28, 32].includes(b.length), 'ENTROPY', 'Entropy must contain 128–256 bits in 32-bit steps.');
return { mnemonic: entropyToMnemonic(b, wordlist), wordCount: b.length * 3 / 4 };
}
新錢包一律使用所有錢包都支援的 英文 單字表。其他九種 BIP39 語言的助記詞可以匯入。
例如,32 個零位元組(切勿使用)會產生:
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art
從助記詞到種子
64 位元組的 BIP39 種子是以 CommonCrypto 在原生程式碼中計算,而非在 JavaScript 中計算,因為引擎在 iOS 上是在沒有 JIT 的情況下執行,在那裡執行 PBKDF2 會花上將近一秒:
seed = PBKDF2-HMAC-SHA512(
password = NFKD(phrase words joined by single spaces),
salt = NFKD("mnemonic" + passphrase),
iterations = 2048,
length = 64 bytes)
引擎只會收到用於衍生與簽名的 64 位元組種子(以 hex 表示),從不接觸助記詞,而原生緩衝區在使用後會以 memset_s 覆寫。BIP39 passphrase 頁面列出了測試助記詞在使用與不使用 passphrase 時的種子。
其他所有隨機值
| 值 | 來源 |
|---|---|
| 助記詞的熵 | SecRandomCopyBytes,16 或 32 位元組 |
| 保險庫 salt(PBKDF2) | SecRandomCopyBytes,32 位元組,每次加密都重新產生 |
| 裝置金鑰 | SecRandomCopyBytes,32 位元組,每個錢包一把 |
| AES-GCM nonce | CryptoKit,96 位元,每次加密都重新產生 |
引擎內的 crypto.getRandomValues() | 在引擎載入前橋接到 SecRandomCopyBytes |
引擎內的 Math.random() | 替換為以 SecRandomCopyBytes 為基礎的函式(部分函式庫會呼叫它來產生內部 ID) |
| Taproot(BIP340)輔助隨機數 | 每次簽名使用 32 個全新的隨機位元組 |
| ECDSA nonce(Bitcoin、EVM、TRON) | 確定性,RFC 6979 |
| Ed25519(Solana) | 設計上即為確定性(RFC 8032) |
引擎的 crypto 物件會在任何函式庫程式碼執行之前安裝並凍結,因此相依套件無法替換它:
Object.defineProperty(globalThis, 'crypto', {value: Object.freeze({
getRandomValues(array) { /* fills the array from native SecRandomCopyBytes */ }
}), writable: false, configurable: false});
Object.defineProperty(Math, 'random', {value: /* also SecRandomCopyBytes */, writable: false, configurable: false});
有些簽名器允許使用者混入自己的熵(擲骰子或擲硬幣)。BitTool Signer 不這麼做:人工產生的熵很容易出錯,而 iOS 的產生器已經過充分研究。如果使用者想使用自己的熵,可以在其他地方產生助記詞,再將它匯入。
Android 版
| 數值 | 來源 |
|---|---|
| 助記詞熵、保險庫鹽值、AES-GCM 隨機數 | java.security.SecureRandom:BoringSSL 的產生器(透過 Conscrypt),由 Linux 核心提供種子。任何錯誤都會中止操作,不存在退而求其次的來源。 |
| 裝置金鑰 | 在 Android Keystore 內部產生,永不匯出(手機支援時位於 StrongBox 中) |
引擎中的 crypto.getRandomValues() 與 Math.random() | 在引擎載入之前安裝並凍結,由 Android C 函式庫中的 arc4random_buf 提供,其種子來自核心 |
| 簽名 | 與 iPhone 版相同的引擎:ECDSA 使用 RFC 6979,Ed25519 使用 RFC 8032,Taproot 使用全新的輔助隨機數 |
從助記詞到種子的計算同樣是 BIP39 的 PBKDF2-HMAC-SHA512,由 App 在引擎之外完成,引擎同樣只接收種子。
安全審查的發現
1.0.0 版的兩次安全審查都追蹤了這條路徑並加以確認:熵只來自 SecRandomCopyBytes,錯誤狀態會中止錢包建立,沒有任何基於時間或其他較弱的備援方式,而且引擎的 crypto 物件與 Math.random 會在任何函式庫程式碼之前安裝,且無法被替換。GPT-6 Astra 報告補充了一項中肯的但書:在 iOS 與裝置值得信任的前提下,這是一個密碼學安全的產生器,而不是經過認證的物理隨機數串流;而從其他地方匯入的助記詞,其隨機性只會和產生它的錢包一樣好。
測試方式
原生測試套件會檢查熵函式的邊界、一個以 Python 獨立產生的 PBKDF2 向量,並透過真實的 JavaScriptCore 橋接檢查公開的 BIP39 向量。引擎測試則會檢查從助記詞衍生種子與從原生種子衍生,是否會得到相同的金鑰。