Skip to main content

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.

On Android

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:

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()
}
}

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:

  1. Take the entropy: 32 bytes for 24 words, 16 bytes for 12 words.
  2. Append the first entropy bits ÷ 32 bits of SHA-256(entropy) as a checksum (8 bits for 24 words, 4 bits for 12).
  3. 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:

entropy 0x00…00 (32 bytes) → 24 words
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​

ValueSource
Recovery phrase entropySecRandomCopyBytes, 16 or 32 bytes
Vault salt (PBKDF2)SecRandomCopyBytes, 32 bytes, new for every encryption
Device keySecRandomCopyBytes, 32 bytes, one per wallet
AES-GCM noncesCryptoKit, 96 bits, new for every encryption
crypto.getRandomValues() inside the engineBridged to SecRandomCopyBytes before the engine loads
Math.random() inside the engineReplaced with a function backed by SecRandomCopyBytes (some libraries call it for internal IDs)
Taproot (BIP340) auxiliary randomness32 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:

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});
Why no dice rolls?

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​

ValueSource
Recovery phrase entropy, vault salts, AES-GCM noncesjava.security.SecureRandom: BoringSSL's generator (through Conscrypt), seeded by the Linux kernel. Any error stops the operation; there is no fallback.
Device keyGenerated inside the Android Keystore and never exported (in StrongBox when the phone has one)
crypto.getRandomValues() and Math.random() inside the engineInstalled before the engine loads and frozen, backed by arc4random_buf from Android's C library, which the kernel seeds
SignaturesThe 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.