跳到主要内容

种子生成与随机性

钱包的安全性取决于生成它的随机性。BitTool Signer 的所有随机性都来自操作系统的密码学安全随机数生成器,从不使用 JavaScript 内置的伪随机函数。

Android 版

本页以 iPhone 版为准进行说明。BitTool Signer Android 版使用 Android 中的对应机制获取随机数,见Android 版。

熵源​

新的助记词始于 Apple SecRandomCopyBytes(kSecRandomDefault, …) 生成的随机字节,这是 iOS 以硬件熵为种子的系统 CSPRNG。该调用在原生 Swift 代码中完成:

BitToolSigner/Core/VaultCrypto.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 算法转换为单词:

  1. 取熵:24 个单词需要 32 字节,12 个单词需要 16 字节。
  2. 取 SHA-256(entropy) 的前 entropy bits ÷ 32 位作为校验和追加在后面(24 个单词为 8 位,12 个单词为 4 位)。
  3. 将结果拆分为每 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 个零字节(切勿使用)会生成:

熵 0x00…00(32 字节)→ 24 个单词
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 nonceCryptoKit,96 位,每次加密都重新生成
引擎内的 crypto.getRandomValues()在引擎加载前桥接到 SecRandomCopyBytes
引擎内的 Math.random()替换为由 SecRandomCopyBytes 支持的函数(有些库会调用它生成内部 ID)
Taproot(BIP340)辅助随机数每次签名使用 32 个新的随机字节
ECDSA nonce(Bitcoin、EVM、TRON)确定性生成,RFC 6979
Ed25519(Solana)按设计即为确定性(RFC 8032)

引擎的 crypto 对象在任何库代码运行之前就已安装并被冻结,因此依赖库无法替换它:

Installed by SigningEngine.swift before the bundle is evaluated
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 向量。引擎测试会检查:从助记词派生种子与从原生种子派生,得到的密钥是否一致。