Skip to content

Repository files navigation

Offline Export Demo

Reconstruct your full private key from Dynamic wallet key shares — entirely client-side, no server needed.

Why this exists

Dynamic uses MPC (Multi-Party Computation) with a 2-of-3 threshold signature scheme to secure wallet private keys. No single party ever holds the full key. Instead, three key shares are distributed:

  1. Server share — held by Dynamic's infrastructure
  2. Browser share — stored in the user's browser (localStorage / iframe)
  3. Backup share — backed up to a cloud provider (Google Drive or iCloud)

Any 2 of the 3 shares can reconstruct the full private key. This is the foundation of the offline export flow.

2-of-2 offline recovery (newer flow)

Dynamic's offline recovery feature mints an independent 2-of-2 share set instead of the classic 2-of-3 above:

  1. Client share — uploaded to the user's Google Drive, encrypted the same way as the classic backup share (password/environment-ID-derived AES-GCM key)
  2. Server share — delivered to the developer's webhook, encrypted to an RSA public key the developer supplied (HYBRID-RSA-AES-256: a random AES-256-GCM key encrypts the payload, and that AES key is wrapped with RSA-OAEP for the developer)

This demo also accepts this share set: load the Google Drive share and the webhook-delivered share into the two share slots, and paste the developer's RSA private key (PEM) into the "RSA Private Key" field so the tool can unwrap the webhook share. Format detection is automatic — each slot works with either share type.

Important: the two shares must come from the same key epoch. If the wallet was reshared (e.g. adding a cloud backup after the fact), an older exported share no longer lies on the current polynomial and reconstruction will fail with The private key is invalid even though both shares reference the same wallet. Re-export both shares after the latest reshare.

What this tool does

This demo takes the two user-controlled shares and reconstructs the full private key:

  1. Paste or upload the downloaded browser share (plaintext .json)
  2. Paste or upload the Google Drive / iCloud backup share (encrypted .json — find it in your Google Drive)
  3. Enter your environment ID (used to decrypt the backup if no password was set)
  4. Paste or upload the webhook-delivered server share for 2-of-2 offline recovery (or use the second share slot)
  5. Paste the RSA private key (PEM) if decrypting a 2-of-2 webhook share
  6. Click Export — the tool decrypts the backup/webhook share, combines both shares using Dynamic's MPC SDK, and displays your private key

Everything runs in the browser. No data leaves your machine.

The chain is auto-detected from the share's derivation path (SLIP-44 coin type: 60 → EVM, 501 → SVM, 784 → SUI), so there's nothing to select. A manual selector appears only as a fallback when no share carries a path.

Getting the share files

Downloaded browser share

The .json file saved during the backup setup flow:

{
  "keyShares": [{ "pubkey": {}, "secretShare": "03000000..." }],
  "derivationPath": "{\"0\":44,\"1\":501,\"2\":0,\"3\":0,\"4\":0}"
}

Google Drive backup share

  1. Go to Google Drive
  2. Search for dynamicWalletKeyShareBackup
  3. Download the .json file — it contains an encrypted share:
{
  "keyShares": ["eyJzYWx0Ijoi..."],
  "metadata": { "encryptionVersion": "v2", "hasPassword": true, ... }
}

Webhook-delivered server share (2-of-2 offline recovery)

This is the JSON payload Dynamic POSTs to the developer's webhook. It looks like:

{
  "alg": "HYBRID-RSA-AES-256",
  "kid": "dynamic_rsa_...",
  "iv": "...", "ct": "...", "tag": "...", "ek": "..."
}

Decrypting it requires the developer's RSA private key (PKCS8 PEM) — the counterpart to the RSA public key configured for the webhook.

Output format

The reconstructed key is displayed in each chain's conventional import format:

  • EVM0x-prefixed 32-byte hex (importable into MetaMask/viem)
  • SVM — base58 of the 64-byte ed25519 keypair (matches Solana wallets / the Dynamic dashboard)
  • SUI — reconstructed correctly, but shown as raw hex; the suiprivkey1... bech32 format is not implemented

How decryption works

The encrypted backup share uses AES-256-GCM with a key derived from your password (or the environment ID as fallback). Parameters default to the ENCRYPTION_VERSIONS constants for the share's version, but the file's metadata.encryption block is read when present (Argon2id work factors and the PBKDF2 iteration count) to ensure correct decryption.

Version Key derivation Parameters
v1 PBKDF2-SHA256 100,000 iterations
v2 PBKDF2-SHA256 1,000,000 iterations
v3 Argon2id 3 iterations, 64 MB memory, parallelism 2

The webhook-delivered server share uses a different scheme entirely: the AES key is wrapped with RSA-OAEP (SHA-256) to the developer's RSA public key. The tool unwraps it with the supplied RSA private key, then decrypts the payload with AES-256-GCM (ciphertext and auth tag are delivered separately and concatenated before decryption).

PBKDF2 (v1/v2) runs on the native Web Crypto API. Argon2id (v3) runs in a Web Worker via the bundled hash-wasm package — no CDN fetch, so the app works fully offline.

Running locally

This project uses pnpm.

pnpm install
pnpm dev         # http://localhost:5173

For a production build:

pnpm build
pnpm exec vite preview # http://localhost:4173

Lint:

pnpm lint

Tech stack

  • Vite + React + TypeScript — minimal client-side app
  • @dynamic-labs-wallet/browser — Dynamic's MPC SDK for key reconstruction (offlineExportKey)
  • Web Crypto API — PBKDF2 key derivation and AES-GCM decryption (native browser, no external crypto libs)
  • hash-wasm — Argon2id (v3) in a Web Worker, bundled for offline use

Security

  • All cryptographic operations happen in the browser. Nothing is sent to any server.
  • The private key is displayed on screen and can be copied to clipboard. Treat it like a password.
  • Close the tab when you're done. The key is not persisted anywhere.

Releases

Packages

Contributors

Languages