Dual-Envelope Encryption A Multi-Audience Encryption Pattern for NIP-82 Medical Data Events

Dual-Encryption as a 5th sharing mode for NIP-82 medical data events.

PROPOSAL

Dual-Envelope Encryption

A Multi-Audience Encryption Pattern for

NIP-82 Medical Data Events March 2026

Based on a production ready Nostr-native EHR

Executive Summary

This document proposes a dual-envelope encryption pattern for NIP-82 medical data events. The pattern embeds two independent NIP-44 ciphertexts within a single Nostr event: one decryptable by the provider, one by the patient. This eliminates the need for a separate key-distribution event or access-granting ceremony while preserving independent cryptographic access for both parties.

This pattern emerged while building a production-ready electronic health record that stores all clinical data as encrypted FHIR resources on a private Nostr relay. The system handles the full clinical workflow — charting, vitals, medications, immunizations, lab orders, secure messaging, and telehealth — across a provider-facing EHR and a patient-facing portal.

The core insight: in healthcare, every clinical record has exactly two parties who need independent, persistent access — the provider who created it and the patient it describes. Rather than managing this through key exchange protocols, we embed both access paths directly into each event at write time.

The Problem: Multi-Audience Access in Healthcare

NIP-82 defines how FHIR medical data can be carried as Nostr events. Its September 2024 revision specifies four sharing modes:

  1. Unencrypted — for non-sensitive data (immunization records, public health data)
  2. Gift wrap (kind 1059) — one-time point-to-point delivery
  3. NIP-17 DM embedding — FHIR resources shared within direct messages
  4. Kind 32225 — parameterized replaceable events with dynamic access control

All four modes assume a single audience per encryption envelope. When a provider wants to store a record for their own use AND give the patient access, the current options are:

•Publish two separate events — one encrypted to self, one gift-wrapped to the patient. This doubles the event count, creates consistency problems (which copy is canonical?), and requires the patient to have a keypair at the time of creation. •Use kind 32226 for key sharing — the original NIP-82 approach. The provider encrypts the record once, then publishes a second event containing the decryption secret encrypted to the patient. This requires a two-event protocol, introduces timing dependencies, and the key-sharing event becomes a high-value target. •Encrypt only to the provider — the patient has no independent access. They must request records through the provider, which defeats the purpose of patient data ownership on Nostr.

None of these approaches achieve what healthcare actually needs: a single canonical event that both the provider and the patient can independently decrypt using only their own private key and the other party’s public key.

The Solution: Dual-Envelope Encryption

How It Works: When a provider publishes a clinical event (encounter, vital signs, medication, etc.), the plaintext FHIR resource is encrypted twice using NIP-44:

-Provider envelope (event.content) — encrypted using getSharedSecret(provider_sk, provider_pk). The provider encrypts to themselves via ECDH with their own public key. This works even if the patient has no keypair. -Patient envelope (“patient-content” tag) — encrypted using getSharedSecret(provider_sk, patient_pk). The patient decrypts using getSharedSecret(patient_sk, provider_pk) — the ECDH shared secret is identical regardless of which side computes it. Both ciphertexts are placed inside the same Nostr event. The provider’s copy goes in the standard content field; the patient’s copy goes in a dedicated tag.

Event Structure A dual-encrypted NIP-82 event looks like this:

{
  "id": "<sha256 hash>",
  "pubkey": "<provider pubkey hex>",
  "created_at": 1708000000,
  "kind": 82,              // or per-resource kind (see below)
  "content": "<nip44 ciphertext: provider envelope>",
  "tags": [
    ["p", "<provider pubkey hex>"],
    ["p", "<patient pubkey hex>"],
    ["patient-content", "<nip44 ciphertext: patient envelope>"],
    ["fhir", "Encounter"],
    ["v", "R4"],
    ["enc", "nip44-v2"],
    ["pt", "<patient identifier>"]
  ],
  "sig": "<schnorr signature>"
}

Decryption Paths

Reader - Field - Shared - Secret - Result

Provider - event.content - ECDH(provider_sk, provider_pk) - Decrypted FHIR resource

Patient - tag: “patient-content” - ECDH(patient_sk, provider_pk) - Decrypted FHIR resource

Reference Implementation The encryption function (TypeScript):

async function dualEncrypt(
  plaintext: string,
  practiceSk: Uint8Array,
  practicePkHex: string,
  patientPkHex: string
): Promise<{ practiceEncrypted: string; patientEncrypted: string }> {
  // Envelope 1: provider encrypts to self
  const practiceSharedX = getSharedSecret(practiceSk, practicePkHex);
  const practiceEncrypted = await nip44Encrypt(plaintext, practiceSharedX);

  // Envelope 2: provider encrypts to patient
  const patientSharedX = getSharedSecret(practiceSk, patientPkHex);
  const patientEncrypted = await nip44Encrypt(plaintext, patientSharedX);

  return { practiceEncrypted, patientEncrypted };
}

The decryption function is standard NIP-44 — no special logic needed:

async function dualDecrypt(
  encrypted: string,
  viewerSk: Uint8Array,
  publisherPkHex: string
): Promise<string> {
  const sharedX = getSharedSecret(viewerSk, publisherPkHex);
  return await nip44Decrypt(encrypted, sharedX);
}

The tag builder:

function buildDualEncryptedTags(
  practicePkHex, patientPkHex, patientEncrypted,
  resourceType, patientId, additionalTags = []
): string[][] {
  return [
    ["p", practicePkHex],
    ["p", patientPkHex],
    ["patient-content", patientEncrypted],
    ["fhir", resourceType],
    ["v", "R4"],
    ["enc", "nip44-v2"],
    ["pt", patientId],
    ...additionalTags
  ];
}

Properties and Advantages

Single Canonical Event There is exactly one Nostr event per clinical record. No consistency problems between provider and patient copies. The event ID is the universal reference for that record. Relay operators, auditors, and the Nostr event log all see one event, not two.

No Key Distribution Protocol NIP-82’s original kind 32226 key-sharing event becomes unnecessary. Both parties derive their decryption key from the same ECDH primitive that NIP-44 already uses. There is no second event to publish, no timing dependency, and no key-sharing event that could be lost or intercepted.

Retroactive Patient Access This is the most operationally significant property. In Medicine, the provider creates records before the patient has portal access. A newborn seen at 2 days old has encounter notes, vitals, and immunizations recorded months before the family ever logs into a portal.

Because the provider pre-generates the patient’s keypair at registration and encrypts the "patient-content" tag to that public key from day one, the moment the patient receives their nsec (access code), every historical record is immediately accessible. There is no backfill step, no re-encryption, no migration. The access was always there, waiting for the key.

Standard NIP-44 on Both Sides The decryption path is identical for both parties: compute the ECDH shared secret, call nip44Decrypt. No new cryptographic primitives. Any NIP-44-compatible client can decrypt its respective envelope without understanding the dual-envelope pattern — it just needs to know which field to read.

Defense in Depth: NIP-42 + NIP-44 In the production deployment, content encryption (NIP-44) runs alongside relay-level authentication (NIP-42), where the relay challenges each connecting client to prove ownership of a whitelisted pubkey before allowing any subscription or publication. The relay is the single point of trust. A misconfigured whitelist, a compromised server, or a backup file in the wrong hands would expose every record on the relay — unless the records themselves are independently encrypted. NIP-42 controls who can connect; NIP-44 ensures that connection alone is not sufficient to read clinical data. These then require that both encryption methods be breached in order to compromise patient records.

Relay-Level Filtering Without Decryption The "fhir" and "pt" tags are cleartext, enabling relay-level subscription filtering by resource type and patient identifier without the relay ever seeing plaintext clinical data. Combined with per-resource event kinds (see below), this allows efficient querying: “give me all Observation events for patient X” is a single REQ filter.

Companion Pattern: Per-Resource Event Kinds

This implementation diverges from NIP-82’s single kind 82 by assigning each FHIR resource type its own event kind:

Event Kind - FHIR Resource - Use 1000-Patient, Demographics 1001 Encounter, Visit notes (SOAP) 1002 MedicationRequest, Medication orders 1003 Observation, Vitals 1004 Condition, Problem list 1005 AllergyIntolerance, Allergies 1006 Immunization, Vaccinations 1007 Message, Secure messaging (Bi-directional kind) 1008 ServiceRequest, Lab/Image Orders 1009 DiagnosticReport, Labs/Imaging Results 1010 RxOrders, ePrescribing 1011 DocumentReference, Encrypted File Attachment (NIP-B7)

This enables relay-level filtering by both resource type (via kind) and patient (via tags) without decryption. A client subscribing to a patient’s immunization history sends a single filter for kind 1006 + the patient’s identifier tag. This is materially more efficient than fetching all kind 82 events and filtering client-side after decryption.

We recognize that kind allocation is a community decision and these specific numbers are from our implementation. The pattern works with any kind assignment, including a single kind 82 with the fhir tag for resource-type discrimination. The per-kind approach is a performance optimization, not a requirement for dual-envelope encryption.

Tradeoffs and Limitations

Event Size Each event carries two ciphertexts for the same plaintext, roughly doubling the encrypted payload size. For a typical FHIR Encounter resource (1–3 KB of JSON), this adds 1–3 KB per event. For a solo pediatric practice producing thousands of events per year, this is negligible. For high-volume hospital systems producing millions of events, it may warrant consideration — though relay storage is cheap relative to the operational cost of managing a separate key-distribution protocol.

Provider-Generated Patient Keys In this implementation, the provider generates the patient’s keypair at registration so that dual encryption can begin immediately. This means the provider technically knows the patient’s private key. For a trusted provider-patient relationship (especially in pediatrics, where parents are the users), this is acceptable and operationally necessary.

In our iteration, the patient’s secret key (nsec) is displayed once and never stored by the practice — only the public key (npub) is retained. If the patient loses their nsec, the provider can re-key the patient in a single action: the system automatically generates a fresh keypair, re-encrypts all existing events for the new patient public key, updates the billing record, triggers syncing to the relay whitelizode. The provider cannot recover the original nsec.

A more trust-minimized variant would have the patient generate their own keypair and share only the public key with the provider. The dual-envelope pattern works identically in this case — the only difference is that the provider cannot encrypt the patient envelope until the patient’s public key is known, so records created before that point would only have the provider envelope. This is a deployment choice, not a protocol limitation.

Two Parties Only The pattern encrypts each event for exactly two audiences: the authoring practice and the patient. Within a single practice, this is intentional — staff access is controlled at the relay and application level (e.g., per-user keypairs with cryptographic enforcement), not at the event encryption level. All authorized staff share the ability to decrypt practice-content; a nurse and doctor at the same practice should see the same clinical data.

The two-party boundary becomes relevant in multi-provider scenarios such as specialist referrals or care team access across organizations. A receiving practice cannot decrypt the originating practice’s .content without a separate sharing mechanism. Several paths exist: additional audience tags (e.g., ["specialist-content", encrypted]) following the same ECDH pattern, NIP-82 gift wrap for point-to-point sharing, or patient-mediated re-encryption — where the patient decrypts their own patient-content, re-encrypts for the receiving provider’s pubkey, and publishes to that provider’s relay. This last approach aligns most naturally with the system’s data sovereignty model, where the patient is the authoritative bridge between providers.

Tag Visibility The "patient-content" tag name is visible in cleartext to the relay operator. This reveals that the event uses dual encryption and has a patient audience, but reveals nothing about the content. The "p" tags reveal the provider and patient public keys, which is standard Nostr practice for encrypted events.

Production Context

This pattern is not theoretical. It runs in a production ready pediatric EHR (Immutable Health Pediatrics) with the following architecture:

•EHR application — Next.js/React, runs locally on the provider’s machine. Publishes dual-encrypted FHIR events to a private relay. •Patient portal — Next.js web app. Patients log in with their nsec (given as an access code). Reads the “patient-content” tag from every event to display their records, vitals, medications, immunizations, and messages. •Private relay — Self-hosted, whitelisted. NIP-42 authenticated. Only the practice pubkey and registered patient pubkeys can publish or subscribe. •Secure messaging — Kind 1007 threaded messages between provider and patient. Also uses dual encryption so both parties have independent access to the conversation. •Encryption library — Pure TypeScript NIP-44 implementation (secp256k1 ECDH, HKDF, ChaCha20-Poly1305). No external crypto dependencies. The system handles the full lifecycle: patient registration with keypair generation, clinical charting with dual-encrypted publish, patient portal login and independent decryption, and secure messaging. Growth charts plot vitals data pulled from the relay and decrypted client-side. The entire clinical record exists as Nostr events — there is no separate database. •Telehealth — WebRTC video visits with call establishment signaled entirely through NIP-44 encrypted Nostr events (kinds 4050–4055). SDP offers, ICE candidates, and call state are exchanged through the same relay infrastructure — no third-party signaling server. TURN relay for NAT traversal. •FHIR R4 REST API — Read-only endpoints serving decrypted FHIR R4 Bundles, protected by API key authentication with scoping and access logging. Enables interoperability with external systems without exposing the Nostr layer. •Document attachments — Encrypted file storage via Blossom (NIP-B7). Files are encrypted client-side with AES-256-GCM before upload; the encryption key is stored inside a dual-encrypted DocumentReference event (kind 1011). The Blossom server never sees plaintext. •Clinical decision support — Automated immunization schedule evaluation against the CDC/ACIP childhood schedule, well-child visit tracking against AAP periodicity, and allergy-medication interaction alerts surfaced at prescribing time. •PDF generation — Client-side generation of six standardized pediatric forms (school excuses, immunization records, growth charts, sports physicals, child care clearances, kindergarten readiness forms) directly from encrypted relay data.

Relationship to NIP-82

This proposal is intended as a complementary pattern for NIP-82, not a replacement. Specifically:

•Dual-envelope encryption could become a fifth sharing mode alongside the four already specified (unencrypted, gift wrap, DM embedding, kind 32225). •It is specifically designed for the EHR use case where a provider publishes records about a patient and both need persistent access. •It does not replace gift wrap for point-to-point sharing (e.g., sending records to a specialist) or NIP-17 for conversational sharing (e.g., discussing results with a patient in a DM). •The “patient-content” tag name is a suggestion. Any agreed-upon tag name works. The pattern could also be generalized to “audience-content” with a role identifier for multi-audience extensions.

Next Steps

We are sharing this because Vitor Pamplona has explicitly asked for real-world NIP-82 implementations, and this is one. We’d welcome feedback on:

1.Tag naming convention: Is “patient-content” the right tag name? Should it be more generic (e.g., “audience-content” with a role parameter)? 2.Kind allocation: Should per-resource-type kinds be standardized alongside NIP-82, or should the community stick with kind 82 and rely on tag filtering? 3.Multi-audience extension: Is there interest in generalizing beyond two envelopes for care team scenarios? 4.Integration with NIP-82 spec: Would this work as a fifth sharing mode in the existing spec, or is it better as a standalone companion NIP? 5.Open source release: We are preparing the EHR and portal for open source release (sanitized, configurable). Happy to coordinate timing with the health data working group.

The code is running. We’d like to help make this a standard.

Disclaimer Portions of this document were written with the assistance of Claude AI. The architectural decisions, clinical requirements, implementation details, and production system described herein are the original work of the author. AI assistance was used for drafting, editing, and refining the written presentation of these ideas.

Immutable Health Pediatrics Practice npub: nostr:npub1l88nev3unjuheqd63v4wfzusvdr2kfyk9z8fmhqxp0pt4dnwmfk (hex: f9cf4e…eed9) Relay: wss://relay.immutablehealthpediatrics.com March 2026


Write a comment