feat: NIP-SB — steganographic key backup#373
Closed
tlongwell-block wants to merge 17 commits intomainfrom
Closed
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.
…ped HKDF Two structural improvements to the NIP-SB spec: 1. Two-property reframe: cleanly separates cryptographic unlinkability (proven, computational — the core security property) from steganographic cover (environment-dependent, unvalidated — defense in depth). Adds adversary-class table with per-tier assessment of both properties. Adds active relay operator caveat: IP/timing correlation can group blobs at the network layer. 2. Relay-scoped HKDF: mixes the normalized relay URL into HKDF info strings for d-tags and signing keys. Same backup published to different relays now produces completely different metadata on each relay, eliminating the cross-relay durable fingerprint. Zero additional scrypt cost — only HKDF calls absorb the relay differentiation. Uses WHATWG URL Standard (parse + serialize, exclude fragment) for normalization. wss:// only, reject userinfo and query strings. Recovery UX unchanged: user provides password + pubkey + relay URL. Wrong relay URL produces d-tags that match nothing — no harm, client can silently try every relay in the user's relay list.
11 rounds of adversarial crossfire review (codex CLI + opus subagent). Final score: 9/10 APPROVE, zero critical issues. Spec (NIP-SB.md): - Injective encoding: length-prefixed password in base construction - Corrected cost claims: 1x rejection cost, accumulation resistance framing - Validate-then-select event ordering (prevents malformed-newer-duplicate DoS) - Deterministic padding via HKDF (safe partial re-publication, consistent RS parity) - Deterministic dummy payloads via HKDF from cover key - Relay-scoped derivation in Overview diagram (was missing relay_url_bytes) - Password rotation: per-relay with relay_url_bytes - Signing key retry: relay_url_bytes in retry info strings - created_at jitter: monotonic on refresh for NIP-33 compatibility - d-tag squatting mitigation: pagination to EOSE, authors-filter fallback - Cross-relay blob count correlation noted in Limitations - Network observer: traffic analysis caveat - Steganography claims: environment-dependent throughout tables - Password max length: 65535 bytes (2-byte prefix) - Multiple events per d-tag: validate all, select newest valid Tamarin model (NIP-SB.spthy): - Headline scoped to core cryptographic properties - Relay URL abstraction documented - Event-level attacks explicitly listed as not modeled - Dummy payload abstraction documented - Password compromise split-world modeling documented Demo (nip_sb_demo.py): - relay_url_bytes threaded through all HKDF calls - make_base() helper with length-prefixed password - Deterministic padding and dummy payloads - created_at field, validate-then-select recovery - Cross-relay isolation test (Phase 7b) - d-tag squatting resistance test (Phase 7c) - Malformed-newer-duplicate resistance test (Phase 7d) - Strict base64 with unpadded acceptance - Explicit simplifications disclaimer
…uirements - Comparison tables: scope linkability/deniability claims to passive-dump adversary model, reference §Adversary Classes for active operator caveats - Remove bold absolutism from prior-art table (None → No, Yes → Probabilistic) - Fix step ordering: pull H_i scrypt derivation into Step 3b before padding/RS (now Step 3c), eliminating forward reference to Step 4 - Update all downstream step references (6 occurrences) - Relay Requirements: distinguish standard access control from protocol extensions; acknowledge authorization scopes in Design Principle 7 Crossfire-reviewed by Claude Opus (8/10 APPROVE) and GPT-5.4 Codex (5/10 → 8/10 → 9/10 APPROVE across three passes). No crypto bugs found in spec, Tamarin model, or Python demo.
Collaborator
Author
|
superseded by #385 |
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 are published as
kind:30078events signed by throwaway keypairs. To recover, the user needs their password and their public key.Two distinct privacy properties
NIP-SB provides two properties that are often conflated — this spec separates them explicitly:
Cryptographic unlinkability (the security property). No field in any blob references the user's real pubkey. An attacker who obtains a blob — even one they suspect is a NIP-SB backup — cannot determine which user it belongs to and cannot link it to other blobs in the same backup set. Crucially, each password guess is bound to a specific pubkey, preventing the batch-cracking accumulation attack that NIP-49 warned about. This property holds under standard computational assumptions (pseudorandomness of scrypt/HKDF). It does not depend on cover traffic or relay population.
Steganographic cover (environment-dependent). Blobs share the same event structure as other
kind:30078application data. The degree to which they blend into ambient relay traffic depends on the relay's traffic distribution and has not been empirically validated against a statistical classifier. Steganographic cover provides defense-in-depth but the core security properties hold without it.How it works
kind:30078events signed by throwaway keypairs — shuffled, with jittered timestampsRelay-scoped derivation
Blob d-tags and signing keys are scoped to the relay they are published to — the normalized relay URL is mixed into the HKDF
infoparameter. This means:Relay URL normalization uses the WHATWG URL Standard (parse + serialize, exclude fragment).
wss://only, reject userinfo and query strings.Key properties
Adversary-class analysis
What's in this PR
crates/sprout-core/src/backup/NIP-SB.md— formal spec (~900 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 (~750 lines, all test cases pass including adversarial scenarios)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.
Adversarial review
The spec, model, and demo were hardened through multiple rounds of adversarial crossfire review (Claude Opus + GPT-5.4 Codex CLI). Final score: 9/10 APPROVE, zero critical issues. Key fixes from the review:
Tamarin verification
All 10 lemmas verified by
tamarin-prover --provein ~150 seconds. The model verifies core cryptographic properties (KDF chain, encryption, RS parity, dummy isolation) of a reduced symbolic model (N=3, P=2, D=2). It does NOT cover event-level validation, relay scoping, variable N/D, or traffic analysis — these are argued in the spec's security analysis.Demo test coverage
The Python demo exercises 12 test phases including adversarial scenarios:
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/)