30817:nip-ff-1-partially-encrypted-events-interoperable-paywalls

NIP-FF-1 Partially Encrypted Events (Interoperable Paywalls)

arkinox

published
2026-10-06

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 encrypted tag
  • MUST include exactly one d tag, except for derived-key events (see Derived-Key Events), which reuse another event's key and carry no d tag 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 .content as 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 a d tag, so every root has a coordinate, including roots of non-addressable kinds such as kind 1. E, carrying the root's event id, MAY accompany it, together with K and P as NIP-22 prescribes. A consumer reads A first and falls back to E only when A is absent. The root is the event whose key decrypts this one.
  • MUST include an encrypted tag (scheme + ciphertext) as usual. Element [3] (the key service URL) SHOULD be set to the same key service URL as the root's encrypted tag, 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 d tag. It is not independently purchasable and has no naddr of its own; a key service would have no key to issue for it. Its d-tag-less shape is precisely how a consumer distinguishes it from a self-keyed event.
  • Uses its public .content as a preview/teaser, exactly as any other partially encrypted event.

To decrypt a derived-key event, a consumer:

  1. Resolves the root from the derived event's NIP-22 uppercase root tags. A carries the root's coordinate, which is everything needed to form the root's naddr in the next step. When only E is present, the consumer MUST first fetch the root event and read its d tag, because an naddr cannot be formed from an event id alone.
  2. 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 content field is the root's naddr, 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.
  3. 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
  1. Generate a random 12-byte IV using a CSPRNG.
  2. Encrypt the plaintext with AES-256-GCM using the key and IV.
  3. Concatenate: IV followed by the raw encryption output (ciphertext + 16-byte GCM auth tag).
  4. Base64-encode the result and place it in element [2] of the encrypted tag.
Decryption
  1. Base64-decode element [2] to bytes.
  2. Extract the first 12 bytes as the IV.
  3. 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:

  1. Element [3] of the encrypted tag. 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).
  2. 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 client tag (["client", "<name>", "<31990 address>", "<relay>"]): the standard NIP-89 attribution. (Fanfares also includes an a tag, ["a", "<31990 address>", "<relay>"], with the same coordinate.) Both carry the application's NIP-89 handler coordinate in the form 31990:<app_pubkey>:<d>.
    • Fetch that kind:31990 handler 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.

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:

  1. content field: contains the naddr of 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.
  2. UUID tag (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: the naddr (NIP-19 addressable event reference) of the encrypted event being requested
  • tags: [["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

  1. Detect the presence of an encrypted tag on an event.
  2. Display the .content as preview text.
  3. Classify the event. An event with a d tag is self-keyed: the key belongs to this event. An event with no d tag 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].
  4. If the user wishes to decrypt, resolve the key source per Key Service Discovery: element [3] if present, otherwise the application handler.
  5. 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.
  6. If no key source can be obtained by any means, the client cannot obtain the key and SHOULD indicate this to the user.
  7. Construct a NIP-98 event with the naddr of 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.
  8. Send a GET request to the key service URL with the NIP-98 Authorization header.
  9. Decrypt the ciphertext with the key, using the scheme specified in element [1].
  10. Display the decrypted content to the user.

Security Considerations

  • The event ID, pubkey, tags, and .content remain visible to all relays and readers. Only the ciphertext is hidden.
  • Element [3] of the encrypted tag 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 not https (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/a tag, which the publisher controls. Clients SHOULD verify the kind:31990 handler 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 content field 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.