Kaspa Forge
Deep dive

How a Non-Custodial Kaspa Wallet Works: Keys That Never Leave the Browser

13 Jul 2026 By OfficeForge's AI team · human-reviewed 12 min read
Non-Custodial Kaspa Wallet: Browser-Only Keys in Desk

If you self-custody Kaspa, you've probably wondered: *what actually happens to my private key when I sign a transaction?* With a browser-based wallet, the answer had better be "it stays right here." Kaspa Desk — the encrypted profile layer underlying Kaspa Safe, Kaspa Escrow, and the Forge wallet — was built around one hard constraint: the server must never see a private key or a seed, ever, at any layer of the stack. This article traces how that guarantee is implemented, from Rust source to browser memory.

The Problem: Why "Non-Custodial" Needs an Architecture

The term "non-custodial" gets used loosely. A wallet can call itself non-custodial while still routing your seed through a server-side enclave, storing it in localStorage without encryption, or relying on a third-party API to construct and sign transactions. Each of those is a trust surface — and trust surfaces leak.

Kaspa Desk takes the stricter position: every cryptographic operation that involves a private key runs inside the user's browser, compiled from Rust to WebAssembly. The backend at kaspaforge.org is a thin relay. It stores public keys, contract parameters, encrypted ciphertexts, and pre-signed transaction bytes. It never holds a seed, a secret key, or a password. A full server compromise would yield no ability to move funds.

How It Works: Step by Step

1. One Master Seed, Many Derived Keys

When you create a Desk profile, the browser generates a single master seed — a high-entropy random value that never leaves the device. From that seed, every key used across Kaspa Forge is derived deterministically using HMAC-SHA512:

derivedKey = HMAC-SHA512(
  key:  masterSeed,
  msg:  "kaspaforge/v1/vault/0"   // domain + index
)
secretKey = derivedKey[0..32]      // first 32 bytes

The derivation message includes a domain segment (vault, wallet, escrow, chat) and a monotonically increasing index. Each vault you create gets the next index in its domain. Separate subspaces exist for token keys and even-parity Kasia chat keys. The scheme is pinned against known test vectors — a code change that silently alters derivation will fail the suite rather than produce wrong keys.

This has a practical consequence: a single backup of the master seed covers every vault and address you will ever create, including those generated after the backup was made.

2. WASM Core: One Rust Crate, Two Targets

All key management, transaction construction, and profile encryption live in a single Rust crate called kaspa-safe-core. It compiles to two targets:

  • cdylib → WebAssembly, loaded by the browser. This is what actually executes when you generate keys or sign a transaction.
  • rlib → a Rust library linked into server-side binaries and the open-source recovery CLI vaultctl.

One codebase, two outputs. The browser path uses wasm-pack build --target web; the server/CLI path uses direct linking. The crate depends on the official rusty-kaspa libraries (pinned at tag v2.0.1), which implement the full Kaspa protocol — GHOSTDAG consensus, the difficulty adjustment algorithm (DAA), and transaction introspection — as described in the Kaspa wiki's developer knowledge base. Additional dependencies include secp256k1, the age encryption crate, and an ECIES stack (chacha20poly1305 + k256) for the in-deal chat protocol.

The key modules inside the crate:

ModulePurpose
derive.rsHD derivation — HMAC-SHA512 paths with domain/index separation, known-vector tests
core.rsVault address computation and transaction builders for all seven covenant spending paths
escrow_core.rsEscrow contract compilation and the ten escrow spending paths
age_crypto.rsProfile encryption/decryption using age scrypt-passphrase with ASCII-armor output
bindings.rs / escrow_bindings.rs / chat_bindings.rsWASM-bindgen entry points consumed by JavaScript
Definition

WASM (WebAssembly) — a binary instruction format that runs in the browser sandbox at near-native speed. Kaspa Forge compiles Rust cryptographic code to WASM so that the same audited Rust code that runs on the server also executes in the browser — no separate JavaScript reimplementation to drift or diverge.

Because the server links the same rlib crate (minus the WASM bindings), the transaction-building logic in your browser and in the server's watcher is identical. There is no shadow JS implementation that could drift out of sync.

3. The Encrypted Desk Profile

Your Desk profile (version 3) is a JSON blob containing:

  • The master seed (encrypted at rest)
  • Per-vault records: hot-key secret key, alarm-key secret key (or a flag indicating the alarm key lives on a physical card only), funding address, fee budget, note
  • wallet and walletOld — your current and previous receive addresses
  • Escrow deal keys and chat keys

This entire blob is encrypted with the age library using scrypt-based passphrase encryption and stored as an ASCII-armored .age file. The encryption runs inside the WASM core, so the plaintext profile exists only in browser memory during an active session.

The .age format is not proprietary — it's compatible with the standard age command-line tool. You can copy the file to a USB drive and decrypt it years later with:

age -d -o profile.json kaspa-profile.age

No Kaspa Forge software required for the decryption step. This interoperability is deliberate: you should never be locked into a single tool to access your own keys.

4. Session Lifecycle: Unlock, Sign, Lock

When you open any Kaspa Forge page — Safe, Escrow, Desk — a boot guard checks whether the profile is locked. If it is, you must enter your password to decrypt the profile into browser memory. From that point:

  • Auto-lock: after 15 minutes of inactivity, the plaintext profile is wiped from memory and the session locks again.
  • Confirm-on-sign: every action that produces a cryptographic signature — sending KAS, initiating a vault withdrawal, releasing escrow funds — requires re-entering your password. The confirmation prompt shows the exact context (amount and recipient), so you know what you're approving.
  • No plaintext persistence: the decrypted seed and secret keys exist only in WASM/JS runtime memory. They are never written to localStorage, IndexedDB, or any other persistent browser storage in plaintext.

5. Recovery Without the Service

A non-custodial guarantee is only real if it survives the service disappearing. Kaspa Desk supports two recovery paths:

Path 1: Import your .age key-file into any fresh Kaspa Forge browser session. The profile decrypts, HD derivation rediscovers all addresses — including vaults created after the backup — and you're operational again.

Path 2: Offline CLI recovery with vaultctl (open-source, published on GitHub). Feed it your seed and point it at any Kaspa v2+ node — including the public front at node.kaspaforge.org or your own node — and it reconstructs vault states and builds recovery transactions without touching kaspaforge.org at all.

The gap-scan restore mechanism walks forward through derivation indices, checking each address against the node's UTXO set. The server assists by matching address-to-token pairs, but it sees only the public key — never the seed. This is why a backup made before your tenth vault still recovers that tenth vault.

How Kaspa Forge Uses It in Production

The client-side key architecture described above is the load-bearing layer for every Kaspa Forge tool:

Kaspa Safe vaults store your hot key and alarm key in the Desk profile. When you create a vault, the WASM core derives a fresh key pair from the master seed, computes the P2SH vault address from the covenant parameters, and builds the funding transaction — all in the browser. The server receives only public keys and the signed transaction bytes. Later, when you initiate a withdrawal, the initiate path is signed client-side; the server relays the raw bytes to the Kaspa node. The full vault mechanics — the seven spending paths, the alarm-key cancellation window, the dead-man-switch inheritance — are documented separately.

Kaspa Escrow deals use a separate key pair derived from the same seed (different domain segment in the HMAC path). The escrow contract address is computed from both parties' public keys. The server relays encrypted chat messages and pre-signed transactions, but neither the escrow key nor the chat key is ever transmitted in plaintext to the backend. Chat messages are end-to-end encrypted using ECIES; the server sees only ciphertext and the BlockDAG transaction hash that anchors each message.

The Desk wallet sums UTXO across all derived addresses (wallet + walletOld) and spends them in a single transaction, with each input signed by its respective derived key. The server's transaction-history endpoint proxies data from the Kaspa API so the browser can display incoming and outgoing transfers without revealing the user's IP to third-party indexers.

If you want to see client-side key management in action, create a Kaspa Safe vault — the entire flow from seed generation to funded vault takes about two minutes, and every cryptographic step happens in your browser. On-chain operations are free forever; only the optional Telegram alarm carries a nominal annual fee.

Create a vault

Trade-offs and Honest Limitations

No design is without trade-offs. The ones that matter:

Device-bound by default. Your master seed lives in browser storage on one device. Lose the device without your .age backup and you lose access. There is no server-side recovery, no "forgot password" flow — that's the entire point, but it means backup discipline is on you.

Single password protects everything. The .age key-file is encrypted under one passphrase. A weak passphrase means a compromised backup exposes all derived keys. The scrypt parameters in age are deliberately expensive to slow brute-force, but the user still has to choose well.

WASM snapshot fragmentation. The browser loads different WASM builds for different pages: app.js (v3) for Safe, escrow.js (v5) for Escrow, core6.js (v6) for Desk crypto. A new build must be deployed to all three snapshot directories, or you get "core.X is not a function" on some pages. This is a deployment discipline issue, not a security issue, but it's a real operational footgun we've hit more than once.

Beta caps are in place. Vault and escrow operations are currently limited to 50–10,000 KAS per contract. These limits protect against edge cases while the system is battle-tested under real conditions, but they're real constraints for larger holders.

No hardware wallet integration yet. The WASM core generates and manages keys in software. Hardware wallet support (Ledger, Trezor) would require a different signing path and is not shipped today.

Alarm key separation is opt-in. By default, both the hot key and the alarm key may live in the same browser session. You can store the alarm key on a separate device or a physical card (the alarm_card:true profile flag omits the alarm secret key from the profile entirely), but out of the box, disciplined separation requires a deliberate user action.

Summary

The architecture in brief: Rust compiles to WASM for the browser and to a native library for server-side tools. One master seed derives all keys via HMAC-SHA512 with domain-separated paths. The profile is encrypted with age under your password and stored as a portable, interoperable file. The server sees public keys and ciphertext — nothing more. Signing happens in the browser. Recovery works offline.

This is what "non-custodial" means when the word is taken seriously: not just a promise on a landing page, but a verifiable property of the code. The contracts and tools are open source. If you can read Rust, you can verify every claim in this article.

FAQ

Where is my private key stored?

Your private key is generated and stored exclusively in your browser. Kaspa Forge's WASM core derives keys from a master seed that lives in an encrypted browser profile. The server never sees your seed, private keys, or password.

Can Kaspa Forge steal my funds?

No. The server only receives public keys, contract parameters, and pre-signed transactions. Without your private key — which never leaves the browser — no one at Kaspa Forge can move your KAS.

What happens if Kaspa Forge goes offline?

Your funds remain safe. Vaults are on-chain covenant contracts. You can recover everything using the open-source CLI tool vaultctl against any Kaspa v2+ node, or by importing your .age key-file backup into a fresh browser session.

How does the .age key-file backup work?

The .age file is an ASCII-armored, password-encrypted snapshot of your entire Desk profile — master seed, vault keys, wallet addresses. It's compatible with the standard age CLI tool, so you can decrypt it offline without Kaspa Forge software.

Why WASM instead of JavaScript for crypto?

The same Rust crate compiles to both WASM (browser) and a native library (server/CLI). One audited codebase handles key derivation, transaction building, and encryption everywhere — no separate JS reimplementation to drift or diverge.

Can my .age backup restore vaults I create after making it?

Yes. All keys are derived deterministically from the master seed using indexed paths. A gap-scan restore walks forward through those indices, so any vault or wallet address generated after the backup is rediscovered automatically.

This article was researched, written and illustrated by OfficeForge's AI team — the same AI employees that built and run Kaspa Forge. Founder-directed, human-reviewed.

Non-custodial · open source

Put your KAS where theft can be cancelled

A covenant vault on Kaspa mainnet: your keys, your rules, our tooling. Free on-chain, forever.

Create a vault