clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / source-nips / 59.md
59.md 12.9K rendered
1NIP-592======34Gift Wrap5---------67`optional` `relay`89This NIP defines a protocol for encapsulating any nostr event. This makes it possible to obscure most metadata10for a given event, perform collaborative signing, and more.1112This NIP *does not* define any messaging protocol. Applications of this NIP should be defined separately.1314This NIP relies on [NIP-44](./44.md)'s versioned encryption algorithms.1516## Overview1718This protocol uses three main concepts to protect the transmission of a target event: `rumor`s, `seal`s, and `gift wrap`s.1920- A `rumor` is a regular nostr event, but is **not signed**. This means that if it is leaked, it cannot be verified.21- A `rumor` is serialized to JSON, encrypted, and placed in the `content` field of a `seal`. The `seal` is then22 signed by the author of the note. The only information publicly available on a `seal` is who signed it, but not what was said.23- A `seal` is serialized to JSON, encrypted, and placed in the `content` field of a `gift wrap`.2425This allows the isolation of concerns across layers:2627- A rumor carries the content but is unsigned, which means if leaked it will be rejected by relays and clients,28 and can't be authenticated. This provides a measure of deniability.29- A seal identifies the author without revealing the content or the recipient.30- A gift wrap can add metadata (recipient, tags, a different author) without revealing the true author.3132## Protocol Description3334### 1. The Rumor Event Kind3536A `rumor` is the same thing as an unsigned event. Any event kind can be made a `rumor` by removing the signature.3738### 2. The Seal Event Kind3940A `seal` is a `kind:13` event that wraps a `rumor` with the sender's regular key. The `seal` is **always** encrypted41to a receiver's pubkey but there is no `p` tag pointing to the receiver. There is no way to know who the rumor is for42without the receiver's or the sender's private key. The only public information in this event is who is signing it.4344```json45{46 "id": "<id>",47 "pubkey": "<real author's pubkey>",48 "content": "<encrypted rumor>",49 "kind": 13,50 "created_at": 1686840217,51 "tags": [],52 "sig": "<real author's pubkey signature>"53}54```5556Tags MUST always be empty in a `kind:13`. The inner event MUST always be unsigned.5758### 3. Gift Wrap Event Kind5960A `gift wrap` event is a `kind:1059` event that wraps any other event. `tags` SHOULD include any information61needed to route the event to its intended recipient, including the recipient's `p` tag or [NIP-13](13.md) proof of work.6263```json64{65 "id": "<id>",66 "pubkey": "<random, one-time-use pubkey>",67 "content": "<encrypted kind 13>",68 "kind": 1059,69 "created_at": 1686840217,70 "tags": [["p", "<recipient pubkey>"]],71 "sig": "<random, one-time-use pubkey signature>"72}73```7475### 4. Ephemeral Gift Wrap Event Kind7677An ephemeral gift wrap is a `kind:21059` event. It has the same structure as `kind:1059` but follows ephemeral event78semantics (relays MUST NOT store it).7980```json81{82 "id": "<id>",83 "pubkey": "<random, one-time-use pubkey>",84 "content": "<encrypted kind 13>",85 "kind": 21059,86 "created_at": 1686840217,87 "tags": [["p", "<recipient pubkey>"]],88 "sig": "<random, one-time-use pubkey signature>"89}90```9192`kind:1059` is intended for regular direct messages and asynchronous communication where message persistence93and offline delivery are expected, such as in NIP-17. `kind:21059` is intended for real-time applications94that do not require asynchronous operations and where messages are only relevant to currently connected95recipients, such as live chat or real-time gaming.9697## Encrypting Payloads9899Encryption is done following [NIP-44](44.md) on the JSON-encoded event. Place the encryption payload in the `.content`100of the wrapper event (either a `seal` or a `gift wrap`).101102## Spam Protection103104Gift wrap events are signed by random one-time keys, so relays cannot rely on pubkey-based anti-spam or anti-sybil measures (e.g. reputation, trust scores, rate limits by key). To mitigate this, a relay may enforce a [NIP-42](42.md) `auth-required` policy and authenticate clients before accepting gift wraps. Therefore clients SHOULD support authenticating to such relays using the user's real identity. Because NIP-42 authentication is ephemeral (it is not recorded in the event signature), it does not fully undermine the privacy or deniability properties of gift wrapping.105106## Other Considerations107108If a `rumor` is intended for more than one party, or if the author wants to retain an encrypted copy, a single109`rumor` may be wrapped and addressed for each recipient individually.110111The canonical `created_at` time belongs to the `rumor`. All other timestamps SHOULD be tweaked to thwart112time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps113SHOULD be in the past.114115Relays may choose not to store gift wrapped events due to them not being publicly useful. Clients MAY choose116to attach a certain amount of proof-of-work to the wrapper event per [NIP-13](13.md) in a bid to demonstrate that117the event is not spam or a denial-of-service attack.118119To protect recipient metadata, relays SHOULD guard access to `kind 1059` events based on user AUTH. When120possible, clients should only send wrapped events to relays that offer this protection.121122To protect recipient metadata, relays SHOULD only serve `kind 1059` events intended for the marked recipient.123When possible, clients should only send wrapped events to `read` relays for the recipient that implement124AUTH, and refuse to serve wrapped events to non-recipients.125126When adding expiration tags to both `seal` and `gift wrap` layers, implementations SHOULD use independent random timestamps for each layer. Using different `created_at` values increases timing variance and helps protect against metadata correlation attacks.127128Since signing keys are random, relays SHOULD delete `kind:1059` events whose p-tag matches the signer of129[NIP-09](09.md) deletions or [NIP-62](62.md) vanish requests.130131## An Example132133Let's send a wrapped `kind 1` message between two parties asking "Are you going to the party tonight?"134135- Author private key: `0beebd062ec8735f4243466049d7747ef5d6594ee838de147f8aab842b15e273`136- Recipient private key: `e108399bd8424357a710b606ae0c13166d853d327e47a6e5e038197346bdbf45`137- Ephemeral wrapper key: `4f02eac59266002db5801adc5270700ca69d5b8f761d8732fab2fbf233c90cbd`138139Note that this messaging protocol should not be used in practice, this is just an example. Refer to other140NIPs for concrete messaging protocols that depend on gift wraps.141142### 1. Create an event143144Create a `kind 1` event with the message, the receivers, and any other tags you want, signed by the author.145Do not sign the event.146147```json148{149 "created_at": 1691518405,150 "content": "Are you going to the party tonight?",151 "tags": [],152 "kind": 1,153 "pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",154 "id": "9dd003c6d3b73b74a85a9ab099469ce251653a7af76f523671ab828acd2a0ef9"155}156```157158### 2. Seal the rumor159160Encrypt the JSON-encoded `rumor` with a conversation key derived using the author's private key and161the recipient's public key. Place the result in the `content` field of a `kind 13` `seal` event. Sign162it with the author's key.163164```json165{166 "content": "AqBCdwoS7/tPK+QGkPCadJTn8FxGkd24iApo3BR9/M0uw6n4RFAFSPAKKMgkzVMoRyR3ZS/aqATDFvoZJOkE9cPG/TAzmyZvr/WUIS8kLmuI1dCA+itFF6+ULZqbkWS0YcVU0j6UDvMBvVlGTzHz+UHzWYJLUq2LnlynJtFap5k8560+tBGtxi9Gx2NIycKgbOUv0gEqhfVzAwvg1IhTltfSwOeZXvDvd40rozONRxwq8hjKy+4DbfrO0iRtlT7G/eVEO9aJJnqagomFSkqCscttf/o6VeT2+A9JhcSxLmjcKFG3FEK3Try/WkarJa1jM3lMRQqVOZrzHAaLFW/5sXano6DqqC5ERD6CcVVsrny0tYN4iHHB8BHJ9zvjff0NjLGG/v5Wsy31+BwZA8cUlfAZ0f5EYRo9/vKSd8TV0wRb9DQ=",167 "kind": 13,168 "created_at": 1703015180,169 "pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",170 "tags": [],171 "id": "28a87d7c074d94a58e9e89bb3e9e4e813e2189f285d797b1c56069d36f59eaa7",172 "sig": "02fc3facf6621196c32912b1ef53bac8f8bfe9db51c0e7102c073103586b0d29c3f39bdaa1e62856c20e90b6c7cc5dc34ca8bb6a528872cf6e65e6284519ad73"173}174```175176### 3. Wrap the seal177178Encrypt the JSON-encoded `kind 13` event with your ephemeral, single-use random key. Place the result179in the `content` field of a `kind 1059`. Add a single `p` tag containing the recipient's public key.180Sign the `gift wrap` using the random key generated in the previous step.181182```json183{184 "content": "AhC3Qj/QsKJFWuf6xroiYip+2yK95qPwJjVvFujhzSguJWb/6TlPpBW0CGFwfufCs2Zyb0JeuLmZhNlnqecAAalC4ZCugB+I9ViA5pxLyFfQjs1lcE6KdX3euCHBLAnE9GL/+IzdV9vZnfJH6atVjvBkNPNzxU+OLCHO/DAPmzmMVx0SR63frRTCz6Cuth40D+VzluKu1/Fg2Q1LSst65DE7o2efTtZ4Z9j15rQAOZfE9jwMCQZt27rBBK3yVwqVEriFpg2mHXc1DDwHhDADO8eiyOTWF1ghDds/DxhMcjkIi/o+FS3gG1dG7gJHu3KkGK5UXpmgyFKt+421m5o++RMD/BylS3iazS1S93IzTLeGfMCk+7IKxuSCO06k1+DaasJJe8RE4/rmismUvwrHu/HDutZWkvOAhd4z4khZo7bJLtiCzZCZ74lZcjOB4CYtuAX2ZGpc4I1iOKkvwTuQy9BWYpkzGg3ZoSWRD6ty7U+KN+fTTmIS4CelhBTT15QVqD02JxfLF7nA6sg3UlYgtiGw61oH68lSbx16P3vwSeQQpEB5JbhofW7t9TLZIbIW/ODnI4hpwj8didtk7IMBI3Ra3uUP7ya6vptkd9TwQkd/7cOFaSJmU+BIsLpOXbirJACMn+URoDXhuEtiO6xirNtrPN8jYqpwvMUm5lMMVzGT3kMMVNBqgbj8Ln8VmqouK0DR+gRyNb8fHT0BFPwsHxDskFk5yhe5c/2VUUoKCGe0kfCcX/EsHbJLUUtlHXmTqaOJpmQnW1tZ/siPwKRl6oEsIJWTUYxPQmrM2fUpYZCuAo/29lTLHiHMlTbarFOd6J/ybIbICy2gRRH/LFSryty3Cnf6aae+A9uizFBUdCwTwffc3vCBae802+R92OL78bbqHKPbSZOXNC+6ybqziezwG+OPWHx1Qk39RYaF0aFsM4uZWrFic97WwVrH5i+/Nsf/OtwWiuH0gV/SqvN1hnkxCTF/+XNn/laWKmS3e7wFzBsG8+qwqwmO9aVbDVMhOmeUXRMkxcj4QreQkHxLkCx97euZpC7xhvYnCHarHTDeD6nVK+xzbPNtzeGzNpYoiMqxZ9bBJwMaHnEoI944Vxoodf51cMIIwpTmmRvAzI1QgrfnOLOUS7uUjQ/IZ1Qa3lY08Nqm9MAGxZ2Ou6R0/Z5z30ha/Q71q6meAs3uHQcpSuRaQeV29IASmye2A2Nif+lmbhV7w8hjFYoaLCRsdchiVyNjOEM4VmxUhX4VEvw6KoCAZ/XvO2eBF/SyNU3Of4SO",185 "kind": 1059,186 "created_at": 1703021488,187 "pubkey": "18b1a75918f1f2c90c23da616bce317d36e348bcf5f7ba55e75949319210c87c",188 "id": "5c005f3ccf01950aa8d131203248544fb1e41a0d698e846bd419cec3890903ac",189 "sig": "35fabdae4634eb630880a1896a886e40fd6ea8a60958e30b89b33a93e6235df750097b04f9e13053764251b8bc5dd7e8e0794a3426a90b6bcc7e5ff660f54259",190 "tags": [["p", "166bf3765ebd1fc55decfe395beff2ea3b2a4e0a8946e7eb578512b555737c99"]],191}192```193194### 4. Broadcast Selectively195196Broadcast the `kind:1059` or `kind:21059` event to the recipient's relays only. Delete all the other events.197198## Code Samples199200### JavaScript201202```javascript203import {bytesToHex} from "@noble/hashes/utils"204import type {EventTemplate, UnsignedEvent, Event} from "nostr-tools"205import {getPublicKey, getEventHash, nip19, nip44, finalizeEvent, generateSecretKey} from "nostr-tools"206207type Rumor = UnsignedEvent & {id: string}208209const TWO_DAYS = 2 * 24 * 60 * 60210211const now = () => Math.round(Date.now() / 1000)212const randomNow = () => Math.round(now() - (Math.random() * TWO_DAYS))213214const nip44ConversationKey = (privateKey: Uint8Array, publicKey: string) =>215 nip44.v2.utils.getConversationKey(bytesToHex(privateKey), publicKey)216217const nip44Encrypt = (data: EventTemplate, privateKey: Uint8Array, publicKey: string) =>218 nip44.v2.encrypt(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey))219220const nip44Decrypt = (data: Event, privateKey: Uint8Array) =>221 JSON.parse(nip44.v2.decrypt(data.content, nip44ConversationKey(privateKey, data.pubkey)))222223const createRumor = (event: Partial<UnsignedEvent>, privateKey: Uint8Array) => {224 const rumor = {225 created_at: now(),226 content: "",227 tags: [],228 ...event,229 pubkey: getPublicKey(privateKey),230 } as any231232 rumor.id = getEventHash(rumor)233234 return rumor as Rumor235}236237const createSeal = (rumor: Rumor, privateKey: Uint8Array, recipientPublicKey: string) => {238 return finalizeEvent(239 {240 kind: 13,241 content: nip44Encrypt(rumor, privateKey, recipientPublicKey),242 created_at: randomNow(),243 tags: [],244 },245 privateKey246 ) as Event247}248249const createWrap = (event: Event, recipientPublicKey: string) => {250 const randomKey = generateSecretKey()251252 return finalizeEvent(253 {254 kind: 1059,255 content: nip44Encrypt(event, randomKey, recipientPublicKey),256 created_at: randomNow(),257 tags: [["p", recipientPublicKey]],258 },259 randomKey260 ) as Event261}262263const createEphemeralWrap = (event: Event, recipientPublicKey: string) => {264 const randomKey = generateSecretKey()265266 return finalizeEvent(267 {268 kind: 21059,269 content: nip44Encrypt(event, randomKey, recipientPublicKey),270 created_at: randomNow(),271 tags: [["p", recipientPublicKey]],272 },273 randomKey274 ) as Event275}276277// Test case using the above example278const senderPrivateKey = nip19.decode(`nsec1p0ht6p3wepe47sjrgesyn4m50m6avk2waqudu9rl324cg2c4ufesyp6rdg`).data279const recipientPrivateKey = nip19.decode(`nsec1uyyrnx7cgfp40fcskcr2urqnzekc20fj0er6de0q8qvhx34ahazsvs9p36`).data280const recipientPublicKey = getPublicKey(recipientPrivateKey)281282const rumor = createRumor(283 {284 kind: 1,285 content: "Are you going to the party tonight?",286 },287 senderPrivateKey288)289290const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey)291const wrap = createWrap(seal, recipientPublicKey)292293// Recipient unwraps with their private key.294295const unwrappedSeal = nip44Decrypt(wrap, recipientPrivateKey)296const unsealedRumor = nip44Decrypt(unwrappedSeal, recipientPrivateKey)297```