cryptic node's avatar
cryptic node
npub1axr4...47gn
Positive and encouraging, hopefully ✝️🫶😂 Vote with your money! BIP110.org I try my hand at comedy, but I mostly only make myself laugh and come across as a jerk 😂🤷‍♂️🫶
cryptic node's avatar
CrypticNode 1 month ago
#FridayNightThoughts Do you ever think your future self might be communicating with your present and past self through the same protocol as ESP?
cryptic node's avatar
CrypticNode 1 month ago
Nostr Enhanced Relay Dashboard should be fully complete by end of May 🫶 #goals
cryptic node's avatar
CrypticNode 1 month ago
You ever just collect bitcoin miners but let most of them collect dust because the electric utility in your area is approximately double what it is in most other places but you just keep praying that some nukes get built instead of data centers so that electricity prices could come down for the first time in 21 years? This is hyperbole but captures the vibe.
cryptic node's avatar
CrypticNode 1 month ago
Bitcoin is an intelligence test measured in sats. The only way you can fail is by dying without ever buying a sat or learning anything about it.
cryptic node's avatar
CrypticNode 1 month ago
Trying to vibe code with some actual thought and effort. Doing research first: Vaultwarden × Nostr A Bunker 47 Architecture Briefing Self-hosted remote Nostr signer built on Vaultwarden, with NIP-46 bunker transport (direct) and a browser-extension fallback. Prep reading before we start building. TL;DR — You are extending Vaultwarden (Rust, Bitwarden-API-compatible, encrypted-at-rest vault) so it can also hold Nostr private keys and sign on their behalf, over NIP-46 (Nostr Connect) using a direct WebSocket/relay transport. The nsec never leaves the server. Clients — mobile apps, web apps, and your own browser extension — become permission-scoped requestors, not key holders. What’s inside • Section 1 — The big picture: threat model, why bunker, why direct transport • Section 2 — NIP-46 protocol in the detail you actually need • Section 3 — Vaultwarden internals you’ll be touching • Section 4 — Proposed architecture & component layout • Section 5 — Key-at-rest & key-in-use security choices • Section 6 — The browser-extension fallback (NIP-07 that proxies to your bunker) • Section 7 — Build order (MVP → v1 → v2) and gotchas • Section 8 — Reading list & source links Vaultwarden × Nostr — Bunker 47 briefing Page 1 1. The big picture Why merge these two projects Every Nostr signer extension solves the same set of problems — secure secret storage, cross-device sync, unlock gating, browser integration, permission prompts. Vaultwarden already has mature, audited implementations of all of them for passwords. The nsec is, from a storage standpoint, just another secret. What makes a Nostr key different isn’t storage — it’s that it must be used to sign without ever being exposed. That’s the one new primitive you’re adding. Threat model you’re defending against • Malicious web page calling window.nostr with a poisoned event (e.g. delegation kind, or draining zaps). → Mitigated by per-request user approval with human-readable summaries. • Compromised browser extension (supply-chain attack on a dependency). → In a bunker architecture, the extension holds only a session token, not the nsec. Worst case: attacker signs while session is live; you revoke the session and your identity is intact. • Malware on the client device scraping memory. → Same mitigation: no nsec in client memory, ever. • Server compromise (your Vaultwarden host is breached). → This is the real risk. Mitigations: encrypt-at-rest with master-password-derived key, never store the derivation key on disk, optionally back the key with a TPM/HSM so even root can’t exfiltrate it. • Relay-level metadata leakage (who is signing what, when). → Ephemeral kind-24133 events are NIP-44 encrypted, but traffic analysis is still possible. Consider running your own relay for bunker traffic. Why direct transport (and what that actually means) NIP-46 carries its RPC messages as ephemeral Nostr events of kind 24133, encrypted with NIP-44, published to a relay that both sides subscribe to. ‘Direct’ in our plan means: we don’t introduce a separate side-channel (no HTTP callback, no custom WebSocket protocol). The bunker and the client both connect to a WebSocket relay and talk over kind 24133. That relay can be yours, a public one, or (cleanest) a tiny relay you ship inside the Vaultwarden container so bunker traffic never leaves the host unless a remote client needs it. Benefits: firewall-friendly (outbound WS only), works through NAT, reuses the existing Nostr ecosystem, no custom client SDK required — any NIP-46-aware app can talk to us. Vaultwarden × Nostr — Bunker 47 briefing Page 2 2. NIP-46 in the detail you need The three keypairs — memorize these • user-keypair — the actual Nostr identity. The nsec lives in Vaultwarden. The bunker signs as this key. • remote-signer-keypair — the bunker’s own identity for the RPC channel. MAY equal the user keypair but SHOULD NOT; use a distinct key so the user identity isn’t fingerprinted by connection metadata. • client-keypair — generated per-session by the app (mobile client, browser extension, etc). Ephemeral. Used only to encrypt/authenticate RPC traffic to the signer. The two connection flows Bunker-initiated (most common for our case): the bunker emits a URI the user pastes into the client. bunker://<remote-signer-pubkey>?relay=wss://your-relay&secret=<otp> Client-initiated (nostrconnect flow): the client shows a URI/QR, bunker scans/pastes it. nostrconnect://<client-pubkey>?relay=wss://your-relay&perms=sign_event:1,nip44_encrypt&secr et=<otp> Implement both — mobile apps prefer bunker:// via paste/QR; web apps prefer nostrconnect:// so the user can approve on the bunker side. The RPC wire format Every message is a kind-24133 event: { "kind": 24133, "pubkey": "<sender pubkey>", "tags": [["p", "<recipient pubkey>"]], "content": "<NIP-44 encrypted JSON-RPC payload>" } Decrypted content is JSON-RPC-ish: // request { "id": "<random>", "method": "sign_event", "params": ["<stringified event JSON>"] } // response { "id": "<same id>", "result": "<stringified signed event>", "error": "<reason or omitted>" } Methods the bunker MUST implement • connect(remote-user-pubkey, secret?, perms?) — session establishment, validate the OTP secret • get_public_key() — returns the user-pubkey (hex) • sign_event(event_json) — returns a fully signed event • ping() — liveness • nip04_encrypt / nip04_decrypt(third_party_pubkey, payload) • nip44_encrypt / nip44_decrypt(third_party_pubkey, payload) • get_relays() — user’s preferred relays (stored alongside the key) Vaultwarden × Nostr — Bunker 47 briefing Page 3 Anything else (delegation signing, payment-related kinds, DVM kinds) should be gated by explicit permission grants like sign_event:1,sign_event:4,nip44_decrypt issued at connect time. Permission model Permissions are comma-separated method[:param] tokens, e.g. nip44_encrypt,sign_event:1,sign_event:4. The bunker SHOULD support: a) approve-once, b) approve-for-session, c) approve-forever. Store grants per (client-pubkey, method, param) tuple. Expose a revoke UI in the Vaultwarden admin panel. Spec churn warning NIP-46 has gone through several revisions. Older material references NIP-04 encryption and single-key schemes. Current spec uses NIP-44 and the three-key model above. Always check the live spec at nips.nostr.com/46 before implementing a detail. The nostrconnect.org ‘gotchas’ list (in the reading section) is gold. Vaultwarden × Nostr — Bunker 47 briefing Page 4 3. Vaultwarden internals you’ll touch The lay of the land • Language: Rust, Rocket web framework, Diesel ORM • Storage: SQLite/MySQL/PostgreSQL (all three supported via Diesel) • Auth: JWT (RS256) with multiple token types • Crypto at rest: AES-256 + PBKDF2-SHA256 from the user’s master password — the server never sees plaintext vault items; they arrive already encrypted from the client. • Realtime: WebSocket notifications to clients (notifications module) • API: Bitwarden-compatible REST (so official clients work). Main surface is /api/ciphers Key source files (as of v1.35.x) src/main.rs — Rocket launch, route mounting src/auth.rs — JWT, key init src/api/core/ciphers.rs — cipher CRUD, the main vault endpoints src/db/models/cipher.rs — the Cipher struct, atype enum (5 types today) src/api/core/accounts.rs — user keys, 2FA, WebAuthn bits src/api/notifications.rs — WebSocket realtime to clients src/db/schema.rs — Diesel schema macros The cipher model — where an nsec would live Ciphers today have an atype (Login=1, SecureNote=2, Card=3, Identity=4, SSHKey=5). You have two options: (a) reuse SecureNote with a custom field convention, or (b) add atype=6 NostrKey with its own typed JSON payload. (b) is cleaner, but requires matching client support to display nicely. For the MVP, (a) works — the signing endpoint reads the nsec from a well-known custom field name regardless of UI. Important: vault items arrive at the server already encrypted with keys derived from the master password. The server cannot decrypt them without the user being logged in. This is great for passwords — terrible for a bunker that needs to sign while the user is asleep. Section 5 covers how to resolve this tension. Where to bolt on the bunker • New module: src/api/nostr/ — bunker RPC handlers, session management, permission store • New DB tables: nostr_sessions (client-pubkey, user-id, perms, expires_at), nostr_grants (session_id, method, param, policy), nostr_audit (who signed what when) • New background task: a relay client that subscribes to kind 24133 events p-tagged to your remote-signer-pubkey, dispatches them to the RPC handlers, publishes replies • Config additions: BUNKER_ENABLED, BUNKER_RELAYS, BUNKER_KEY_UNLOCK_POLICY (always-unlocked, session-unlocked, per-request) Vaultwarden × Nostr — Bunker 47 briefing Page 5 4. Proposed architecture Component Role Lives in NostrKeyStore Encrypted-at-rest nsec storage, unlock logic Vaultwarden server (Rust) RelayClient WebSocket to configured relay(s); filters kind:24133 Vaultwarden server Nip46Dispatcher Parse RPC, check perms, call signer, publish reply Vaultwarden server Signer secp256k1 Schnorr sign; NIP-44 en/decrypt Vaultwarden server SessionManager Client-pubkey → user, perms, expiry Vaultwarden server (DB) ApprovalBroker Pushes approval prompts to the user WS to web vault + push to extension AdminUI List/revoke sessions, view audit log Vaultwarden web-vault fork NostrBridge Ext NIP-07 window.nostr that proxies to bunker Browser extension (TS) Data flow, happy path 1. User adds their nsec to Vaultwarden (encrypted client-side with master-password-derived key, then additionally wrapped with a server-side bunker key — see §5). 2. User generates a bunker:// URI in the Vaultwarden web vault and pastes it into their Amethyst / Damus / web client. 3. Client posts a kind-24133 connect request to the configured relay, encrypted to remote-signer-pubkey. 4. RelayClient receives it, Nip46Dispatcher validates the OTP secret, creates a session row, publishes a connect response. 5. Client sends sign_event requests. For each, Dispatcher checks the session’s permission grant; if ‘ask’, emits an approval push to the user; if ‘approve-forever’ for that kind, signs immediately. 6. Signer pulls the nsec (unwrapping as per §5), signs, returns. Audit row written. Vaultwarden × Nostr — Bunker 47 briefing Page 6 5. Key-at-rest, key-in-use This is the thorniest design question. Vaultwarden’s password model assumes the server can’t read your secrets — they’re decrypted client-side after login. A signing bunker needs the opposite: the server must be able to decrypt the nsec to sign with it. You need a separate key-wrapping scheme for bunker-enabled items. Option A — Session-unlocked (recommended for v1) • When the user logs in to the web vault, they enter their master password → derived key decrypts an nsec-wrapping key stored alongside their user record. • Server holds the wrapping key in memory only while the user has an active session. • Bunker can sign only while some client of that user is logged in. • Good security posture, minor UX cost: if you want to sign from your phone while your laptop is off, it won’t work. Workaround: keep a long-lived session from a dedicated device. Option B — Always-unlocked (convenient, weaker) • Wrapping key derived from a bunker-specific passphrase set at bunker-enable time, stored in a file readable only by the Vaultwarden process. • Server can sign 24/7. Compromise of the server = compromise of the nsec. • Suitable for low-value identities or where you trust the host completely. Option C — Hardware-backed (endgame) • nsec encrypted with a key that lives in the host TPM, or signing is delegated to a PKCS#11 device (YubiKey HSM, SoloKey, etc.). • Even root on the server cannot exfiltrate the key — only request signatures. • Note: secp256k1 Schnorr isn’t universally supported in HSMs. You may need to store the key sealed by TPM instead of using TPM for signing directly. Research PKCS#11 + secp256k1 support before committing. Recommendation: ship Option A for MVP. Design the KeyStore trait so Options B and C slot in as alternative implementations without touching dispatcher code. Zeroization and memory hygiene • Use the zeroize crate for all structs holding secret bytes. Implement Drop. • Avoid String for secrets — use SecretVec<u8> from the secrecy crate. • Never log raw secrets, even at trace level. Redact in Debug impls. • Consider mlock/mprotect on the wrapping-key page (memsec crate) to prevent swap leakage. Vaultwarden × Nostr — Bunker 47 briefing Page 7 6. The browser-extension fallback The extension’s job: provide window.nostr (NIP-07) to web pages, so existing web apps (Iris, Snort, Coracle, nostr.band) work without needing to implement NIP-46 themselves. The extension does NOT hold your nsec. It holds a bunker session token. The NIP-07 surface is tiny window.nostr = { getPublicKey(): Promise<string>, signEvent(event): Promise<SignedEvent>, getRelays?(): Promise<RelayMap>, nip04: { encrypt, decrypt }, nip44: { encrypt, decrypt } } Each call is forwarded as a NIP-46 request to your bunker and the result is returned. The content-script → background-script → relay → bunker → relay → background → content-script round-trip typically finishes in 200–600ms on a good relay. Starting point: fork or build fresh? For the MVP, a fresh lightweight extension is faster than forking the Bitwarden extension. The Bitwarden extension is a large TypeScript codebase with lots of surface area you don’t need. Build a minimal MV3 extension whose only job is: 1) pair with the bunker once (store session), 2) inject window.nostr, 3) show approval prompts. Later you can merge it into a Vaultwarden extension fork if you want a single unified UX. Reference implementations worth reading: nos2x (nsec-in-extension baseline), nostr-login (bunker-aware login flow), Alby (integrates WebLN + NIP-07). Vaultwarden × Nostr — Bunker 47 briefing Page 8 7. Build order & gotchas MVP (the thing that proves it works) • Fork Vaultwarden. New nostr module. • Hard-code ‘Option B’ key wrapping (skip the master-password derivation plumbing for now). • Implement get_public_key, sign_event, ping only. • Single hard-coded relay. • CLI to add an nsec to the bunker (no UI yet). • Sign a note from a mobile client (Amethyst) via bunker://. When that works end-to-end, celebrate. v1 (shippable) • Option A key wrapping (session-unlocked, tied to master password). • Full method set including NIP-44 en/decrypt (required for DMs on modern clients). • Session + permission tables; approval prompts via WebSocket to the web vault. • Admin UI to list/revoke sessions, view audit log. • Support both bunker:// and nostrconnect:// flows. • Multi-relay with automatic failover. v2 (the cool version) • Browser extension NIP-07 bridge. • Hardware-backed key wrapping (Option C). • Embedded micro-relay so self-hosted bunkers don’t depend on a public relay. • Multi-key support: one Vaultwarden user → multiple Nostr identities (work, personal, anon). • Push notifications to mobile for approval prompts. Real gotchas (don’t learn these the hard way) • Ephemeral events and the since filter: many relays (esp. strfry) don’t actually delete kind 24133 events. Always subscribe with since = now − 10s or you’ll replay old replies. • Use remote-user-pubkey, not local-pubkey, as connect’s first param. This is the #1 bug in new NIP-46 implementations. • The secret in bunker:// is one-shot. Clients must validate it on the connect response and never reuse it. Burn after use. • All params and returns are strings. You’ll stringify events for sign_event and parse the string result. Easy to forget when writing Rust typed dispatchers. • Handle relay disconnect/reconnect. Re-subscribe to kind 24133 p-tagged to your signer-pubkey. • Dedupe replies and auth_url messages. Relays deliver duplicates; only handle the first. • switch_relays support. The spec asks compliant clients to send switch_relays immediately after connect. Your bunker should honor this or explicitly refuse — don’t silently ignore. • NIP-44 not NIP-04. Older docs/libraries still use NIP-04. Current spec is NIP-44. Pick a Rust crate that supports both and prefer NIP-44 when both peers support it. Vaultwarden × Nostr — Bunker 47 briefing Page 9 8. Reading list Primary specs • NIP-46 (Nostr Connect) — the wire spec. Read this first, twice. • NIP-07 — window.nostr surface you’ll expose in the extension. • NIP-44 — current encryption scheme for event content. • NIP-19 — bech32 encodings (npub/nsec/nprofile/bunker). • NIP-49 — encrypted nsec format (ncryptsec…). Useful for import/export. Implementation guides • nostrconnect.org — practical implementation notes and the gotchas list. https://nostrconnect.org/ • nostr-signer-connector (TS) — reference client library, both flows. • Welshman NIP-46 docs — another clean client-side impl. • Nostrify NConnectSigner — minimal example. Vaultwarden internals • Vaultwarden repo — dani-garcia/vaultwarden. • DeepWiki: Vaultwarden overview — auto-generated architecture docs with file references. • DeepWiki: Core Vault API — cipher types, sync, endpoints. • Passkey discussion #7074 — context on PRF/WebAuthn state in Vaultwarden. Rust crates you’ll likely want • nostr-sdk / nostr (rust-nostr) — protocol primitives, event signing, NIP-44, NIP-46 client • secp256k1 — Schnorr signatures (what Nostr uses) • secrecy + zeroize — memory hygiene for secret bytes • tokio-tungstenite — async WebSocket for the relay client • argon2 — key derivation if you add a bunker-specific passphrase path Reference signer implementations to read • nsec.app (Keystache) — web-based bunker, good reference for session/permission UX • nsecBunker (pablof7z) — canonical Node.js bunker implementation Vaultwarden × Nostr — Bunker 47 briefing Page 10 • Amber — Android bunker, good mobile approval-UX reference • Keystache — another take on web-based signing — end of briefing — Vaultwarden × Nostr — Bunker 47 briefing Page 11
cryptic node's avatar
CrypticNode 1 month ago
There’s a peace that settles over you when you know 5 jars of hodl butter are in the mail. image