ZenWallet is a truly Decentralized, Mobile-First Multi-Party Computation (MPC) cryptographic wallet demonstration.
Rather than relying on closed-source backend mainframes to control smartphone "dummy" nodes, ZenWallet compiles the ultra-heavy cryptographic threshold algorithms into WebAssembly (WASM). This allows your iOS or Android device's native browser to perform distributed offline key generation and transaction signatures using its own CPU without ever exposing private data!
ZenWallet runs deeply on a 2-of-3 threshold signature scheme (n=3, t=1) spanning directly across three devices via Local Wi-Fi bridging.
- 📱 Mobile Array: Two separate smart devices handle computation dynamically using
main.wasm. - 💻 Desktop Hub: Your local PC functions natively as the final
Node 1keyholder, while seamlessly acting as the transparent Wi-Fi Message Router tracking transactions.
- Zero App Download: It operates robustly as a Progressive Web App (PWA). No App Store downloads required.
- Air-Gapped Local Storage: Your mobile keyshares are stored safely inside the physical
localStorageof your smartphone. The Desktop has strictly zero visibility into your Mobile fractions! - Dynamic Multiple Wallets: Support for an endless keychain. Zen Wallet gracefully multiplexes all generated keys directly to their public Ethereum Addresses.
- Complete Disaster Recovery: A fully functioning Disaster Recovery matrix means if your Desktop's
desktop_keys.jsonfile burns to a crisp, the two Mobile Phones can completely bypass it and blindly combine remote thresholds to save your transaction! - 🔐 Passkey Authentication: Mobile devices require biometric verification (Face ID / Touch ID) via WebAuthn before every signing operation. Your keyshares are protected by hardware-backed authentication.
ZenWallet uses WebAuthn Passkeys as a biometric gatekeeper for MPC signing operations on mobile devices. This means every transaction must be explicitly authorized by the device owner through Face ID, Touch ID, or equivalent platform authenticator.
flowchart TD
subgraph Mobile["📱 Mobile Device"]
direction LR
SE["🔒 Secure Enclave<br/>(P-256 key)<br/>Face ID / Touch ID"]
WASM["⚙️ WASM Runtime<br/>MPC keyshare (secp256k1)<br/>tss-lib threshold party"]
SE -- auth gate --> WASM
end
User(("👤 User")) -- Biometric scan --> SE
WASM <-->|TSS messages| Hub["🖥️ Hub Server (HTTPS)"]
- User taps "Sign" on the mobile interface
- WebAuthn challenge is requested from the Hub server
- Biometric prompt appears — Face ID or Touch ID on the device
- Assertion is verified server-side and a short-lived auth token is issued
- Auth token is passed to WASM — the MPC engine only proceeds if the token is present
- MPC ceremony executes — the threshold signature is computed collaboratively
- Auth token is consumed — one-time use per signing operation
| Aspect | Detail |
|---|---|
| Passkey Role | Authentication gatekeeper — does NOT participate in MPC math |
| Curve | Passkey uses secp256r1 (P-256); MPC uses secp256k1 — no curve bridging needed |
| Scope | One passkey per mobile device, gates ALL wallets on that device |
| Token Lifetime | 5 minutes, single-use (consumed after each sign) |
| HTTPS Requirement | WebAuthn requires a secure context; hub serves HTTPS via self-signed TLS cert |
- Ensure you have Go 1.24+ installed.
- Install Anvil (
foundry) or have a generic local EVM chain spinning if you want mock transactions to successfully execute.
Clone the repository and run the startup script right off the bat!
cd ZenWallet
chmod +x run_demo.sh
./run_demo.shWhat this script does under the hood:
- Spins up a fresh local EVM
anvilenvironment at port8545. - Downloads standard Golang WASM execution scripts.
- Automatically transpiles
mobile_wasm.gointostatic/main.wasm. - Generates a self-signed TLS certificate (first run only).
- Compiles and launches
hub_server.goonhttps://localhost:8081.
Since WebAuthn requires HTTPS, the server uses a locally generated TLS certificate. When you first connect from your mobile device, you'll see a browser safety warning.
On your phone:
- Open
https://<YOUR_LOCAL_IP>:8081/ui/in Safari or Chrome - Tap "Advanced" → "Proceed anyway" (or equivalent)
- The page will load and WASM will initialize normally
This is only necessary once per device. The certificate is valid for 1 year.
Once your servers boot gracefully:
- Open the Dashboard: Go to
https://localhost:8081/ui/in your Desktop Browser. Accept the certificate warning. - Generate Native Keys: Click
Generate New MPC Walletfreely to add brand-new Multi-Party Wallets to your Active Selector Dropdown. - Connect Your Phones: Ensure your mobile phones are natively connected to exactly the same Local Wi-Fi as your PC. Open your iPhone or Android camera to individually scan the
Mobile 1andMobile 2QR codes. - Offline Computation: Keep an eye out for
"WASM Crypto Engine Active". Select a Wallet from the dropdown and hit Participate in Keygen to securely generate matching shards onto your phone. - Register Passkey: After keygen completes on a mobile device, you'll be prompted to register a passkey. Tap to register — this triggers Face ID / Touch ID and creates a hardware-backed credential. You can also tap the "Register Passkey" button at any time.
- Approve a Transaction: Select a wallet and tap "Sign with Desktop". Your phone will prompt Face ID / Touch ID for biometric verification. Only after successful authentication will the WASM MPC engine start computing the threshold signature.
On the mobile UI, you'll see a Passkey status badge in the info panel:
- 🔐 Registered (green) — passkey is set up, signing is enabled
⚠️ Not Registered (yellow) — you must register a passkey before you can sign
The integration of passkeys adds a biometric authentication layer that protects access to the MPC keyshares stored on each mobile device. There are three distinct architectural strategies, each with different trade-offs.
Passkey does NOT participate in MPC math — it protects access to the keyshare
This is the most pragmatic approach and fits perfectly into the existing architecture.
sequenceDiagram
participant User as 👤 User (Biometrics)
participant Passkey as 🔑 Secure Enclave (P-256)
participant WASM as ⚙️ WASM (tss-lib)
participant Hub as 🖥️ Desktop Hub
participant Chain as ⛓️ Ethereum
Note over User,Passkey: Step 1: Authentication
User->>Passkey: Face ID / Touch ID
Passkey-->>WASM: ✅ Assertion verified
Note over WASM: Step 2: Unlock Keyshare
WASM->>WASM: Auth token validated — keyshare access granted
Note over WASM,Hub: Step 3: MPC Signing (unchanged)
WASM->>Hub: TSS partial signatures
Hub->>WASM: TSS partial signatures
WASM-->>Hub: Final (R, S, V)
Hub->>Chain: broadcast signed tx
Implementation details:
mobile_wasm.go—wasmSignis gated behind an auth token set by JS after WebAuthn assertionstatic/index.html—navigator.credentials.get()triggers Face ID / Touch ID before every signhub_server.go— 6 WebAuthn endpoints handle registration/authentication ceremonies- Auth tokens are single-use and expire after 5 minutes
Key advantage: Zero changes to the MPC cryptographic layer. The TSS ceremony, the secp256k1 signing, and the Ethereum transaction flow remain completely untouched.
Mobile keyshares are encrypted at rest using hardware-backed keys derived from the device's Secure Enclave via the WebAuthn PRF extension. The encryption key is never stored — it's derived on-the-fly during biometric authentication.
| Layer | Technology | Purpose |
|---|---|---|
| Secure Enclave | Apple A-series / Android Titan M | Stores P-256 key, computes HMAC-based PRF |
| PRF Extension | WebAuthn Level 3 (built into browser) | Derives 32-byte secret from Secure Enclave |
| Key Derivation | HKDF-SHA256 | Converts PRF output → AES-256-GCM key |
| Encryption | AES-256-GCM (Web Crypto API) | Encrypts keyshare JSON before localStorage write |
No installation required — PRF is a built-in browser capability (Safari 18+, Chrome 120+).
sequenceDiagram
participant User as 👤 User
participant Browser as 🌐 Browser
participant Enclave as 🔒 Secure Enclave
participant WASM as ⚙️ WASM
participant Storage as 💾 localStorage
User->>Browser: Tap "Participate in Keygen"
Browser->>WASM: wasmKeygen()
WASM-->>Browser: keyshare JSON (via zenSaveKeyshare callback)
Browser->>Enclave: navigator.credentials.get() + PRF salt
Note over Enclave: Face ID / Touch ID
Enclave-->>Browser: PRF output (32 bytes)
Browser->>Browser: HKDF-SHA256 → AES-256-GCM key
Browser->>Browser: Encrypt keyshare JSON
Browser->>Storage: Store {enc:true, iv, ciphertext}
sequenceDiagram
participant User as 👤 User
participant Browser as 🌐 Browser
participant Enclave as 🔒 Secure Enclave
participant WASM as ⚙️ WASM
participant Storage as 💾 localStorage
User->>Browser: Tap "Sign with Desktop"
Browser->>Enclave: navigator.credentials.get() + PRF salt
Note over Enclave: Face ID / Touch ID
Enclave-->>Browser: Assertion + PRF output
Browser->>Browser: HKDF-SHA256 → AES-256-GCM key
Browser->>Storage: Read encrypted keyshare
Browser->>Browser: AES-GCM decrypt → plaintext keyshare
Browser->>WASM: wasmLoadDecryptedKeys(addr, json)
Browser->>WASM: wasmSetAuthToken(token)
Browser->>WASM: wasmSign(peer, addr)
WASM->>WASM: MPC ceremony with plaintext keyshare (in memory only)
When the PRF extension is not supported (older browsers/authenticators), ZenWallet falls back to plaintext localStorage storage — the same behavior as before. The passkey badge on the mobile UI indicates the encryption status:
- 🔒 Registered (Encrypted) — keyshares are encrypted with hardware-backed key
- 🔐 Registered — passkey works, but PRF not supported (plaintext storage)
⚠️ Not Registered — no passkey, no encryption
To protect the server-side architecture, the Desktop Hub supports native Hardware Security Module (HSM) integration via the PKCS#11 standard. When enabled, desktop keyshares are never stored on disk in plaintext and are never exposed during rest.
| Layer | Architecture |
|---|---|
| Storage at Rest | Desktop keyshares are encrypted via AES-256-GCM. The Key Encryption Key (KEK) is generated and resides permanently inside the HSM. |
| Runtime Decryption | Keyshares are decrypted dynamically only in memory during a signing ceremony, then aggressively zeroed out. |
| Entropy (TRNG) | The Distributed Key Generation (DKG) PreParams ceremony relies on the HSM's True Random Number Generator for maximum cryptographic entropy. |
For local development, we use SoftHSM2, a software emulation of a cryptographic token.
# Provide the --hsm flag to the demo script
./run_demo.sh --hsmThe script will:
- Initialize a
zenwalletSoftHSM token if it doesn't exist - Compile the Desktop Hub using
//go:build hsmtags (which linkscrypto11andpkcs11) - Interactively prompt you for the HSM PIN (
1234by default) - Encrypt all newly generated keyshares into
.encciphertext files instead of.json.
Warning
Disaster Recovery (HA): Because keyshares are securely bound to the HSM, losing the HSM configuration or KEK results in permanent loss of the Desktop keyshare. For production, organizations must perform standard HSM Key Export Ceremonies to securely backup the KEK to redundant geographic regions. (Note: because ZenWallet uses a 2-of-3 scheme, the two mobile clients can still independently recover the wallet even if the Desktop HSM is physically destroyed).