30817:nip-ff-1-partially-encrypted-events-interoperable-paywalls
NIP-FF-1 Partially Encrypted Events (Interoperable Paywalls)
NIP-FF-1
Partially Encrypted Events
draft optional
This NIP defines a convention for partially encrypted nostr events. The encrypted portion contains paywalled or locked content, while the .content and other public fields serve as a preview.
The purpose is to restrict access to part of an event's content while keeping the event's preview visible and functional in any nostr client, even those that do not support decryption. The primary use case is paywalled content where a decryption key can be obtained (e.g., via payment) from a key service. Once the key is obtained, the client can display the decrypted content in place of the preview content, giving the event two states: a preview state and an unlocked state.
This NIP's convention is compatible with all typical content and media-bearing event kinds.
Structure Overview
A partially encrypted event has the following structure beyond NIP-01:
- MUST include exactly one
encryptedtag - MUST include exactly one
dtag, except for derived-key events (see Derived-Key Events), which reuse another event's key and carry nodtag of their own - For encrypted media, NIP-92 imeta tags MUST include
encrypted <scheme>
Encrypted Tag Structure and Meaning
["encrypted", "<scheme>", "<ciphertext>", "<key source>"]
| Index | Field | Required | Description |
|---|---|---|---|
| 0 | "encrypted" |
yes | Tag name: identifies this as a partial encryption tag |
| 1 | <scheme> |
yes | Encryption scheme identifier (e.g., "aes-256-gcm") |
| 2 | <ciphertext> |
yes | Base64-encoded encrypted payload (IV || ciphertext || authTag). Use "" (empty string) when .content holds the full visible text and no hidden content exists, but a key service URL is still provided |
| 3 | <key source> |
recommended; required when the key is derived | Where the decryption key comes from. Two forms are defined. (1) A key service URL: an HTTPS URL of the endpoint that issues the key through the Key Retrieval flow. This is the form for every key that is issued to a consumer, for example after payment. New events with an issued key SHOULD include it; when absent, clients MUST discover it via the application handler (see Key Service Discovery). (2) A key derivation URI: a URI whose scheme appears in the Key Derivation Registry, naming a procedure by which every consumer computes the key locally. It is never fetched. Events whose key is derived MUST include it, because it is the only signal that no key service exists (see Roots Whose Key Is Derived, Not Issued). |
The presence of the encrypted tag on an event indicates that there is either ciphertext within the event, NIP-92 encrypted media, or both.
With Ciphertext
If the encrypted tag contains ciphertext, the .content of the event is considered the preview content.
Clients following this NIP SHOULD:
- Display the
.contentas a preview to users who have not decrypted the event. - Replace the displayed content with the decrypted content upon successful decryption.
- Allow users to toggle between preview and decrypted content.
Without Ciphertext
If the encrypted tag does not contain ciphertext (empty "" string), then the .content is treated as the canonical content of the event (as usual), and encrypted media is presumed within one or more NIP-92 imeta tags.
If a NIP-92 imeta tag references encrypted media, it must include an encrypted <scheme> tag to inform the Client how to decrypt it.
Examples
When an event's .content is a preview and the encrypted tag contains ciphertext that will replace the preview when unlocked:
["encrypted", "aes-256-gcm", "u5sINPT2/xr6AW51CHEARt3yiOFZ9IZwrVlcqCVVa2J+...", "https://keyservice.example.com/request-key"]
// other media assets in this event may also be encrypted; they will have their own `encrypted <schema>` tag within the imeta tag.
When an event's .content is canonical but other media assets are encrypted:
["encrypted", "aes-256-gcm", "", "https://keyservice.example.com/request-key"]
d Tag Structure and Purpose
Partially encrypted events MUST include a d tag with a unique identifier, typically a UUIDv4:
["d", "<unique identifier>"]
The d tag enables construction of an naddr reference (NIP-19) for any event kind, including non-addressable kinds like kind 1. While NIP-01 only requires d tags for addressable events (kind 30000+), including d on all events provides a single, uniform way to reference any partially encrypted event regardless of its kind. This matters for key services: supporting both naddr (for addressable events) and nevent (for non-addressable events) would require forked lookup, validation, and key derivation logic for each entity type. A d tag on every partially encrypted event means all key service operations use one code path and one reference format.
Derived-Key Events
Most partially encrypted events have their own decryption key: they are independently purchasable, carry a d tag, and a consumer authenticates against the event's naddr with the event's key service to obtain the event's key. A derived-key event is different: it has no key of its own and encrypts its ciphertext with the key of a different event (its root). The canonical case is an encrypted comment (a NIP-22 kind:1111 event) on paid content, where only holders of the content's key should be able to read the comment.
A derived-key event:
- MUST reference its root through NIP-22 uppercase root tags.
A, carrying the root's coordinate<kind>:<pubkey>:<d>, MUST be present: every partially encrypted root carries adtag, so every root has a coordinate, including roots of non-addressable kinds such askind 1.E, carrying the root's event id, MAY accompany it, together withKandPas NIP-22 prescribes. A consumer readsAfirst and falls back toEonly whenAis absent. The root is the event whose key decrypts this one. - MUST include an
encryptedtag (scheme+ciphertext) as usual. Element [3] (the key service URL) SHOULD be set to the same key service URL as the root'sencryptedtag, mirrored onto the derived event, so a consumer can reach the key service without first fetching the root. When the root omits element [3], the key service is discovered from the root via the application handler fallback. When the root's key is derived rather than issued, element [3] names the derivation instead (see Roots Whose Key Is Derived). - MUST NOT include a
dtag. It is not independently purchasable and has nonaddrof its own; a key service would have no key to issue for it. Itsd-tag-less shape is precisely how a consumer distinguishes it from a self-keyed event. - Uses its public
.contentas a preview/teaser, exactly as any other partially encrypted event.
To decrypt a derived-key event, a consumer:
- Resolves the root from the derived event's NIP-22 uppercase root tags.
Acarries the root's coordinate, which is everything needed to form the root'snaddrin the next step. When onlyEis present, the consumer MUST first fetch the root event and read itsdtag, because annaddrcannot be formed from an event id alone. - Resolves the root's key source (see Key Service Discovery). When it is a key service URL, obtains the root's key via the normal Key Retrieval flow: the NIP-98
contentfield is the root'snaddr, not the derived event's. Payment/authorization is verified against the root, because that is the event that was purchased. When it is a key derivation URI, computes the root's key locally per the Key Derivation Registry; no request is made. - Decrypts the derived event's element [2] ciphertext with the root's key and the scheme in element [1].
A consumer who already holds the root's key (e.g. because it unlocked the root in the same session) decrypts the derived event directly, with no additional key request.
Because the key is bound to the root, all of the root's access rules transfer automatically: anyone entitled to read the root content is entitled to read every derived-key event under it, and no one else. As with any event, a consumer MUST validate the derived event's signature before treating its decrypted body as authentic.
Roots Whose Key Is Derived, Not Issued
Some roots have no key service because their key is never issued to anyone: every consumer derives it. The canonical case is a Cyberspace bag, a kind:33330 event whose contents are encrypted to the region key of the location it is hidden at (Cyberspace Protocol v2, section 7). A client that reaches that location derives the key itself, and so can any client that has already opened the bag.
A derived-key event under such a root (for example an ONOSENDAI comment on a hidden shard or message) sets element [3] to a URI whose scheme names the derivation, in place of an https key service URL:
["encrypted", "aes-256-gcm", "<ciphertext>", "cyberspace:region"]
cyberspace:region means: the root named by the A tag is a Cyberspace bag, and the key is the region key of that bag's location (Cyberspace Protocol v2, sections 7.2 and 7.4). A consumer that holds the key, because it opened the bag, decrypts directly. A consumer that does not recognise the scheme, or has not reached the place, treats the event as sealed and shows the preview, which for ONOSENDAI reads "This comment is hidden at an undisclosed location in cyberspace. Happy hunting: https://onosendai.tech". For such roots element [3] is required rather than recommended: it is the only way a consumer learns that no key service exists.
The same derivation serves a second shape: an event referenced from a bag. A bag's decrypted list may name events published on their own, by a coordinate or e id (Cyberspace Protocol v2, section 7.6), and each such event is a self-keyed partially encrypted event in this NIP's shape: it carries its own d tag, a public preview in .content, and ["encrypted", "aes-256-gcm", "<ciphertext>", "cyberspace:region"]. Unlike a derived-key event it names no root, because naming the bag would tie it to the place; its key is the region key of the bag that references it. A consumer that opened that bag decrypts it with the key in hand. A consumer that reaches the event any other way, for example by querying its kind, sees only the preview, and MUST NOT attempt a network request for the key.
The effect is that a conversation can live in a place. The thread's structure (root, parent, who is tagged) is public and notifies through any NIP-22 client; its words are readable exactly where the thing it discusses is readable.
Key Derivation Registry
A key derivation URI is recognised by its scheme. This table is the registry of defined schemes. Registering a derivation means adding a row: the scheme MUST be unambiguous, MUST name a deterministic procedure that any consumer can run given the root event, and MUST NOT be https (which always denotes a key service URL). A consumer that meets a scheme not in this table treats the event as sealed and MUST NOT attempt a network request to it.
| Scheme | Key | Root shape | Defined by | Reference implementation |
|---|---|---|---|---|
cyberspace:region |
The region key of a Cyberspace bag's location: SHA256 of the region number derived from the bag's coordinates and encryption height (Cyberspace Protocol v2, sections 7.2 and 7.4). 32 bytes, used directly as the aes-256-gcm key. |
Either a derived-key event whose root named by A is a Cyberspace bag, kind:33330; or a self-keyed event of any kind referenced from inside a bag's encrypted list, which names no root and whose key is that bag's region key (section 7.6). |
Cyberspace Protocol v2 | ONOSENDAI comments on hidden shards and messages; objects hidden by reference (DECK-0003 sections 3.2 and 3.4) |
Encryption Schemes
The scheme identifier in encrypted tag index 1 MAY be any string that unambiguously identifies the encryption algorithm. Clients MUST understand the scheme to decrypt. Unknown schemes SHOULD be reported to the user with an appropriate error.
This section defines the reference scheme used by Fanfares.
aes-256-gcm (Fanfares Reference)
Fanfares uses AES-256 in GCM mode via the Web Crypto API. This scheme was chosen so that no dependencies beyond native Web Crypto would be necessary for handling encrypted events in web-based contexts.
Byte layout
| Component | Size | Notes |
|---|---|---|
| Key | 32 bytes (256 bits) | Hex-encoded as 64 lowercase characters |
| IV | 12 bytes | Randomly generated per encryption |
| Ciphertext | variable | Raw AES-GCM output, not including auth tag |
| Auth tag | 16 bytes | Appended by AES-GCM; verified automatically on decrypt |
| Encoded payload | 12 + len(plaintext) + 16 bytes | IV || ciphertext || authTag, base64-encoded |
Encryption
- Generate a random 12-byte IV using a CSPRNG.
- Encrypt the plaintext with AES-256-GCM using the key and IV.
- Concatenate:
IVfollowed by the raw encryption output (ciphertext + 16-byte GCM auth tag). - Base64-encode the result and place it in element [2] of the
encryptedtag.
Decryption
- Base64-decode element [2] to bytes.
- Extract the first 12 bytes as the IV.
- Pass the key, IV, and remaining bytes to an AES-256-GCM decrypt. The GCM auth tag is the trailing 16 bytes; compliant libraries verify it automatically.
Encrypted Files
To include encrypted files alongside a partially encrypted event, add an imeta tag (per NIP-92) for each file and append "encrypted <scheme>" as an additional element in the imeta tag. Per the NIP-92 space-delimited key/value convention, "encrypted aes-256-gcm" encodes key=encrypted, value=aes-256-gcm.
["imeta",
"url https://cdn.example.com/file.enc",
"m application/octet-stream",
"ox <original file hash>",
"x <encrypted file hash>",
"encrypted aes-256-gcm"
]
The encrypted tag MUST still be present on the event so that partially encrypted events can be identified without parsing all imeta tags.
Key Retrieval
When the key source is a key service URL, the decryption key is obtained from that URL. The key source is taken from element [3] of the encrypted tag when present; when element [3] is absent, the key service URL is discovered via the application handler as described in Key Service Discovery below. When the key source is a key derivation URI, nothing in this section applies: the key is computed locally per the Key Derivation Registry.
Key Service Discovery
A consumer resolves the key source in the following order:
- Element [3] of the
encryptedtag. If present, branch on its scheme:- Scheme
https: it is the key service URL. Use it directly. New events with an issued key SHOULD include it. - A scheme listed in the Key Derivation Registry: it is a key derivation URI. There is no key service; derive the key locally and skip step 2.
- Any other scheme: the publisher has declared a key source this client does not understand. Treat the event as sealed, show the preview, and do not fall through to step 2 (see Security Considerations).
- Scheme
- Application handler (NIP-89 fallback). If element [3] is absent, resolve the key service from the application that published the event:
- Read the event's
clienttag (["client", "<name>", "<31990 address>", "<relay>"]): the standard NIP-89 attribution. (Fanfares also includes anatag,["a", "<31990 address>", "<relay>"], with the same coordinate.) Both carry the application's NIP-89 handler coordinate in the form31990:<app_pubkey>:<d>. - Fetch that
kind:31990handler information event (NIP-89) using the coordinate and relay hint. - Read the
["key-service", "<HTTPS URL>"]tag from the handler event. Its value is the key service URL.
- Read the event's
NIP-89 defines no native field for a service endpoint, so the key-service tag is an application-defined extension to the handler event (unknown tags are ignored by other NIP-89 clients, so this is safe). Clients SHOULD verify that the fetched kind:31990 event is signed by the <app_pubkey> named in the coordinate before trusting its key-service URL, and MUST enforce the HTTPS-only requirement on the discovered URL (see Security Considerations).
NIP-98 Extension
Key retrieval uses NIP-98 HTTP Auth with two extensions:
contentfield: contains thenaddrof the encrypted event being requested, rather than the empty string prescribed by base NIP-98. This binding prevents a buyer's authorization from being replayed against a different event: the signed naddr is proof of which event the requester intends to unlock.UUIDtag (for key generation requests only): the NIP-98 event MAY include a["UUID", "<uuid>"]tag when calling a key generation endpoint. This binds the derived key to a specific event identifier before the event is published.
Clients and key services that implement this NIP MUST accept these extensions. The deviations from base NIP-98 are intentional and load-bearing.
Request
Clients make an HTTP GET request to the key service URL:
GET https://keyservice.example.com/request-key
The request MUST include an Authorization header containing a NIP-98 token:
Authorization: Nostr <nip98_token>
The NIP-98 token is a base64-encoded signed nostr event of kind 27235 with:
content: thenaddr(NIP-19 addressable event reference) of the encrypted event being requestedtags:[["u", "<key service URL>"], ["method", "GET"]]
Clients MUST only send key retrieval requests to HTTPS URLs. Clients MUST NOT follow redirects to non-HTTPS schemes. Key service URLs using any other scheme SHOULD be rejected by clients before a request is made.
If the requester's pubkey matches the creator's pubkey (i.e., the author is decrypting their own content), no payment verification is required.
Response
If the key service approves the request (based on proprietary payment verification or whatever other condition, out of scope of this spec), the key is returned as a 64-character lowercase hex string with Content-Type: text/plain:
200 OK
Content-Type: text/plain
a1b2c3d4e5f6... (64 hex chars)
Error Responses
| HTTP Status | Body | Meaning |
|---|---|---|
| 401 | plaintext string | Invalid naddr in NIP-98 content field |
| 402 | Payment not verified |
Requester has not paid the required amount |
| 403 | plaintext string | Authorization failed |
| 405 | Method not allowed |
Request was not GET |
| 500 | Error: <message> |
Internal server error |
Client Behavior
- Detect the presence of an
encryptedtag on an event. - Display the
.contentas preview text. - Classify the event. An event with a
dtag is self-keyed: the key belongs to this event. An event with nodtag and NIP-22 uppercase root tags is a derived-key event: resolve its root (see Derived-Key Events) and continue with the root's key source, while the ciphertext to decrypt remains the derived event's own element [2]. - If the user wishes to decrypt, resolve the key source per Key Service Discovery: element [3] if present, otherwise the application handler.
- If the key source is a key derivation URI whose scheme the client recognises, compute the key locally and go to step 9. If the scheme is not recognised, the event stays sealed; indicate this to the user and stop.
- If no key source can be obtained by any means, the client cannot obtain the key and SHOULD indicate this to the user.
- Construct a NIP-98 event with the
naddrof the self-keyed event, or of the root for a derived-key event, as its content and the key service URL + GET method in its tags. - Send a GET request to the key service URL with the NIP-98
Authorizationheader. - Decrypt the ciphertext with the key, using the scheme specified in element [1].
- Display the decrypted content to the user.
Security Considerations
- The event ID, pubkey, tags, and
.contentremain visible to all relays and readers. Only the ciphertext is hidden. - Element [3] of the
encryptedtag is public and attacker-controlled: a malicious event could embed an arbitrary value there. The HTTPS-only rule applies to anything a client would fetch: clients MUST only contact key service URLs over HTTPS and MUST reject, without making a request, any value they would otherwise fetch whose scheme is nothttps(file://,javascript:,http:, etc.). A key derivation URI is never fetched: a client that recognises its scheme in the Key Derivation Registry computes the key locally, and a client that does not recognise it treats the event as sealed and MUST NOT attempt a network request to it. - The key service URL is otherwise unrestricted: anyone can operate a compatible key service. Clients should make it clear to users which key service they are contacting before sending an authenticated request.
- When the key service URL is discovered via the application handler (NIP-89 fallback) instead of element [3], the same trust caveats apply: the handler is referenced by the event's
client/atag, which the publisher controls. Clients SHOULD verify thekind:31990handler event is signed by the<app_pubkey>in its coordinate, and MUST enforce the HTTPS-only rule on the discovered URL. - The naddr in the NIP-98
contentfield binds the authorization to a specific event. A key service MUST verify that the naddr in the request's NIP-98 token matches the event whose key is being requested, to prevent replaying a valid authorization against a different event. - The key service URL is public. Anyone who has paid for (or otherwise obtained) the key can re-share it. This is by design: once a buyer has the key, they own the decrypted content.
- Key services SHOULD rate-limit requests and require NIP-98 auth to prevent abuse.
- Clients MUST validate the event signature before displaying decrypted content as authentic.
Discussion
Connect a key to comment.