nips/108.md
2023-10-17 14:56:44 -06:00

7.2 KiB

NIP-108

Lightning Gated Notes

draft optional author:coachchuckff author:excalibur_guild

This NIP defines three events for gating Notes behind lightning paywalls:

  • Lightning-Gated Note ( kind:55 ): This note allows you wrap any type of note behind a lightning gated paywall by encrypting the payload with a purchasable decrypt key.
  • Key Note ( kind:56 ): This note encrypts the key for a given note, per user, using NIP-04 between the corresponding gate creator's public key and the purchasers. It is linked to the gated note with the g tag.
  • Announcement Note ( kind:54 ): This note acts as the announcement of the gated note, giving a short preview of the content. It is linked to the gated note with the g tag.

A complete open-source implantation of NIP-108 is available.

An example client can be found at Nostr Playground.

Protocol flow

Creating Gated Notes

  1. Poster creates a note of any kind
  2. Poster JSON.stringifys the whole note.
  3. Poster encrypts the note string with a new nsec secret, and iv using aes-256-cbc.
  4. Poster creates the gated note kind:55 with the encrypted note json string in the content field, while putting iv, cost (mSats), and endpoint as tags. The endpoint tag is the server endpoint you use to hold your secret's and issue lightning invoices from your lud16.
  5. Poster then creates an announcement note kind:54 with the g tag (gated note's id) to preview the gated content.

Consuming Gated Notes

  1. Client finds gated content they want to purchase by browsing kind:54 announcement notes.
  2. Client loads the associated gated note of kind:55 found in the g tag
  3. Client then GETs the [endpoint]/[id]
  4. Gate server will respond with a 402 PR requesting a payment for the cost tag's amount in mSats
  5. Client pays the amount
  6. Client uses the successAction url returned in the PR to fetch the secret which will unlock the gated content.
  7. Client uses the secret and the gated note's iv tag to decrypt the content using aes-256-cbc
  8. Client then creates a key note kind:56 with the content being the nip-04 encrypted secret with their publicKey and the gate note's creator publicKey.
  9. Upon revisiting the gated note, the client can then decrypt the content using their key note.

Server Functions

NIP-108 requires an outside server to store secrets and issue lighting invoices to those wishing to purchase the digital content.

The server should have two endpoints: [endpoint]/create - POST to create new notes [endpoint]/[id] - GET to fetch the PR to purchase the gated note's secret, where id is the gated note's id.

Create a Gated Note

The server first needs to be able to store a gated note's secret. Minimally, the server needs to store four items: the gated note's id, the owner's lud16, the decrypt secret, and the cost in msats.

It is advised to also check, server-side, that the gated note can be unlocked. To accomplish this the following should be done:

  1. Accept a POST to the server's create endpoint, with the following: gateEvent, lud16, secret, and cost;
  2. The server should then decrypt the gateEvent using the secret and the iv tag provided in the event. Since a gated event is just an encrypted JSON stringified event, you should be able to check any of the decrypted note's field to know it's been decrypted successfully.
  3. One should also check that the endpoint matches the server's domain
  4. Store in the server's database the gated note's id, owner's lud16, decrypt secret and cost.
APP.post("/create")
export interface CreateNotePostBody {
  gateEvent: VerifiedEvent<number>;
  lud16: string;
  secret: string;
  cost: number;
}

Handling Purchases

Once the server has stored a gated note's secret, it can then be purchased via lightning.

  1. A user will GET [endpoint]/[id] and the server will...
    1. If id exists, return a 402 with a PR fetched from the stored lud16 for the amount of the stored cost
    2. If id does not exist, return a 404.
  2. The PR will contain a successAction url which should be formatted as such: [endpoint]/[id]/[payment_hash]. It is up to the user to poll this successAction.
  3. When the [endpoint]/[id]/[payment_hash] endpoint is hit, the server should check the payment status...
    1. If paid, return a JSON string {secret: [secret]}
    2. If not paid, return a 402 with the same PR
APP.get("/:noteId")
APP.get("/:noteId/:paymentHash")

Event Reference and Examples

Gated Note ( Kind:55 )

kind:55

.content should be a JSON stringified event of any kind.

.tag MUST include the following:

  • iv, the initialization vector used to encrypt the content
  • cost, the cost to unlock in msats
  • endpoint, the domain of the server used to store your decrypt secret. The user can then call GET on [endpoint]/[id] to fetch the unlock PR.

Announcement Note ( Kind:54 )

kind:54

.content some preview or announcement of the content you have locked away.

.tag MUST include the following:

  • g, the id of the gated event.

Key Note ( Kind:56 )

kind:54

.content the secret encrypted via NIP-04's encrypt function between the gated note's creator's pubkey and your pubkey.

.tag MUST include the following:

  • g, the id of the gated event.

Encryption/Decryption

To encrypt/decrypt kind:56 key notes, we use NIP-04's encrypt/decrypt functions between the gated note's creator's pubkey and your pubkey.

To encrypt/decrypt the gated note, we use aes-256-cbc. Below is a simple implementation in ts:

import * as cryptoBrowser from 'crypto-browserify';

const algorithm: string = 'aes-256-cbc';

export interface EncryptedOutput {
    iv: string;
    content: string;
}

export function hashToKey(inputString: string): Buffer {
    return cryptoBrowser.createHash('sha256').update(inputString).digest();
}

export function encrypt(text: string, key: Buffer): EncryptedOutput {
    const iv: Buffer = cryptoBrowser.randomBytes(16);
    const cipher = cryptoBrowser.createCipheriv(algorithm, key, iv);
    const encrypted: Buffer = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);

    return {
        iv: iv.toString('hex'),
        content: encrypted.toString('hex')
    };
}

export function decrypt(iv: string, content: string, key: Buffer): string {
    const decipher = cryptoBrowser.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex'));
    const decrypted: Buffer = Buffer.concat([decipher.update(Buffer.from(content, 'hex')), decipher.final()]);

    return decrypted.toString('utf8');
}

Problems

  • Servers need to be trusted
  • Nothing is stopping people from freely giving their decrypt key

Example Implementations