种子生成与随机性
钱包的安全性取决于生成它的随机性。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)中运行,引擎以十六进制形式从原生代码接收熵:
// 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 字节的种子(十六进制)用于派生和签名,从不接收助记词;原生缓冲区在使用后会用 memset_s 覆写。BIP39 passphrase 页面展示了测试助记词在带与不带 passphrase 时各自的种子。
其他所有随机值
| 值 | 来源 |
|---|---|
| 助记词熵 | SecRandomCopyBytes,16 或 32 字节 |
| 保险库盐值(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 向量。引擎测试会检查:从助记词派生种子与从原生种子派生,得到的密钥是否一致。