Skip to content

EOA wallets — client mode

The default Sigil wallet is a plain EOA (externally-owned account) whose private key is split into three shares with 2-of-3 Shamir Secret Sharing over GF(256). Reconstruction only happens inside the user’s browser, inside the wallet iframe, for the milliseconds it takes to sign one message. Sigil — and your servers — never see a plaintext key.

This is the right choice for retail / consumer apps where the user should feel they own their keys.

The three shares

ShareWhere it livesWho controls it
DeviceBrowser localStorage, namespaced by wallet idThe end user
ProviderSigil backend, envelope-encrypted with Cloud KMSSigil
RecoveryYour backend (webhook) or Sigil under a separate KEKYou (or Sigil)

Two of any of these suffice to rebuild the key. One share alone is information-theoretically useless — stealing one share reveals zero bits about the private key.

What happens at wallet creation

  1. The iframe asks Sigil to authenticate the user (Sigil-hosted OTP or your OIDC).
  2. The iframe generates a fresh keypair with the browser’s CSPRNG.
  3. The iframe splits the private key into three Shamir shares.
  4. The private key buffer is wiped from memory.
  5. The provider share is PUT to Sigil (encrypted at rest with Cloud KMS, AAD bound to wallet_id).
  6. The recovery share is POSTed to your webhook (or stored in Sigil under a distinct KEK).
  7. The device share is written to localStorage under a key namespaced by (organization_id, user_identity_id, wallet_id).

The SDK exposes none of this — your code sees wallet.address and moves on.

What happens on every sign

Browser iframe Sigil backend
────────────── ─────────────
1. read device share (localStorage)
2. POST /v1/wallets/me/shares/provider ─── 3. KMS unwrap DEK, return
encrypted provider share
4. reconstruct private key in memory
5. sign the digest
6. wipe device + provider + private key
7. nothing — Sigil holds
encrypted provider share only

The reconstruction uses the shamir-secret-sharing TS package, imported only inside the iframe. The same try/finally block that holds the reconstructed key explicitly wipe()s every buffer that ever held secret material — including the intermediate share bytes returned by the API.

Polynomial rotation on recovery

Every recovery rotates the polynomial. Sigil generates a fresh f(x) of degree 1 with the same f(0), issues three new shares, and marks the old ones rotated. After a grace period the old rows are purged. A leaked old share becomes useless once rotation completes.

See Recovery model for the end-to-end flow.

What lives where, recapped

  • Your bundle: only the SDK + the publishable key. No secrets.
  • The iframe (wallet.sigilkeys.com): the only place the reconstructed private key ever exists. Strict CSP, no third-party scripts, no analytics, reproducible build.
  • Sigil backend: holds only the encrypted provider share + the metadata needed to authenticate the user (or proxy your OIDC).
  • Cloud KMS: holds the KEK that wraps the DEK that wraps the share. AAD binds it to (organization_id, wallet_id).
  • Your backend (default recovery model): receives a webhook with the recovery share at creation and serves it back during recovery.

Why an iframe

Browsers enforce hardware-backed isolation between origins. Embedding Sigil as a same-origin SDK would mean any XSS in your app could read the reconstructed key. By living on a separate origin (wallet.sigilkeys.com), the iframe is unreachable from your DOM — your code can only talk to it via postMessage, and Sigil verifies every message against the per-org allowlist.

Multi-network

A Sigil EOA is multi-network. Organizations enable networks (Ethereum, Bitcoin, Solana, …); Sigil maps networks to curves:

CurveNetworks
secp256k1Ethereum, Bitcoin, Tron, Cosmos
Ed25519Solana, Near, Aptos, Stellar

One keypair per curve. Addresses are derived per network from the shared public key (0x hex for Ethereum, bech32 for Bitcoin, base58 for Solana, etc.). Signatures come back in the network’s canonical format — your code just passes network: 'solana' and the SDK returns base58.

Dependencies

  • Cloud KMS (already managed by Sigil): wraps the provider share’s DEK.
  • A recovery custodian: either your backend webhook or Sigil-managed recovery. No third option.
  • The iframe origin (wallet.sigilkeys.com) added to the Content-Security-Policy of your app if you have one. Most apps don’t need to think about this — iframe embedding requires no CSP changes from the parent.

What the SDK shows you

import { Sigil } from '@sigilkeys/sdk';
const sigil = new Sigil({
organizationId: 'YOUR_ORG_ID',
publishableKey: 'pk_live_...',
iframeUrl: 'https://wallet.sigilkeys.com',
});
await sigil.init();
console.log(sigil.getAddress()); // 0xabc... (Ethereum)
console.log(sigil.getAddressForNetwork('solana')); // base58 (if enabled)

Sign as usual:

const sig = await sigil.signMessage('hello, world');

All of the SSS plumbing above runs inside the iframe; your code only ever sees the returned signature.