Security & Threat Model
This document names what the wallet defends against, what it deliberately doesn’t, and which mitigations ship today. It tracks the in-repo docs/Threat_Model.md and the §12 + §14 sections of the wallet specification.
Protected assets
| Asset | Location | Consequence of compromise |
|---|---|---|
| BIP39 seed phrase | Only in the user’s possession (paper / hardware). Encrypted at rest in the vault. | Full loss of funds across every chain the wallet derives |
| Wallet master key | Derived from password via Argon2id; held in memory while unlocked | Decryption of the persisted vault → access to all private keys |
| Vault blob | chrome.storage.local (extension), IndexedDB (web), or encrypted file (desktop). AES-256-GCM with the master key |
Offline access to the ciphertext. Still requires the password to decrypt |
| Session master key | chrome.storage.session (extension), OS keychain (desktop, optional), or in-memory (web). Cleared on browser close / tab close |
Skips the Argon2id cost of re-unlocking. Not the raw password |
| User password | Never persisted. In memory only during unlock / sign, zeroed after use | Full access to every locked vault on the device |
| Connected-site grants | Persisted in the vault’s connectedSites collection |
Silent approval of dApp requests the user previously granted |
In scope
Browser-execution threats
- XSS against the dApp bridge. Mitigated by content-script isolation (extension) and CSP on the web app.
originis stamped by the content script. Never read from page content. Handled bypackages/extension/src/content/contentScript.jsand the bridge handler’srequireSitecheck. - Malicious same-origin scripts in the web app. The web shell’s key isolation is fundamentally weaker than the extension’s. Mitigations: short session lifetime (in-memory only; refresh = re-locked), no master key in
sessionStorage, no third-party<script>tags onwallet.xchain.io, optional extension-detect banner that recommends the extension when installed. - Compromised extension page chrome. The popup and approval window run in the extension’s origin, isolated from page content. Password never leaves the approval window.
Storage threats
- Offline attacker with the encrypted blob. Wallet is password-locked with Argon2id (calibrated to ≥ 750 ms per derivation on the device, floor 64 MiB × 3 iterations × 1 parallelism). Without the password, the blob is AES-256-GCM-protected.
- Tampering with the ciphertext. AES-GCM tag mismatch surfaces as an unlock failure. An attacker who modifies the blob cannot produce a valid plaintext that opens.
- Key recovery from
chrome.storage.session. Session key is the derived master key (32 bytes), not the password. On browser close, the session namespace is cleared by Chrome. Attackers with runtime access to the session have already won; the line isn’t held there.
Network threats
- MITM against SDK endpoints. Every shell endpoint is HTTPS-by-default. Per-chain URLs live in the
ChainDescriptorand are user-settable only via explicit Settings action. - Malicious Hub / Explorer. A lying server can never sign transactions; signing paths take destination / amount only from the user-authored Send form. For displayed data, token balances and action history are no longer taken on trust: the wallet SPV-verifies them against a quorum-signed checkpoint (see Proof verification). UTXO reads remain informational.
- Malicious Encoder. Returns a PSBT the wallet signs. Mitigation: the Send form renders a plain-English decoder summary (§30) BEFORE sign; the user sees
to,amount,assetas they typed them, even if the encoder fabricates output bytes. Known gap: byte-level cross-check of encoder PSBT vs user intent is the next iteration of the safety rail.
Proof verification (SPV)
The wallet is the first consumer of the platform’s light client (sdk.light). Instead of displaying the amount the explorer returns, it fetches a compact Merkle proof and verifies it locally against a checkpoint signed by a stake-weighted quorum of the validator federation. A compromised explorer cannot make a forged balance or a fabricated action verify; it can at most withhold a proof.
- What is verified. Token balances (Tokens rows) and history actions (
EntryRow). The native coin is never badged, it is not in the committed state tree. Verification is on by default (verifyProofssetting, opt-out in Settings). - Badges. Each row shows
verified,proof-failed, orunverified(a not-yet-checkpointed action). Verification never throws and never blocks the UI. - Fail semantics. Only a concrete proof-versus-amount contradiction badges as
proof-failed. Quorum, checkpoint, or transport problems degrade tounavailable/unverified, the convenience path raises no false alarms. - Trust root. The signer set is not taken from the explorer. It comes from the SDK’s pinned launch trust root (spec D4) and follows validator rotation forward against the committed BTC stakes root. The pinned root ships inert until launch values are filled in; see the SDK Light Client reference for the trust model and rotation behavior.
dApp-bridge threats
- Origin spoofing. Content script stamps
originfromwindow.location.originserver-side; the bridge handler refuses requests without a stamped origin. - Approval bypass. Every privileged method (
signMessage,signPsbt,signAction,signIn) routes through the approval window with explicit user confirmation. Per-origin grants (connectedSites) opt the user into “always allow” or “ask each time” per action. - Replay across origins.
signIn(Sign-In with XChain) bakes the origin and a nonce into the challenge prefix. The wallet refuses to sign a challenge whoseorigindoesn’t match the stamped one.
User-error threats
- Forgotten password. Irrecoverable without the recovery phrase. Onboarding copy states this explicitly. No “forgot password” link by design.
- Lost recovery phrase. Irrecoverable. The Create flow requires the user to acknowledge they’ve saved the phrase before persisting the vault, closing the tab at the mnemonic stage leaves no persisted wallet behind.
- Phishing domain. The extension-detect banner on the web app encourages users with the extension installed to use it (browser URL bar = trust anchor). Store-listing copy emphasizes the canonical domain.
Out of scope
- Zero-day browser sandbox escapes. If the browser is compromised, so is the wallet. Users with extreme threat models should use air-gapped PSBT-QR flows (see URI Schemes) or hardware wallets.
- Vendor firmware bugs. Hardware-signer firmware (Trezor, Ledger) is out-of-scope for the wallet’s audit; the wallet trusts the vendor’s signed firmware.
- Supply-chain attacks on vendored deps. Mitigated by
pnpm audit --prod --audit-level=highin CI + the per-dep review indocs/DEPENDENCIES.md. Reproducible builds (Level-2, see Reproducible Builds) narrow the blast radius; a verifier can prove the published artifact came from public source. - Physical access to an unlocked device. No wallet can defend against this. Mitigations: foreground auto-lock (configurable in Settings) and manual lock action.
- Raw-password attacks. Outside the wallet’s design; Argon2id raises the cost, the user’s password choice does the rest.
- Blockchain consensus. The wallet trusts the platform’s encoding / decoding logic and the underlying coin nodes’ chain selection. For displayed balances and actions it no longer trusts a single indexer: those are SPV-verified against checkpoints signed by a stake-weighted quorum of the federation (see Proof verification), so trust there bottoms out at the federation quorum rather than one server. The platform’s correctness is audited separately.
Sign-screen safety rails
Every signing surface (Send, Issue, Mint, Order, Dispenser, Dividend, Airdrop, Sweep, Stake, Delegate, Deploy, Execute, Deposit, Withdraw, Coinpay, Swap, Cross-chain, Multisig, and the remaining user-submittable actions available through the Advanced Actions form: Price, File, Broadcast, Message, List, Link, Address, Batch, Collect, Callback, Sleep, Unstake) is built on top of the same review pattern:
- The user fills a form. The form’s plain-English values are the canonical user intent.
- The action descriptor encodes the intent into an XChain ACTION string.
xchain-sdkcalls the encoder service to produce an unsigned PSBT.- The wallet re-decodes the action string with
core/src/decoder/actionDecoder.jsand renders the human-readable summary alongside the PSBT. - The user reviews the form values and the decoded summary (both must match) and confirms.
- The signer signs the PSBT.
sequenceDiagram
participant UserForm as User Form
participant ActionDescriptor as Action Descriptor
participant Encoder
participant WalletDecoder as Wallet Decoder
participant Signer
UserForm->>UserForm: user fills form, plain-English values are canonical intent
UserForm->>ActionDescriptor: form values
ActionDescriptor->>ActionDescriptor: encode intent into ACTION string
ActionDescriptor->>Encoder: ACTION string
Encoder-->>ActionDescriptor: unsigned PSBT
ActionDescriptor->>WalletDecoder: ACTION string
WalletDecoder->>WalletDecoder: re-decode ACTION string, render human-readable summary
WalletDecoder-->>UserForm: decoded summary, alongside the PSBT
UserForm->>UserForm: user reviews form values and decoded summary, both must match, confirms
UserForm->>Signer: PSBT
Signer->>Signer: sign PSBT
This means: even if the encoder is malicious and fabricates PSBT bytes, the user sees to, amount, asset, etc., reflected back from their own form input. The safety rail is user-visible: a mismatch is something a careful user can catch by eye.
The next iteration adds a byte-level cross-check that re-decodes the encoder’s PSBT and compares it to the user’s intent automatically, raising the rail from “user-visible” to “wallet-enforced”.
Audit posture
The wallet is pre-v1.0 GA at 0.333.0. Two external audits are queued before GA:
- Security audit. Cryptography (xchain-sdk MuSig2 + signEcdsa + ECPair / WIF / KDF), wallet flows (unlockWallet, signers, multisig session state machine, signMultisigPsbt path), shell IPC (extension service-worker boundary, desktop main / preload / renderer split). Audit-readiness packet at
claude/reports/specs/2026-04-24_security-audit-readiness.mdin the platform repo (gitignored). - Accessibility audit. Color contrast, focus-visible, live-region timing, keyboard traps, screen-reader walkthroughs (NVDA + JAWS + VoiceOver), reduced-motion verification, touch-target sizing per WCAG 2.5.5, forced-colors / Windows high-contrast, reflow + zoom per WCAG 1.4.10. WCAG 2.2 AA target. Audit-readiness packet at
claude/reports/specs/2026-04-24_a11y-audit-readiness.md.
Static gates run on every commit and protect against regressions in both surfaces between audits:
packages/core/scripts/a11y-audit.js: five-rule mechanical scan over every shared route + UI primitivepackages/core/scripts/extension-manifest-audit.js: 11-rule MV3 manifest auditpackages/core/scripts/repro-build-audit.js: 18-rule reproducible-build scaffolding audittest/smoke/audits/release-gates.smoke.js: release readiness check
Disclosure
Pre-GA: report security issues to security@dankest.llc. Audit findings will be coordinated with the external auditor on a 90-day responsible-disclosure window for High+ severity, with public disclosure timed to GA release notes.
Post-GA: the same channel; full disclosure policy lands with the v1.0.0 GA cut.
Copyright © 2026 Dankest, LLC
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later). See LICENSE and NOTICE for full terms.