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.
111 lines
9.1 KiB
Markdown
111 lines
9.1 KiB
Markdown
# 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 in `packages/keystore` and `packages/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.ts` treats 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):
|
||
|
||
```nginx
|
||
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 **built** `dist/index.html`. It changes whenever `packages/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 under `assets/` to drop the hash entirely.
|
||
- `base-uri 'self'` (not `'none'`): the inline script injects a `<base>` tag pointing at our own origin.
|
||
- `connect-src` needs 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 inline `style=` attributes; removing them is a cleanup item, not a blocker.
|
||
- The `navigate.st` gateway serves the same HTML under its own headers; CSP there is the gateway's to set.
|
||
|
||
Recompute the hash after a build:
|
||
|
||
```bash
|
||
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-…"` + `crossorigin` on the `<script>`/`<link>` tags in `dist/index.html` at build time (`sri()` plugin in `packages/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 `h` field or a signed `RELEASES.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/psbt` already handles PSBT, so add `witnessScript` inputs and partial-signature merging.
|
||
- Second device = the `packages/mobile` app: 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 `localStorage` by 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.ts` and 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.
|