#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
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 😂🤷♂️🫶
Nostr Enhanced Relay Dashboard should be fully complete by end of May 🫶 #goals
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.
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.
Breville BDC650 + @Satoshi Coffee Co. San Pacho Honey 🔥🔥🔥🔥🔥🔥


@Satoshi Coffee Co.
Definitely #worththesats so good 🤌


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 11. 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 22. 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 3Anything 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 43. 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 54. 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 65. 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 76. 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 87. 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 98. 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
GitHub
nips/46.md at master · nostr-protocol/nips
Nostr Implementation Possibilities. Contribute to nostr-protocol/nips development by creating an account on GitHub.
GitHub
nips/07.md at master · nostr-protocol/nips
Nostr Implementation Possibilities. Contribute to nostr-protocol/nips development by creating an account on GitHub.
GitHub
nips/44.md at master · nostr-protocol/nips
Nostr Implementation Possibilities. Contribute to nostr-protocol/nips development by creating an account on GitHub.
GitHub
nips/19.md at master · nostr-protocol/nips
Nostr Implementation Possibilities. Contribute to nostr-protocol/nips development by creating an account on GitHub.
GitHub
nips/49.md at master · nostr-protocol/nips
Nostr Implementation Possibilities. Contribute to nostr-protocol/nips development by creating an account on GitHub.
GitHub
GitHub - jiftechnify/nostr-signer-connector: Nostr signer (NIP-07/-46) connectors for Nostr clients
Nostr signer (NIP-07/-46) connectors for Nostr clients - jiftechnify/nostr-signer-connector
NIP-46 (Nostr Connect) Signer | Welshman
The official Welshman documentation

Nostr Connect | Nostrify
Bring your projects to life on Nostr. 🌱
GitHub
GitHub - dani-garcia/vaultwarden: Unofficial Bitwarden compatible server written in Rust, formerly known as bitwarden_rs
Unofficial Bitwarden compatible server written in Rust, formerly known as bitwarden_rs - dani-garcia/vaultwarden

DeepWiki
dani-garcia/vaultwarden | DeepWiki
Vaultwarden is an unofficial, alternative implementation of the Bitwarden server API written in Rust. It is designed for self-hosted deployment sce...

DeepWiki
Identity and Authentication API | dani-garcia/vaultwarden | DeepWiki
This page documents the identity and authentication endpoints exposed by Vaultwarden, including token acquisition, user registration, and authentic...
GitHub
Add support for Bitwarden-style passkey login (WebAuthn / PRF) · dani-garcia/vaultwarden · Discussion #7074
Bitwarden has introduced vault decryption using PRF-enabled WebAuthn credentials: https://bitwarden.com/help/login-with-passkeys/ Request It would ...
There’s a peace that settles over you when you know 5 jars of hodl butter are in the mail.


@Satoshi Coffee Co.
#worththesats
I keep it flowing,
It keeps me going.
☕️🙏🫶