
ColdCard’s RNG: what about your passphrase?
Table of Contents
A complement to Fabio Akita’s post on the Coinkite RNG problem (in Portuguese), aimed at people who used a BIP39 passphrase and at the question left hanging: did it protect me?
Akita did an excellent job explaining how the random number generator in some ColdCard firmwares failed, and how that lets you reconstruct a wallet’s seed by brute force. If you haven’t read it yet, read it first. This text starts where his ends.
But there’s a sentence that shows up in almost every discussion of the case and rarely comes with the right math behind it:
"Yeah, but I used a passphrase, so I’m safe."
It depends. And the "it depends" is more interesting than it sounds. I’ll show exactly what it depends on, with an experiment you can run on your own machine.
The bug, in one paragraph
The firmware started generating the seed using MicroPython’s software generator (a PRNG called Yasmarang) instead of the hardware one. A PRNG like that is deterministic: if you know the initial state, you reproduce the whole sequence. And the initial state came from very weak sources: the chip’s serial number (fixed), the clock (zeroed on a cold boot), and a counter that, by the boot instant, had only about 16 to 24 useful bits. The result is that, on a ColdCard Mk2/Mk3, the effective entropy collapsed from 2^256 to roughly 2^40. That is enumerable: a regular GPU sweeps that space in about nine days, and a farm of cards in minutes. (The Mk4/Q/Mk5 fared better, but did not escape; more on them later.)
Notice what was compromised: the entropy and therefore the mnemonic (the 24 words). Hold on to that, because it’s where the passphrase enters the story.
Where the passphrase fits in

The BIP39 passphrase, the famous "25th word", is not a word from the list. It’s free text you type, and it enters a single place in the process: the salt of the function that turns the mnemonic into a seed.
seed = PBKDF2(HMAC-SHA512,
password = mnemonic,
salt = "mnemonic" + passphrase,
2048 iterations, 64 bytes)Two things jump out of that formula. The first is that the passphrase does not go through the RNG: you type it afterward, and the generator bug never touched it. It has the entropy you gave it, no more, no less. The second is that changing the passphrase changes the whole seed. The salt changes, PBKDF2 spits out a different 64 bytes, and you land in a completely different wallet, with different addresses. That’s why the passphrase powers "hidden wallets": the same mnemonic, with different passphrases, are distinct and independent wallets.
The plot twist: it doesn’t add, it multiplies
Here’s the point almost everyone gets wrong.
In a normal setting, the mnemonic carries 128 to 256 bits of security and the passphrase is a bonus on top. You think of it as "+a few bits". It just adds.
But the attacker, to reach your addresses, has to hit the mnemonic and the passphrase at the same time. They have no way to test the passphrase in isolation: the only way to know whether a (mnemonic, passphrase) pair is right is to derive the address and check whether it holds a balance on-chain. There’s no shortcut, no oracle for the passphrase alone.
That means the two search spaces multiply:
total work = (mnemonic space) × (passphrase space)And this is where the ColdCard case gets peculiar. Since the mnemonic collapsed to ~2^40, the passphrase stopped being a bonus and became your wallet’s entire security budget. The roles flipped: the mnemonic used to hold the fort, and now it’s the passphrase, on its own.
If it’s strong, the 40 broken bits of the mnemonic don’t matter. If it’s weak, it’s the only thing between the attacker and your coins, and it was not designed for that job.
Showing it in practice
To keep it out of the abstract, I built an offline reproduction of the attack against a wallet I generate myself, without touching any real wallet (the code is at the end). The script generates a victim with the broken RNG, but with a passphrase, and then tries to crack it two ways.
This run’s victim:
secret SysTick : 165
secret passphrase : 'wallet'
public address : bc1q3nm6hrr5dn3z07j04vl9g4xemqvt9l96k0x3emAttack 1: the attacker assumes there is no passphrase
This is Akita’s attack: sweep the SysTick space and derive each wallet without a passphrase.
0 hits in 300 attempts. The passphrase BLOCKED the attack.
even at the correct SysTick (165), without a passphrase the address becomes:
bc1qaleaf0rc0hw6wzp4dpeukan4yyd6es8cs4vgxf != bc1q3nm6hrr5dn3z07j04vl9g4xemqvt9l96k0x3emLook how telling this is: even hitting the exact SysTick (that is, even having reconstructed the entire mnemonic, perfectly), the address derived without the passphrase, is a different one. The attacker walks right past. This is literally what saved many people who used a passphrase: the mass attack derives addresses without a passphrase and simply does not find the protected wallets.
Attack 2: the attacker adds a wordlist of weak passphrases
Now the attacker tries the product passphrases × SysTick, with a list of common words:
>>> KEY RECOVERED <<<
passphrase : 'wallet' (it was #5 in the wordlist)
SysTick : 165
mnemonic : define test follow embark deliver toast rally harsh napkin sleep ...
attempts : 1,366 (vs ~300 for the no-passphrase attack)
cost multiplied by : ~5x (= number of passphrases tried)The wallet fell. And the cost was exactly what the theory predicted: the number of passphrases tried times the original work. With a list of 20 words, the cost would multiply by up to 20. With a list of millions of words and rules (which any serious attacker uses), it multiplies by millions.
The point of the experiment isn’t the absolute number. It’s watching the multiplication happen. It’s the same mechanism, at the same scale, that decides whether your real wallet is safe.
The risk gradient
Now we can put numbers on your situation. Starting from the ~2^40 of the broken mnemonic (Mk2/Mk3), and using a rate measured for real: an RTX 5090 does about 1.4 million BIP39 derivations per second. (The hashcat benchmark on an RTX 5090 clocks ~4.2 million/s for PBKDF2-HMAC-SHA512 at 999 iterations; at BIP39’s 2048 iterations that drops to ~2 million/s, and deriving the key and address pulls it down to around 1.4 million.) The table shows the time for one such card, for ten, and for a farm of a thousand.
| Your passphrase | total | 1 GPU | 10 GPUs | farm (1000 GPUs) |
|---|---|---|---|---|
| none | 2^40 | ~9 days | ~22 hours | ~13 min |
| 4-digit PIN (~13 bits) | 2^53 | ~200 years | ~20 years | ~75 days |
| 1 common word (~13 to 17 bits) | 2^55 | ~1,150 years | ~80 years | ~10 months |
| 2 diceware words (~26 bits) | 2^66 | infeasible | infeasible | ~1,700 years |
| 3 diceware words (~39 bits) | 2^79 | infeasible | infeasible | infeasible |
| 4+ diceware words (~52 bits) | 2^92 | infeasible | infeasible | infeasible |
| random 128-bit | 2^168 | infeasible | infeasible | infeasible |
How to read it: each time is per batch, not per wallet. One sweep finds every wallet in that row at once (the PIN row catches all 4-digit PINs together). Without a passphrase, "every" is literal; on the other rows, "every wallet that used that kind of passphrase".
Read the shape of the table, not the loose numbers. Without a passphrase, the wallet falls in hours for anyone with a handful of cards, and that’s how the mass theft drained thousands of addresses in an afternoon. A PIN or a word resists a single machine, but not a farm, and the 1000+ BTC that vanished in this case pay for a thousand-card farm without breaking a sweat. Two genuinely random words, though, push the time past a thousand years even against that farm. That’s the boundary that matters.
This table is the worst case (Mk2/Mk3, base 2^40). On an Mk4/Q/Mk5 that did the reseed, add ~32 bits to every row. That helps a lot, but the starting point (~2^72 with no passphrase) is still on Coinkite’s "migrate anyway" list, and an Mk4 without the reseed drops back to ~2^41. The passphrase remains the same multiplier on all of them.
The important reading sits at two extremes.
If your passphrase is strong (3 or more genuinely random words), you’re fine. The 40 broken bits of the mnemonic are irrelevant next to the 39-plus bits your passphrase adds. That’s the "plus" that held everything together.
If it’s weak (a PIN, a name, a date, a word), you’re in the danger zone. Look at the farm column: months or a bit more for an attacker with a thousand cards, and that’s the kind of investment 1000+ BTC justify. On a single machine it looks comfortable, but the farm decides, not your laptop. And remember: normally that weak passphrase would be hiding behind a 128-bit mnemonic. Here there’s no mnemonic holding anything behind it.
A rule of thumb: the passphrase has to carry about 40 bits on its own, roughly 3 to 4 genuinely random words, to rebuild a safe search space. And be careful, because "memorable" is almost never "random". A song lyric, a quote, your dog’s name with the year, all of that is very low entropy for an attacker with good wordlists.
Here’s a trap worth isolating: "I invented a word that’s in no dictionary" is not the same as "random". People who crack passwords don’t only test word lists. They also test character masks (every sequence up to some length) and statistical models that generate pronounceable text, imitating how each language strings letters together. An invented word you can memorize is, by construction, pronounceable and shaped like your language, which is exactly what those models hit early. An eight-letter "brixanto" is not worth the nearly 38 bits of eight randomly drawn letters; it’s worth far less. Inventing the word gives a small edge over a dictionary one, but nowhere near enough. What counts is the process that generated the passphrase: characters actually drawn at random, by dice or a generator, not your brain’s creativity.
What’s still needed to crack it
If the scenario looks scary, it’s worth understanding why in practice it’s harder than a table suggests:
- The reproductions (Akita’s and mine) simplify the sequence of calls to the generator. A real attack still has to reproduce, bit for bit, what the firmware did at boot.
- The passphrase doesn’t leak from anywhere: it’s not on the device nor on the blockchain. The attacker can only guess it, and can only validate a guess together with a mnemonic guess. It’s a cartesian product, with no oracle.
- The cost per attempt is high on purpose: each guess requires a PBKDF2 of 2048 iterations plus deriving the keys, and BIP39 uses that slow function exactly for this. It holds with or without a passphrase, so the passphrase multiplies whatever speed you assume, whether the GPU does 1e9/s or 1e6/s.
- The newer models aren’t safe, just more expensive to crack. On the Mk4/Q/Mk5, the firmware sometimes mixed the TRNG of the two secure elements into the PRNG state (the "reseed", which Coinkite itself calls "a backup to a backup"), raising the space to ~2^72. But with two big caveats. First: the reseed didn’t always happen, and an Mk4 that didn’t do it sits at ~41 bits, as fragile as an Mk3. Second: 72 bits is well below the 128-bit target and is not "safe". It’s a gray zone, within reach of a very well-funded adversary, all the more so because the funds sit on a permanent ledger. You can attack today or years from now, when compute gets cheaper (harvest now, crack later). That’s why Coinkite itself advises migrating on those models too, unless you added at least 50 independent dice rolls when generating the seed. The passphrase dilemma, then, applies to every affected model. On the newer ones it just starts from a higher floor.
The other side: the passphrase wasn’t built for this
Here comes the part I think matters most, and that’s easy to get wrong after reading everything above.
The natural temptation is: "then I’ll set a huge, super-complicated passphrase". Don’t do that as a reaction to this bug. For two reasons.
First, because the passphrase was never a defense against an RNG failure. It was designed to protect against physical theft of the seed. If someone finds your 24 words written on paper, without the passphrase they open a wallet (the "duress wallet"), not the real one. It’s a protection of plausibility and physical access. In the ColdCard case, it worked as a defense against the RNG by accident, a happy side effect. The real fix for the bug is not the passphrase.
Second, and more dangerous: there is no "forgot my passphrase". There’s no recovery. If you type the passphrase wrong, the software doesn’t complain. It silently derives another wallet, empty, and you stare at a zero balance without understanding what happened. A passphrase too complex to reproduce with 100% fidelity, and not stored robustly, is an almost guaranteed way to lose your own funds. In this game, you’re a bigger threat to your wallet than the attacker is.
The balance, then:
- It’s anti-theft, not anti-RNG.
- Strong enough not to be guessed, but written down or memorable enough that you never lose it.
- For this bug, it buys time proportional to its own entropy. It doesn’t grant immunity.
What to do, in practice
If you had an affected ColdCard, the answer is the same regardless of how strong your passphrase is:
- Generate a new seed on a device or firmware known to be fixed, and move the funds there. The passphrase doesn’t fix a broken mnemonic; it only slows the attacker down by its own entropy.
- Honestly estimate the entropy of your old passphrase. Most people overestimate wildly. If it was a PIN, a word or a recognizable phrase, treat it as compromised and treat the migration as urgent.
- Don’t reuse the old passphrase as if it were the problem. The problem is the seed’s entropy; the passphrase is a separate control.
Reproduce it yourself
The whole experiment is offline and runs against a synthetic wallet generated on the spot. No sweeping the blockchain for other people’s funds. The code is on GitHub, at romulostorel/coldcard-rng-demo. bruteforce.py reproduces the original attack (recovering the seed via SysTick), and bruteforce_passphrase.py adds the passphrase dimension: the block from Attack 1, the measured multiplication from Attack 2, and the extrapolation table.
git clone https://github.com/romulostorel/coldcard-rng-demo
cd coldcard-rng-demo
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python bruteforce_passphrase.py --seed 7The conclusion in one line: the passphrase turned the 40 broken bits of the mnemonic into your problem, but it only truly saves you if it carries, on its own, the security the mnemonic was supposed to carry. For many people it did. For anyone who used a PIN or a word, it’s a countdown timer, not a vault.
We want to work with you. Check out our Services page!


