Skip to content

tutanch/wallet-to-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

92 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nimiq Wallet

A lightweight, fully client-side Nimiq blockchain wallet built with vanilla JavaScript. No build tools, no frameworks, no backend — just ES modules served directly from the browser, connecting to the Nimiq P2P network.

Features

  • Create or import wallets via BIP39 mnemonic (24 words)
  • Send and receive NIM on Mainnet or Testnet
  • Passkey / biometric unlock — use fingerprint, face, or device PIN instead of a password; powered by WebAuthn PRF extension with AES-GCM encryption
  • Cross-device passkey restore — discoverable credentials let you recover your wallet on a new browser via iCloud Keychain, Google Password Manager, etc.
  • Dual authentication — wallets can have both a password and a passkey; choose which to use at sign-in
  • Lock screen — returning users are prompted to authenticate via passkey or password before accessing the wallet
  • Real-time updates — balance, block height, and transactions stream live via Nimiq's P2P network
  • Cross-origin keyguard — all key operations run in an isolated iframe on a separate origin; private keys never touch the wallet's origin
  • Encrypted key storage — private keys are encrypted with a user password or passkey-derived key and stored in the keyguard's IndexedDB (inaccessible to the wallet)
  • QR code generation for receiving addresses (native encoder, no external library)
  • Batch send — send NIM to multiple recipients at once; paste addresses or upload a CSV, preview totals, sign once, broadcast in parallel
  • Transaction history with pagination
  • Network switching between Mainnet and Testnet
  • Service worker integrity pinning — all assets are SHA-256 verified on install; tampered files are rejected
  • No hosted services — does not use Nimiq Hub, Keyguard, or any API server; connects directly to the blockchain

Architecture

The wallet uses origin separation between the wallet UI and the keyguard:

┌──────────────────────────────────────┐
│  Wallet (tutanch.github.io)          │  UI, routing, network, display
│  ─ views, router, network            │  Never sees private keys or passwords
│  ─ postMessage to keyguard iframe    │
├──────────────────────────────────────┤
│  Keyguard iframe ([ORG].github.io)   │  Separate origin = separate storage
│  ─ keyguard-app.js (UI controller)   │  Renders password prompts, mnemonic
│  ─ keyguard-worker.js (Web Worker)   │  grids, TX confirmations inside iframe
│  ─ IndexedDB (encrypted keys)        │  Keys never leave this origin
└──────────────────────────────────────┘

The wallet communicates with the keyguard exclusively via postMessage with strict origin validation on both sides. The keyguard handles all sensitive flows (wallet creation, import, signing, mnemonic export, deletion) entirely within its own origin — passwords and mnemonic words are never sent to the wallet.

File structure

index.html                Entry point, CSP, keyguard iframe, script loading
sw.js                     Generated service worker (integrity-pinned caching)
.github/workflows/
  deploy.yml              GitHub Actions workflow (replaces placeholders, deploys)
scripts/
  deploy.sh               Cross-origin deploy script (wallet + keyguard)
  generate-sw.js          Generates sw.js with SHA-256 hashes of all assets
src/
  main.js                 App init, SW registration, keyguard readiness
  router.js               Hash-based SPA router with async views
  config.js               Network configs, derivation path, NIM/luna conversion
  nimiq.js                Lazy Nimiq WASM loader (main thread, for network client)
  security-init.js        Freezes critical browser APIs before third-party scripts
  modules/
    keyguard-api.js       postMessage bridge to the keyguard iframe
    network-client.js     Nimiq network client singleton (pico sync)
    webauthn.js           WebAuthn delegation (passkey create/get for keyguard)
  views/
    welcome-view.js       Landing page with create, import, and passkey login
    create-view.js        Triggers keyguard create flow
    import-view.js        Triggers keyguard import flow
    lock-view.js          Lock screen — passkey or password unlock
    dashboard-view.js     Balance, status, recent transactions
    send-view.js          Send NIM flow (keyguard signs)
    receive-view.js       Address display + QR code
    history-view.js       Full transaction history
    settings-view.js      Network switch, biometric toggle, backup, deletion
    batch-send-view.js    Bulk TX tool: paste/upload addresses, sign once, broadcast
  lib/
    qr-encoder.js         Native QR code encoder (no external library)
  styles/
    app.css               Application styles (includes design tokens)
lib/
  nimiq-core/             Nimiq Core WASM library
public/
  favicon.svg

keyguard/                 Keyguard app (deployed to separate origin)
  index.html              Keyguard shell with strict CSP
  src/
    keyguard-app.js       Message handler, worker bridge, all UI flows
    keyguard-worker.js    Web Worker: key operations (isolated JS heap)
    styles/
      keyguard.css        Standalone keyguard styles
  lib/
    nimiq-core/           Nimiq Core WASM library (copy)
  public/
    favicon.svg

Requirements

  • A modern browser with WebAssembly and ES Module support
  • Served over HTTPS (or localhost) — required for crypto APIs and P2P connections

Running locally

Serve the project root with any static HTTP server:

# Python
python3 -m http.server 8080

# Node.js (npx)
npx serve .

Then open http://localhost:8080.

Note: For full origin separation locally, you'll need to serve the keyguard/ directory on a different port (e.g. 8081) and update the [KEYGUARD_ORIGIN] placeholders accordingly.

Deploying

One-time setup

  1. Create a free GitHub Organization for the keyguard (e.g. my-keyguard)
  2. Run the deploy script:
./scripts/deploy.sh <org-name>

This creates a <org-name>.github.io repo in the org, pushes the keyguard code with the correct [WALLET_ORIGIN], and writes a GitHub Actions workflow for the wallet with the correct [KEYGUARD_ORIGIN].

  1. Commit and push the wallet:
git add -A && git commit -m "Add deploy workflow" && git push
  1. Set the wallet repo's GitHub Pages source to GitHub Actions (Settings > Pages > Source)

That's it. Every push to main auto-deploys the wallet. The workflow replaces origin placeholders and regenerates sw.js at build time — no manual steps.

Updating the keyguard

The keyguard source lives in keyguard/ in this repo. After making changes, re-run the deploy script to push them to the org repo:

./scripts/deploy.sh

(It remembers the org name from the first run.)

Service worker integrity hashes

The GitHub Actions workflow runs node scripts/generate-sw.js automatically on every deploy. The generated sw.js contains SHA-256 hashes of every wallet asset. On install, the service worker verifies each cached file against these hashes — if any file has been tampered with on the server, the SW install fails and the previous known-good version stays active.

Security

Origin separation

The keyguard runs on a separate GitHub Pages origin ([ORG].github.io) from the wallet (tutanch.github.io). This means:

  • The wallet cannot access the keyguard's IndexedDB, DOM, or JavaScript context
  • Even if an XSS vulnerability exists in the wallet, an attacker cannot extract private keys — they live in a different origin's storage
  • Passwords are entered inside the keyguard iframe and never cross the origin boundary

The wallet only ever receives from the keyguard:

  • Wallet addresses (strings)
  • Serialized signed transactions (byte arrays)
  • Success/error confirmations

Mnemonic words are displayed inside the keyguard iframe and never sent to the wallet.

Service worker integrity pinning

On first visit, the service worker pre-caches every asset and verifies each file's SHA-256 hash. On subsequent visits:

  • Assets are served from cache (cache-first strategy)
  • If GitHub Pages is compromised and serves tampered files, the SW update fails the hash check — the install is aborted and the old known-good SW stays active
  • The only window of vulnerability is the very first visit before any SW is installed

WebAuthn / passkey security

  • PRF-based encryption — passkey-protected wallets derive a symmetric key from the WebAuthn PRF extension output and encrypt the wallet entropy with AES-GCM; the PRF key is never stored, only the credential ID and salt
  • Passkey backup store — a separate nimiq-passkey-backup IndexedDB preserves passkey metadata (credential ID, encrypted entropy, salt) even if the main wallet is deleted, enabling cross-device restore via discoverable credentials
  • Gesture delegation — since the keyguard iframe cannot trigger navigator.credentials (requires transient user activation from a real click), the wallet shows a brief overlay prompt so the user clicks in the wallet context first

Additional hardening

  • Content Security Policyscript-src limited to 'self' and 'wasm-unsafe-eval' (no 'unsafe-eval'); frame-src restricted to the keyguard origin; no CDN sources
  • No external CSS — design tokens are defined in app.css; no runtime CDN requests
  • API freezing (security-init.js) protects crypto.subtle, indexedDB, and Uint8Array.prototype.fill from prototype pollution before any other scripts load
  • DOM-safe rendering — user-facing text uses textContent / DOM APIs; the keyguard uses escHtml() for all template interpolation
  • Sandboxed iframesandbox="allow-scripts allow-same-origin allow-forms" on the keyguard iframe; allow-same-origin is safe because the origins are already different

What this wallet does NOT use

  • No Nimiq Hub (hub.nimiq.com)
  • No Nimiq Keyguard (keyguard.nimiq.com)
  • No backend API or server
  • No CDN dependencies at runtime
  • No third-party analytics or tracking

All network traffic is direct P2P WebSocket connections to the Nimiq blockchain.

Tech Stack

  • Nimiq Core (Albatross PoS, pico sync mode)
  • Native QR encoder (src/lib/qr-encoder.js, no external library)
  • WebAuthn PRF extension + WebCrypto API (AES-GCM) for passkey support
  • Vanilla JavaScript (ES modules, no bundler)

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages