DigiByte is multi-algorithm, so this brings in six hash functions: SHA256d and Scrypt in JS, and Qubit, Skein, Groestl and Odocrypt from DigiByte Core's own C sources compiled to WebAssembly (packages/p2p/native, emscripten via docker). Each header is checked against its nBits target and the network powLimit before it can enter the chain; a peer serving an invalid or future-dated header is benched. Competing branches are accepted only when they carry more accumulated work than what they replace. npm run pow:test -w @dgb-wallet/p2p verifies real mainnet headers of all six algorithms and that a tampered header fails.
9.1 KiB
DigiByte.X Wallet — Security Roadmap
Derived from D:\Dev\SilentCode\web_wallet_security_analysis.md (the
general web-wallet threat model) and mapped onto what this wallet
actually does today. Each item says where we stand and what the next
concrete step is. Ordered by the analysis' own priority: CSP first,
then key-splitting, then hardware isolation.
Last reviewed: 2026-10-02.
Where we stand against the threat model
| Threat (analysis §1–2) | Status | Notes |
|---|---|---|
| Custodial key control | ✅ N/A | Non-custodial. Seed never leaves the browser; only signed tx bytes go out. |
| Server breach exposes keys | ✅ Mitigated | Nothing key-related on the server. Encrypted blobs live only in the user's localStorage. |
| Phishing / typo-squat | ⚠️ Partial | digibyte.x is a BNS name with IP + TLS fingerprint on-chain, so wallet-aware clients pin the server. Plain browsers reaching us through navigate.st trust that gateway's TLS. |
| XSS / injected script | ✅ Mitigated | Strict CSP live on digibyte.x/web/ since 2026-09-06 (script-src 'self' + hash of the one inline script); asset tags carry SRI since 2026-09-09. |
| Malicious extensions | ⚠️ Inherent | Nothing in-page can stop an extension with page access. Mitigation is the multi-sig item below. |
| Local malware reading storage | ⚠️ Partial | Blobs are PBKDF2-210k + AES-GCM under the master password. Persistent PIN blob (100k rounds) weakens this for users who set a PIN — documented in session-auth.ts. |
| DNS spoofing | ✅ Mitigated for pinned clients | On-chain fingerprint pinning; see ops/nginx-digibyte-x.conf header comment. |
What the wallet already does (analysis §2 "Client-Side Architecture")
- Local cryptography. BIP39/BIP32 via
@dgb-wallet/core; WebCrypto PBKDF2 + AES-GCM inpackages/keystoreandpackages/web-ui/src/session-auth.ts. - Encrypted local storage. One master password per device; every wallet blob encrypted under it. Session auto-locks after 30 min idle. PIN is a shortcut with a 3-attempt budget; the master password restores it.
- Local signing. PSBT built and signed in
SendScreen.tsx; keys are derived on demand and dropped with the component. - Verified broadcast.
packages/p2p/src/broadcast.tstreats a send as done only when an uninvolved peer announces the tx back (DGB Core drops policy failures silently). Fees sized to DGB's 100 sat/vB relay floor. - Address hygiene (opt-in). Settings → Security → "Fresh addresses" rotates receive and change addresses to the first unused index. Off by default because "used" is only complete with Blockbook enabled.
Roadmap
1. Strict Content Security Policy — MANDATORY (analysis §3) — ✅ APPLIED 2026-09-06
Add to the nginx location /web/ block on the VPS (and keep ops/nginx-digibyte-x.conf in sync):
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-MYCXjyaeuO4n6g9JktOO1yXmG9rBTuGJ5Wk4GojDwig=' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.coingecko.com; img-src 'self' data:; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; object-src 'none'" always;
Notes:
- The
sha256-…value is the hash of the inline<base>-fixing script in the builtdist/index.html. It changes wheneverpackages/web-ui/index.html's inline script changes; recompute on each such change (a one-liner is in the deploy notes below) or move the script to a separate file underassets/to drop the hash entirely. base-uri 'self'(not'none'): the inline script injects a<base>tag pointing at our own origin.connect-srcneeds CoinGecko only while the price feed is opt-in enabled; everything else (../p2p/,../blockbook/) is same-origin.'unsafe-inline'for styles is needed by a handful of inlinestyle=attributes; removing them is a cleanup item, not a blocker.- The
navigate.stgateway serves the same HTML under its own headers; CSP there is the gateway's to set.
Recompute the hash after a build:
python -c "import re,hashlib,base64;h=open('packages/web-ui/dist/index.html',encoding='utf-8').read();s=re.findall(r'<script>(.*?)</script>',h,re.S)[0];print('sha256-'+base64.b64encode(hashlib.sha256(s.encode()).digest()).decode())"
Applied and verified (zero violations through both digibyte.x and the navigate.st gateway). The repo mirror is ops/nginx-digibyte-x.conf; ops/push.sh warns when the live header's hash no longer matches the built inline script.
2. Subresource integrity + reproducible bundle — ✅ SRI done 2026-09-09
- ✅
integrity="sha384-…"+crossoriginon the<script>/<link>tags indist/index.htmlat build time (sri()plugin inpackages/web-ui/vite.config.ts), so a tampered asset file fails to load even if the HTML is intact. - Publish the bundle hash alongside each deploy (e.g. in the BNS record's
hfield or a signedRELEASES.md) so a user can compare what their browser loaded against what was released.
3. Drop the remaining inline script — ❌ NOT POSSIBLE as stated
The inline script must run before any relative URL is resolved, because the navigate.st gateway injects its own <base href="/bns/digibyte.x/"> at the top of <head>; an external ./base-fix.js would itself be resolved against that wrong base and never load. The script stays inline and hashed; packages/web-ui/scripts/csp-hash.mjs prints the hash and ops/push.sh checks it on every deploy, so the recompute step is automated instead of removed.
4. Multi-signature co-signing (analysis §4) — replaces "2FA"
DigiByte supports the same script system as Bitcoin, so a 2-of-2 (or 2-of-3 with a recovery key) P2WSH multisig is available today without smart contracts.
@dgb-wallet/core: derive a second account for the co-signer and build BIP48-style (m/48'/20'/0'/2') P2WSH multisig scripts;@dgb-wallet/psbtalready handles PSBT, so addwitnessScriptinputs and partial-signature merging.- Second device = the
packages/mobileapp: it receives the PSBT (QR or the/p2p/daemon acting as a relay), shows outputs and amounts on its own screen, signs with Key B, returns the PSBT. - Browser finalizes when both signatures are present; broadcast path unchanged.
- Recovery: the seed backup for each key plus the redeem script descriptor (
wsh(multi(2,…))) — export it from Settings → Security → Wallet keys.
This is the item that neutralises the extension / XSS / local-malware residual risk: a compromised browser can leak Key A and still cannot move funds.
5. Hardware wallet bridge (analysis §5, "maximum protection")
WebUSB/WebHID to a Ledger or Trezor that carries DigiByte support. Keys never enter the page; the browser only assembles PSBTs. Same PSBT plumbing as item 4, so do it after multisig lands.
6. Browser-extension packaging (analysis §3)
Ship packages/web-ui as a Manifest V3 extension. Static code reviewed by the store, isolated origin, no gateway in the path. The runtime <base> fix and the ../p2p/ relative fetches already make the bundle origin-agnostic; the remaining work is the manifest, a fixed backend origin setting, and CSP in the manifest instead of nginx.
7. Smaller hardening items
- PIN blob lives in
localStorageby design (user requirement: PIN survives reloads). Document it in-app (done, Settings → Security) and consider a device-bound key via WebAuthn PRF when browser support settles, so the blob is useless off-device. - Master password change re-encrypts all wallets and clears the PIN (done).
- Auto-lock is 30 min idle; expose it as a setting.
- Clipboard: the copy buttons put addresses on the clipboard; never the seed. Keep it that way.
- Blockbook exposure: keep it opt-in; the Settings hint spells out that the indexer operator sees queried addresses.
- Rate limiting / error hygiene: gateway text errors and indexer 429s must never look like "no funds". Handled in
p2p-fetch.tsand the Blockbook client (parse-before-JSON, 429 backoff, small concurrency cap).
8. Header proof-of-work validation in the daemon — ✅ DONE 2026-10-02
Every header received during sync is hashed with the algorithm its version
field names and checked against its nBits target and the network powLimit
(Core's CheckProofOfWork). DigiByte is multi-algo, so this needed six hash
functions: SHA256d and Scrypt in JS (@noble/hashes), and Qubit, Skein,
Groestl and Odocrypt from DigiByte Core's own C sources compiled to
WebAssembly (packages/p2p/native, built with emscripten in docker via
build.sh). packages/p2p/src/pow.ts wraps it; npm run pow:test -w @dgb-wallet/p2p checks real mainnet headers of every algorithm and proves a
tampered header fails. Fork choice is now by accumulated work, not header
count, and a peer that serves an invalid or future-dated header is benched
for six hours. Not implemented: per-algorithm difficulty retargeting
(MultiShield) — unnecessary for a light client once work-weighted fork choice
is in place, since a forged low-difficulty chain carries less work and loses.