feat: NIP-SB — steganographic key backup#373
Open
tlongwell-block wants to merge 14 commits intomainfrom
Open
Conversation
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
bf78649 to
85f3914
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:30078events — 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
kind:30078events signed by throwaway keypairs — shuffled, with jittered timestampsEach 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
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.rs—KIND_APP_SPECIFIC_DATA = 30078crates/sprout-relay/src/handlers/ingest.rs— kind:30078 in relay allowlistNo 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 --provein ~150 seconds: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
crates/sprout-core/src/pairing/)