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
| Share | Where it lives | Who controls it |
|---|---|---|
| Device | Browser localStorage, namespaced by wallet id | The end user |
| Provider | Sigil backend, envelope-encrypted with Cloud KMS | Sigil |
| Recovery | Your backend (webhook) or Sigil under a separate KEK | You (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
- The iframe asks Sigil to authenticate the user (Sigil-hosted OTP or your OIDC).
- The iframe generates a fresh keypair with the browser’s CSPRNG.
- The iframe splits the private key into three Shamir shares.
- The private key buffer is wiped from memory.
- The provider share is PUT to Sigil (encrypted at rest with Cloud
KMS, AAD bound to
wallet_id). - The recovery share is POSTed to your webhook (or stored in Sigil under a distinct KEK).
- The device share is written to
localStorageunder 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 share4. reconstruct private key in memory5. sign the digest6. wipe device + provider + private key 7. nothing — Sigil holds encrypted provider share onlyThe 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:
| Curve | Networks |
|---|---|
| secp256k1 | Ethereum, Bitcoin, Tron, Cosmos |
| Ed25519 | Solana, 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.