| Version | Supported |
|---|---|
| Desktop 0.1.x (Linux x86_64) | Yes: a fix ships as a new tagged release |
main and the unreleased daemons |
Best effort (the seeder and gateway are not released) |
| Anything older than the latest 0.1.x | No |
The desktop app has no automatic update (ADR 0017). A fix ships as a new tagged release with a new signed release notice, and users have to download and verify it themselves.
Do not open a public issue. Report it privately, either on GitHub (the Security tab, then Report a vulnerability) or by email to git@sovit.xyz, with:
- a description of the issue and its impact
- steps to reproduce, or a proof of concept
- affected commit or version
- any suggested remediation
You should get an acknowledgement within 7 days. Please allow a reasonable window for a fix to be prepared and released before disclosing publicly. Reporters are credited in the release notes unless they ask not to be.
In scope: anything in this repository, including the released builds. Out of scope: the hosting infrastructure, and issues that require an already-compromised machine or physical access. A Cashu mint that loses or withholds funds is a risk the design limits but cannot remove (T8 below), so report it only if this code makes it worse.
The design and the implementation were reviewed in-house, with AI assistance, and the findings,
fixes and residual risks are recorded in docs/security-review.md. No
independent third party has audited this code. The real-mint tests ran against two mint
implementations with a fake Lightning backend, and no real payment is on record.
What is enforced for every change, in CI on GitHub Actions:
- the adversary tests, one named test per threat row and per invariant below;
scripts/check-locked-dirs.sh, which fails any change to the locked directories that logs or imports from outside the audited libraries;scripts/check-contracts-version.sh, which fails a change to the interface contracts that does not bumpCONTRACTS_VERSION;- lockfile, registry and advisory gates, and a frozen inventory of native modules.
GitHub does not currently require a second reviewer on a pull request, including one that touches the locked directories.
These are known limits of the released app, not bugs awaiting a report:
- Relays see you. The three default relays learn your IP address and what you ask them for (ADR 0009). The app makes no private-browsing claim.
- Images can reveal your IP. With remote images off (the default), an
httpsimage is fetched only when its publisher signed its hash, and the host it comes from still sees your address. - Builds are signed by key, not by a vendor. There is no operating-system code signing and no
claim that the builds are reproducible. Integrity rests on
SHA256SUMSand a signed Nostr notice, and a signature cannot show that a release is the latest one (ADR 0017). - Electron's asar integrity check is not active on Linux. It protects only macOS and Windows builds (ADR 0017).
- The
.debinstalls a setuid helper./usr/lib/nutflix/chrome-sandboxis Chromium's standard setuid-root sandbox helper; the app refuses to run without Chromium's sandbox (ADR 0017). - The recovery phrase is not screen-capture protected on Linux. The window that shows it says so (ADR 0016).
- No NIP-05 check. The desktop app never looks up a creator's NIP-05 name, so it establishes only the signed event's public key (T9).
- Mint risk. Funds held at a mint are the mint's to lose or withhold (T8).
Bound the loss of any single interaction to a block's worth of sats, verify before you pay, and never trust a peer or a server for anything you can check yourself.
This table is the normative source for the adversary test suite
(packages/core/src/payment/__tests__/, packages/core/src/pay-protocol/__tests__/,
packages/gateway/src/auth/__tests__/). Every row has a named test. It began as section 1 of the
original build plan (docs/plan/build-plan.md, now a historical document) and is maintained here.
| # | Actor | Attack | Control | Residual loss |
|---|---|---|---|---|
| T1 | Malicious seeder | Serves wrong bytes | Hypercore Merkle proof per block, before download fires |
0 |
| T2 | Malicious seeder | Stalls | Hypercore already multi-sources; per-peer timeout, drop | Time |
| T3 | Malicious viewer | Downloads, never pays | Window (4 blocks), then stream destroy + ban | ~4 blocks of sats |
| T4 | Malicious viewer | Pays seeder, stiffs creator | Seeder requires both proof sets; creator set bound to the seeder (pay1 tag, ADR 0010) |
0 |
| T5 | Malicious viewer | Double-spends | DLEQ offline check; seen-secret check at verify (ADR 0010); async swap at mint; ban on failure | ≤ window |
| T6 | MITM | Steals proofs in flight | Noise secret-stream + P2PK lock to recipient | 0 |
| T7 | Any peer | Forged proofs | NUT-12 DLEQ against cached mint keyset | 0 |
| T8 | Mint | Rug / compromise | Small balances, creator-chosen mint, one-click melt-out, mint shown in UI | Balance at that mint |
| T9 | Impostor creator | Fake video under real title | Signed NIP-71 event; UI shows verified pubkey/NIP-05, not just display name | Reputation |
| T10 | Sybil seeders | Join topic, serve nothing | Pay-after-verify makes them unprofitable; peer reputation | Time |
| T11 | Internet | DoS seeder/gateway | Per-pubkey rate limits, max streams, OS hardening | Availability |
| T12 | npm | Compromised dep | Lockfile, npm ci --ignore-scripts, exact pins, native module audit |
Depends |
| T13 | Portal operator | Malicious JS | Open source, reproducible build, hash in signed Nostr event, SRI, CSP. Not fully fixable in a browser — say so in the UI. | Web session |
| T14 | Host | Keys read from disk/logs | Encrypted at rest, secure memory, log redaction | Keys on host |
| T15 | Content | XSS via titles/descriptions/comments | Render as text or sanitized markdown; CSP no-inline; Electron contextIsolation, no nodeIntegration, sandboxed renderer |
0 |
| T16 | Content | Malicious thumbnail/blob | Thumbnails are Blossom blobs with x hash in imeta; verify before display; images decoded in renderer only |
0 |
Two rows describe the original plan more than the released app:
- T13 is about a web portal. No web shell ships in 0.1.0 (
packages/app-webis an empty scaffold), so the row is kept for a future one. - T16 now works through ADR 0015: an image is shown only if it matches a hash its publisher
signed. If the user turns on remote images, any
httpsimage loads without that check.
Specified in docs/plan/build-plan.md §1–§3 and enforced by the adversary tests:
- Pay after verify. A viewer pays only for blocks Hypercore has emitted
downloadfor from that specific peer. Hypercore does not emitdownloadfor a block that failed its Merkle proof. - Exact amounts. A
PAYmust cover exactlyblocks × pricesplit per the video'ssplittag, with the creator's fractional share carried across PAYs on the channel (payment/split.ts, ADR 0007/0010). Underpayment and overpayment are both rejected. - Two locked sets. Every
PAYcarries a seeder proof set and a creator proof set, each P2PK-locked (NUT-11) to its recipient, each carrying DLEQ (NUT-12); the creator set is bound to the seeder it pays through. A set is empty only when its share is 0 sats. - Offline verification before
ACK. DLEQ against the cached mint keyset, P2PK target check, amount check, mint allowlist — all before the seeder acknowledges. - Window then cut.
uploaded − paidper peer may not exceed the window (default 4 blocks). Past it the seeder destroys the stream and bans the pubkey. - Ban on double-spend. A PAY re-presenting a proof the seeder already accepted is
refused at
verify, and an async swap at the mint that reports an already-spent proof bans the paying pubkey; either way the ban list is persisted. - No key or proof in a log, ever. A redaction layer sits in front of every logger.
- No browser persistence of proofs or keys. The web shell holds NIP-60 state in memory only.
Invariant 8 is about a browser shell, which does not ship in 0.1.0; it applies to any future one.
These directories are the audit surface: the code that handles money, keys and the pay/1 wire
format (MockPaymentEngine lives in packages/core/src/mocks/, outside the lock).
.github/CODEOWNERS asks the maintainer to review every change to them, and
scripts/check-locked-dirs.sh runs in CI and fails any change that makes them log anything or
import from outside the audited libraries (@cashu/cashu-ts, nostr-tools,
sodium-universal, compact-encoding, @sovit/core, and @scure/bip39 for the NUT-13
recovery phrase, ADR 0016 D4). Write the failing test first.
packages/core/src/payment/
packages/core/src/signer/
packages/core/src/pay-protocol/
packages/core/src/wallet/spend.ts
packages/core/src/wallet/seed.ts
packages/gateway/src/auth/
Hashing, signatures, blinding, DLEQ, key derivation and secure memory come from
@cashu/cashu-ts, sodium-native / sodium-universal, nostr-tools and hypercore.
No code in this project, whether a person or an AI model wrote it, implements a cryptographic
primitive. A contributor who finds that a change needs one stops and opens an issue.