How the XRPL.to Wallet Protects Your Keys
Keys are generated, encrypted and used entirely inside your browser — no seed ever touches our servers. One rule shapes the whole design: the cryptographic parameters are fixed at the top. If a device is too slow to meet them, unlocking takes longer. We never scale the security down to fit the hardware.
Key generation
Entropy comes from crypto.getRandomValues() — the OS random generator via the Web Cryptography API. No Math.random(), no time-seeded fallback, anywhere. We draw 256 bits; the XRPL family seed format encodes 128 of them — the protocol's structural maximum, shared by every XRPL wallet (ripple-keypairs generateSeed takes exactly 16 bytes). Enumerating a 128-bit keyspace is not a hardware problem; it is a thermodynamics problem.
What a stored seed looks like
A seed is written to disk exactly one way: AES-256-GCM inside IndexedDB, key derived from your password with PBKDF2-HMAC-SHA-256. GCM is authenticated encryption (NIST SP 800-38D) — one flipped bit and decryption returns nothing, never a plausible-but-wrong seed. The derived key is non-extractable: the browser will not hand its raw bytes to any JavaScript, including ours. localStorage never holds a seed.
Salt and nonce are freshly random on every write — never reused. The header rides outside the ciphertext, but it can't cheat: a blob claiming fewer than 600,000 iterations is rejected before key derivation is even attempted.
The floor and the ceiling
The OWASP recommendation for PBKDF2-HMAC-SHA-256 is 600,000 iterations. That is our minimum. On first use the wallet times a probe, calibrates how many iterations fit in a 250 ms unlock, and re-checks weekly — capped at 4,000,000. A fast desktop doesn't get a faster unlock; it gets a harder key derivation for the same quarter-second.
Slow hardware waits — it doesn't get weaker crypto
Calibration only ever moves up from the floor. A low-end phone still runs all 600,000 iterations; its unlock simply takes longer than 250 ms. We price the defense against the attacker's hardware, not the slowest phone's — whoever steals your encrypted blob gets to choose their GPUs. The same policy applies to platforms: the wallet hard-requires HTTPS and crypto.subtle. A browser that can't provide them is unsupported. The bar does not move.
The Coldcard test
Three days before this article, on July 30, 2026, an attacker swept ~594 BTC (≈$38M) in about 25 minutes from Coldcard hardware wallets — devices built for exactly one job: keeping keys safe. Coinkite's advisory and the hotfix changelog put the effective seed entropy at ~40 bits on Mk3 and ~72 bits on later models — instead of 128. The flaw shipped in March 2021 and sat in production firmware for over five years. And it didn't stop at one sweep: by August 2, Galaxy Research counted three attack waves totalling 1,367 BTC (≈$88.6M) across 4,585 addresses.
The firmware is open source, so the fix commit shows the mechanism precisely. The board's hardware-TRNG file never exported the global rng_get() symbol — so the linker silently bound RNG calls to MicroPython's fallback software PRNG ("yasmarang"), compiled into every build for boards without hardware RNG. Its entire seed:
pad = *(uint32_t *)MP_HAL_UNIQUE_ID_ADDRESS ^ SysTick->VAL; n = RTC->TR; // wall-clock time d = RTC->SSR; // sub-second counter
A public chip ID, a tick counter, and the time. Nothing errored — the hardware TRNG sat unused while every seed came from an enumerable keyspace. The hotfix comment says it plainly: "this selected some PRNG code we really didnt want." The fix is the right one: the fallback is now compiled to an empty object so any reference breaks the build, and a symbol check asserts the hardware path is linked on every release.
| Coldcard, pre-hotfix | This wallet | |
|---|---|---|
| Randomness source | hardware TRNG — silently replaced by a linked-in software PRNG | OS CSPRNG via crypto.getRandomValues() |
| Fallback path | compiled into every build, waiting | none exists — strong or throw |
| Effective entropy | ~40 bits (Mk3) · ~72 bits (Mk4/Q) | 128 bits |
| How failure surfaces | silent for five years | loud — refuses to run |
This is not a hot-wallet-beats-hardware-wallet story — a Coldcard resists threats a browser never will. The lesson is narrower and harder: key generation is the single point of failure everywhere, an air gap cannot save a seed that was born weak, and no one can tell a weak seed from a strong one by looking at it. The only defense is what this page keeps repeating — the weak path must not exist, and where it cannot exist, it cannot be silently chosen.
Around the ciphertext
Failed attempts, counted in memory, localStorage and IndexedDB at once — worst case wins, so clearing one layer doesn't reset the clock.
- Cached credentials are XOR-masked with a random pad and expire after 15 minutes — plaintext passwords never sit in the heap.
- A revealed seed starts blurred and wipes itself on a countdown, on tab switch, and on unmount — and lives outside React state, invisible to DevTools.
- A portable blob format exists for exactly one flow: the explicit QR transfer between your own devices.
The transaction layer
Every trustline the platform creates sets NoRipple — an open rippling path is an actively exploited way to drain same-currency tokens without a signature. Swap safety values (DeliverMin, slippage) are computed server-side from live ledger state, and transactions are simulated before submission so a failing transaction aborts instead of burning a fee. More in Sandwich Attacks on XRPL.
What this means for you
- Your seed never leaves your device — there is nothing server-side to breach.
- Your password is the last line — the iteration floor makes each guess expensive; a strong password makes the math unwinnable.
- Back up your seed — device binding means a lost device is a lost wallet unless you exported it. The nudge in the wallet is load-bearing.
- Old hardware works, slowly — a sluggish unlock is the security budget being spent. There is no setting to lower it.