NIP-46 relay session
draftWhere the decrypted bridge only converts an
already-decrypted payload into a decision, the relay session is the layer
that actually talks to a remote signer over Nostr relays: it opens the relay,
encrypts and decrypts the NIP-44 envelopes, correlates requests with responses,
and verifies the returned event. It is built in @nsealr/nip46 and consumes the
frozen nsealr-nip46-session-active-v0 contract from nSealr/specs.
Layers
The session is composed from small, independently tested units:
| Layer | What it does | Module |
|---|---|---|
| Relay transport | publish / subscribe-by-filter / close, I/O only | RelayTransport (InMemoryRelay fake, NostrToolsRelay for production) |
| NIP‑44 v2 | authenticated encryption of the envelope payload | @nsealr/core nip44 (hand-rolled ChaCha20 + HMAC-SHA256) |
| Session protocol | build a signed + encrypted kind‑24133 event; decrypt + verify one | buildNip46RequestEvent / decryptNip46Event |
| Session manager | the idle → session_active → session_closed state machine, id‑correlated timed request/response | Nip46SessionManager |
| Verification | recompute the NIP‑01 id and check the BIP‑340 signature of the returned event | @nsealr/core verifySignedEventResponse |
| Persistence | secretless session record (metadata only) | SessionStore / toActiveSessionRecord |
Flow
- connect — the manager subscribes for kind‑24133 events addressed to the
client (
#p= client pubkey), then publishes an encryptedconnectrequest and waits for the signer’sack. - session_active —
signEvent(template)publishes an encryptedsign_eventrequest; the matching response is decrypted, then the returned event is verified (NIP‑01 id recompute + BIP‑340 signature + same-template check) before it is handed back. An unmatched or malformed response is rejected, not silently dropped. - session_closed — the relay subscription is closed and any in‑flight request is rejected.
Every transport event is verified on the way in (kind 24133, recomputed id, BIP‑340 signature) and checked to be addressed to this recipient before it is NIP‑44‑decrypted.
Cryptography stays hand-rolled
NIP‑44 v2 is implemented in @nsealr/core from @noble primitives (secp256k1
ECDH → HKDF schedule → ChaCha20 → HMAC‑SHA256), not delegated. Its correctness is
pinned by a dual oracle: the official NIP‑44 v2 test vectors and a
differential test against nostr-tools/nip44. nostr-tools is used in production
only for relay transport (SimplePool); it never touches the signing or
verification path.
Persistence is secretless
A persisted session is a contract-conformant nsealr-nip46-session-active-v0
record — pubkeys, relays, connect digest, approved permissions, lifecycle phase.
It never stores the client transport key, the NIP‑44 conversation key, or any
other secret material; the record is validated through the same parser the
contract defines, which rejects any secret-bearing field.
Status
See also
- NIP-46 decrypted bridge — the complementary, narrow decision step for an already-decrypted payload (a separate component from this transport/session layer).
- ESP32 USB/NIP-46 signer — the device end of a bridged session.
nSealr/specs—protocols/nip46-session-active-v0.mdand its conformance vectors.