Skip to content

feat: NIP-SB — steganographic key backup#373

Open
tlongwell-block wants to merge 14 commits intomainfrom
feat/nip-sb-steganographic-key-backup
Open

feat: NIP-SB — steganographic key backup#373
tlongwell-block wants to merge 14 commits intomainfrom
feat/nip-sb-steganographic-key-backup

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

@tlongwell-block tlongwell-block commented Apr 20, 2026

NIP-SB: Steganographic Key Backup

Spec, formal verification, and protocol demo for relay-based key recovery. Companion to NIP-AB (device pairing).

What it does

Lets a user back up their private key to any relay using just a password. The backup blobs hide among normal kind:30078 events — against a passive database dump, they are computationally indistinguishable from other application data. To recover, the user needs their password and their public key.

How it works

  1. Password + pubkey → scrypt → derive N (3–16 real chunks), D (4–12 dummy blobs), encryption key, per-blob signing keys and d-tags
  2. Split the 32-byte nsec into N chunks, compute Reed-Solomon parity (P=2), generate D dummy payloads
  3. Pad each payload to constant size, encrypt with XChaCha20-Poly1305, publish all N+P+D blobs as kind:30078 events signed by throwaway keypairs — shuffled, with jittered timestamps
  4. Recovery: re-derive everything from password + pubkey, query relay for all d-tags in random order, decrypt, discard dummies, reassemble (RS erasure decode if up to 2 blobs are missing)

Each blob is signed by a different throwaway key. No blob references the user's real pubkey. The d-tags are derived from the password — you can't find the blobs without it. All blobs are the same size. Real chunks, parity blobs, and dummies are indistinguishable after encryption.

Key properties

  • No bootstrap problem — everything derives from password + pubkey, no stored salt
  • Fault tolerance — RS parity tolerates loss of up to 2 blobs
  • Count obfuscation — variable dummy blobs hide the real chunk count (total 9–30 per user)
  • Multi-target attack resistance — attacker cannot build a target list from a relay dump; each password guess is bound to one user at (N+2)× scrypt cost
  • Probabilistic deniability — backup existence undetectable without the password, against a passive dump adversary
  • No server cooperation — works with any standard Nostr relay

What's in this PR

  • crates/sprout-core/src/backup/NIP-SB.md — formal spec (~800 lines)
  • crates/sprout-core/src/backup/NIP-SB.spthy — Tamarin formal verification (10 lemmas, all verified in ~150s)
  • crates/sprout-core/src/backup/nip_sb_demo.py — protocol demo with real crypto (~700 lines, all test cases pass)
  • crates/sprout-core/src/kind.rsKIND_APP_SPECIFIC_DATA = 30078
  • crates/sprout-relay/src/handlers/ingest.rs — kind:30078 in relay allowlist

No runtime code changes beyond the kind allowlist. Spec, proof, and demo only. Implementation is a follow-up.

Tamarin verification

All 10 lemmas verified by tamarin-prover --prove in ~150 seconds:

Lemma Type Result Steps
Honest backup → recovery works exists-trace ✅ verified 16
Recovery with 1 erasure (RS) exists-trace ✅ verified 20
Recovery with 2 erasures (RS) exists-trace ✅ verified 16
nsec secret without password compromise all-traces ✅ verified 47
Password secret all-traces ✅ verified 2
Individual chunks secret all-traces ✅ verified 128
Parity blobs secret all-traces ✅ verified 716
Password compromise → nsec recoverable exists-trace ✅ verified 13
Compromise rule reachable exists-trace ✅ verified 2
KDF chain functional exists-trace ✅ verified 6

The model fixes N=3, P=2, D=2 for tractability. Unlinkability and accumulation resistance are observational-equivalence properties argued in the spec (not expressible in Tamarin's trace mode).

Prior art

Evaluated against NIP-49, BIP-38, SLIP-39, Kintsugi (arXiv:2507.21122), Apollo (arXiv:2507.19484), PASSAT (arXiv:2102.13607), Shufflecake (CCS 2023), OPAQUE/Signal SVR, and Dark Crystal. See spec §Prior Art and §Comparison to Prior Art for details.

Related

Add NIP-SB, a protocol for backing up a Nostr private key to relays
using password-derived steganographic sharding. The backup is invisible
to relay operators and attackers — chunks are indistinguishable from
normal relay data, unlinkable to each other, and unlinkable to the user.

Recovery requires only the user's password and their public key.

Includes a Tamarin formal verification model (NIP-SB.spthy) with 7
verified lemmas covering correctness, confidentiality, chunk secrecy,
and password compromise semantics.
NIP-SB.md:
- Fix base64 padding claim (56 mod 3 = 2, padding IS required)
- Temper deniability language (passive dump, not active observer)
- Add authors filter to recovery REQ query
- Add explicit nsec scalar validation to recovery step 7
- Add chunks-are-byte-slices note to Limitations

NIP-SB.spthy:
- Note ciphertext strengthening artifact (model embeds blob index)
- Clarify all-chunks-required is structural, not lemma-verified

nip_sb_demo.py:
- Protocol demo with real crypto (scrypt, HKDF-SHA256,
  XChaCha20-Poly1305 via libsodium, secp256k1)
- Simulated relay as in-memory dict
- Tests: backup, recovery, wrong password, different user same
  password, relay dump perspective, base64 padding verification
- Run with: uv run crates/sprout-core/src/backup/nip_sb_demo.py
- Add NFKC password normalization (unicodedata.normalize)
- Implement reject-and-retry for signing key scalar derivation
  (signing-key, signing-key-1, ... up to 255, matching spec §Step 4)
- Accept both padded and unpadded base64 on input (spec §Encoding)
- Clarify demo scope: crypto protocol, not Nostr event layer
@tlongwell-block tlongwell-block force-pushed the feat/nip-sb-steganographic-key-backup branch from bf78649 to 85f3914 Compare April 20, 2026 22:07
tlongwell-block and others added 11 commits April 20, 2026 18:11
Reed-Solomon erasure coding (P=2) tolerates loss of up to 2 blobs.
Variable dummy blobs (D=4-12) obscure real chunk count.
Cover key derivation keeps scrypt budget at N+6.
Random-order publication and recovery with jittered delays.
GF(2^8) test vectors and RS encode/decode vectors in spec.
Demo exercises all erasure classes end-to-end.
… NIP-AB positioning

Tamarin model now includes parity blobs, dummy blobs, cover key, and
RS erasure recovery rules (1-erasure and 2-erasure). New lemmas for
erasure correctness and parity secrecy.

Spec adds explicit three-tier adversary table (external network observer,
passive relay dump, active relay operator) with protection level per tier.

NIP-AB relationship text updated: NIP-AB is the primary backup/multi-device
mechanism; NIP-SB is the secondary break-glass fallback.
…rivacy claim

Tamarin: replace broken placeholder-based 2-erasure rule with proper
double-erasure recovery functions (rs_recover_01_fst/snd, etc.) that
take only available symbols. Add all 6 double-erasure function pairs.
Fix header to accurately describe what the model proves.

Demo: add Phase 4d test for AEAD-failure-as-erasure path. Fix
malformed-content handling to treat as erasure per spec.

Spec: tighten active-operator privacy claim — acknowledge IP/session
visibility even when blob metadata is unlinkable to Nostr identity.
Adversary table: active relay operator row now says 'may identify the
client' instead of 'cannot determine which user' — consistent with the
detailed note later in the section.

Tamarin header: erasure lemmas described as 'representative cases' with
note that other patterns are structurally symmetric but not instantiated.
Add KIND_APP_SPECIFIC_DATA (30078) to the kind registry, ingest
allowlist, and pubkey-match bypass (same pattern as NIP-59 gift wrap —
throwaway signing keys for protocol-level operations).

Live test exercises full NIP-SB v3 cycle against a running Sprout relay:
publish N+P+D blobs via WebSocket with per-blob NIP-42 auth, recover
by d-tag-only queries (no authors filter), verify byte-for-byte key
reconstruction.
Per-blob auth as each throwaway key means the pubkey match check
passes naturally. Only the kind allowlist entry is needed.
The protocol demo (nip_sb_demo.py) is the reference implementation.
The live relay test is a Sprout-specific integration test that belongs
in sprout-test-client when the real implementation lands.
tamarin-prover --prove: 10/10 lemmas verified in ~150s.
Added verification results to proof header.
Includes v3 model: parity blobs, dummy blobs, cover key,
single-erasure and double-erasure RS recovery.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant