Seed generation and randomness
A wallet is only as strong as the randomness that created it. BitTool Signer takes all of its randomness from the operating system's cryptographically secure generator and never from JavaScript's built-in pseudo-random functions.
This page describes the iPhone app. BitTool Signer for Android takes its randomness from Android's equivalents, listed in On Android.
The entropy source
New recovery phrases start as random bytes from Apple's SecRandomCopyBytes(kSecRandomDefault, …), the system CSPRNG that iOS seeds from hardware entropy. The call is made in native Swift code:
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()
}
}
If the generator reports any error, wallet creation stops with an error. There is no fallback to a weaker source.
From entropy to words
The app offers 24 words (the default, 256 bits of entropy) or 12 words (128 bits). The bytes are turned into words with the standard BIP39 algorithm:
- Take the entropy: 32 bytes for 24 words, 16 bytes for 12 words.
- Append the first
entropy bits ÷ 32bits ofSHA-256(entropy)as a checksum (8 bits for 24 words, 4 bits for 12). - Split the result into 11-bit groups. Each group is an index into the 2,048-word English list.
The conversion runs in the bundled engine (@scure/bip39), which receives the entropy from native code as 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 };
}
New wallets always use the English wordlist, which every wallet supports. Phrases in the other nine BIP39 languages can be imported.
For example, 32 zero bytes (never use this) give:
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art
Phrase to seed
The 64-byte BIP39 seed is computed natively with CommonCrypto, not in JavaScript, because the engine runs without a JIT on iOS and PBKDF2 there takes most of a second:
seed = PBKDF2-HMAC-SHA512(
password = NFKD(phrase words joined by single spaces),
salt = NFKD("mnemonic" + passphrase),
iterations = 2048,
length = 64 bytes)
The engine receives only the 64-byte seed (as hex) for derivation and signing, never the phrase, and the native buffers are overwritten with memset_s after use. The BIP39 passphrase page shows the seed for the test phrase with and without a passphrase.
Every other random value
| Value | Source |
|---|---|
| Recovery phrase entropy | SecRandomCopyBytes, 16 or 32 bytes |
| Vault salt (PBKDF2) | SecRandomCopyBytes, 32 bytes, new for every encryption |
| Device key | SecRandomCopyBytes, 32 bytes, one per wallet |
| AES-GCM nonces | CryptoKit, 96 bits, new for every encryption |
crypto.getRandomValues() inside the engine | Bridged to SecRandomCopyBytes before the engine loads |
Math.random() inside the engine | Replaced with a function backed by SecRandomCopyBytes (some libraries call it for internal IDs) |
| Taproot (BIP340) auxiliary randomness | 32 fresh random bytes per signature |
| ECDSA nonces (Bitcoin, EVM, TRON) | Deterministic, RFC 6979 |
| Ed25519 (Solana) | Deterministic by design (RFC 8032) |
The engine's crypto object is installed before any library code runs and is frozen, so a dependency cannot replace it:
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});
Some signers let users mix in their own entropy (dice or coin flips). BitTool Signer does not: human-generated entropy is easy to get wrong, and iOS's generator is well studied. If a user wants their own entropy, they can generate a phrase elsewhere and import it.
On Android
| Value | Source |
|---|---|
| Recovery phrase entropy, vault salts, AES-GCM nonces | java.security.SecureRandom: BoringSSL's generator (through Conscrypt), seeded by the Linux kernel. Any error stops the operation; there is no fallback. |
| Device key | Generated inside the Android Keystore and never exported (in StrongBox when the phone has one) |
crypto.getRandomValues() and Math.random() inside the engine | Installed before the engine loads and frozen, backed by arc4random_buf from Android's C library, which the kernel seeds |
| Signatures | The same engine as on iPhone: RFC 6979 for ECDSA, RFC 8032 for Ed25519, fresh auxiliary randomness for Taproot |
The phrase-to-seed step is the same BIP39 PBKDF2-HMAC-SHA512 computation, done by the app outside the engine, which again receives only the seed.
What the security reviews found
Both security reviews of version 1.0.0 traced this path and confirmed it: entropy comes only from SecRandomCopyBytes, an error status stops wallet creation, there is no time-based or other weak fallback, and the engine's crypto object and Math.random are installed before any library code and cannot be replaced. The GPT-6 Astra report adds a fair caveat: this is a cryptographically secure generator under the assumption that iOS and the device are trustworthy, not a certified stream of physical randomness, and a phrase imported from elsewhere is only as random as the wallet that generated it.
How it is tested
The native test suite checks the bounds of the entropy function, a PBKDF2 vector generated independently with Python, and the published BIP39 vectors through the real JavaScriptCore bridge. The engine tests check that seed derivation from the phrase and from a native seed give the same keys.