Looks random ≠ is random
A deterministic PRNG can produce convincing-looking bytes while remaining reproducible.
// This is a teaching model, not Coldcard source.
function xorshift32(state) {
state ^= state << 13
state ^= state >>> 17
state ^= state << 5
return state
}
function weakRandomBytes(fallbackSeed, length) {
state = fallbackSeed
output = []
for i in 0 .. length - 1:
state = xorshift32(state)
output.push(state & 0xff)
return output
}
// Six hexadecimal digits keep only 24 seed bits.
fallbackSeed = parseHex(userInput) & 0xffffff
fallbackSeed = fallbackSeed || 1
// Global toy space: 2^24 possible initial states
randomBytes = weakRandomBytes(fallbackSeed, 32)
privateKey = hex(randomBytes)
address = toyAddress(privateKey)
This matches the JavaScript below: the same seed produces the same 32 bytes, private key, and toy address. The real case study uses a different fallback generator and a constrained reseed; the security lesson is the same.
for seed in likely_seeds:
candidate = wallet_from_seed(seed)
if candidate.address == observed_address:
recovered_private_key = candidate.private_key
The attacker does not reverse the hash. They reproduce the generator and use the public address as a candidate-validation oracle.
// Use an operating-system CSPRNG
privateKey = crypto.getRandomValues(new Uint8Array(32))
A secure generator must draw fresh, unpredictable entropy. A timestamp, device identifier, counter, or short reseed is not a substitute.
A deterministic PRNG can produce convincing-looking bytes while remaining reproducible.
If the effective seed has only 24 or 32 bits, later hashing cannot expand the number of possible secrets.
An address or public key lets an attacker reject wrong candidates without seeing the private key.