upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/59.md
diff options
context:
space:
mode:
authorfiatjaf_ <fiatjaf@gmail.com>2024-01-29 13:06:25 -0300
committerGitHub <noreply@github.com>2024-01-29 13:06:25 -0300
commit9efafe22940c4465daf20cb388d00c492bc2004c (patch)
treeee35b26fd37eba90e72903e4a1a9ba282c611720 /59.md
parent7ec060375c5aa8697e8b26c2d1088c9077bfa910 (diff)
parent1a2b21b67eeae2eba8c03215826a32d4a03ae172 (diff)
Merge pull request #716 from coracle-social/NIP-59
Introduce NIP-59 gift wrap
Diffstat (limited to '59.md')
-rw-r--r--59.md252
1 files changed, 252 insertions, 0 deletions
diff --git a/59.md b/59.md
new file mode 100644
index 0000000..c990236
--- /dev/null
+++ b/59.md
@@ -0,0 +1,252 @@
1NIP-59
2======
3
4Gift Wrap
5---------
6
7`optional`
8
9This NIP defines a protocol for encapsulating any nostr event. This makes it possible to obscure most metadata
10for a given event, perform collaborative signing, and more.
11
12This NIP *does not* define any messaging protocol. Applications of this NIP should be defined separately.
13
14This NIP relies on [NIP-44](./44.md)'s versioned encryption algorithms.
15
16# Overview
17
18This protocol uses three main concepts to protect the transmission of a target event: `rumor`s, `seal`s, and `gift wrap`s.
19
20- 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 then
22 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`.
24
25This allows the isolation of concerns across layers:
26
27- 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.
31
32# Protocol Description
33
34## 1. The Rumor Event Kind
35
36A `rumor` is the same thing as an unsigned event. Any event kind can be made a `rumor` by removing the signature.
37
38## 2. The Seal Event Kind
39
40A `seal` is a `kind:13` event that wraps a `rumor` with the sender's regular key. The `seal` is **always** encrypted
41to a receiver's pubkey but there is no `p` tag pointing to the receiver. There is no way to know who the rumor is for
42without the receiver's or the sender's private key. The only public information in this event is who is signing it.
43
44```js
45{
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```
55
56Tags MUST must always be empty in a `kind:13`. The inner event MUST always be unsigned.
57
58## 3. Gift Wrap Event Kind
59
60A `gift wrap` event is a `kind:1059` event that wraps any other event. `tags` SHOULD include any information
61needed to route the event to its intended recipient, including the recipient's `p` tag or NIP-13 proof of work.
62
63```js
64{
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```
74
75# Encrypting Payloads
76
77Encryption is done following NIP-44 on the JSON-encoded event. Place the the encryption payload in the `.content`
78of the wrapper event (either a `seal` or a `gift wrap`).
79
80# Other Considerations
81
82If a `rumor` is intended for more than one party, or if the author wants to retain an encrypted copy, a single
83`rumor` may be wrapped and addressed for each recipient individually.
84
85The canonical `created_at` time belongs to the `rumor`. All other timestamps SHOULD be tweaked to thwart
86time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps
87SHOULD be in the past.
88
89Relays may choose not to store gift wrapped events due to them not being publicly useful. Clients MAY choose
90to attach a certain amount of proof-of-work to the wrapper event per NIP-13 in a bid to demonstrate that
91the event is not spam or a denial-of-service attack.
92
93To protect recipient metadata, relays SHOULD guard access to kind 1059 events based on user AUTH. When
94possible, clients should only send wrapped events to relays that offer this protection.
95
96To protect recipient metadata, relays SHOULD only serve kind 1059 events intended for the marked recipient.
97When possible, clients should only send wrapped events to `read` relays for the recipient that implement
98AUTH, and refuse to serve wrapped events to non-recipients.
99
100# An Example
101
102Let's send a wrapped `kind 1` message between two parties asking "Are you going to the party tonight?"
103
104- Author private key: `0beebd062ec8735f4243466049d7747ef5d6594ee838de147f8aab842b15e273`
105- Recipient private key: `e108399bd8424357a710b606ae0c13166d853d327e47a6e5e038197346bdbf45`
106- Ephemeral wrapper key: `4f02eac59266002db5801adc5270700ca69d5b8f761d8732fab2fbf233c90cbd`
107
108Note that this messaging protocol should not be used in practice, this is just an example. Refer to other
109NIPs for concrete messaging protocols that depend on gift wraps.
110
111## 1. Create an event
112
113Create a `kind 1` event with the message, the receivers, and any other tags you want, signed by the author.
114Do not sign the event.
115
116```json
117{
118 "created_at": 1691518405,
119 "content": "Are you going to the party tonight?",
120 "tags": [],
121 "kind": 1,
122 "pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",
123 "id": "9dd003c6d3b73b74a85a9ab099469ce251653a7af76f523671ab828acd2a0ef9"
124}
125```
126
127## 2. Seal the rumor
128
129Encrypt the JSON-encoded `rumor` with a conversation key derived using the author's private key and
130the recipient's public key. Place the result in the `content` field of a `kind 13` `seal` event. Sign
131it with the author's key.
132
133```json
134{
135 "content": "AqBCdwoS7/tPK+QGkPCadJTn8FxGkd24iApo3BR9/M0uw6n4RFAFSPAKKMgkzVMoRyR3ZS/aqATDFvoZJOkE9cPG/TAzmyZvr/WUIS8kLmuI1dCA+itFF6+ULZqbkWS0YcVU0j6UDvMBvVlGTzHz+UHzWYJLUq2LnlynJtFap5k8560+tBGtxi9Gx2NIycKgbOUv0gEqhfVzAwvg1IhTltfSwOeZXvDvd40rozONRxwq8hjKy+4DbfrO0iRtlT7G/eVEO9aJJnqagomFSkqCscttf/o6VeT2+A9JhcSxLmjcKFG3FEK3Try/WkarJa1jM3lMRQqVOZrzHAaLFW/5sXano6DqqC5ERD6CcVVsrny0tYN4iHHB8BHJ9zvjff0NjLGG/v5Wsy31+BwZA8cUlfAZ0f5EYRo9/vKSd8TV0wRb9DQ=",
136 "kind": 13,
137 "created_at": 1703015180,
138 "pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",
139 "tags": [],
140 "id": "28a87d7c074d94a58e9e89bb3e9e4e813e2189f285d797b1c56069d36f59eaa7",
141 "sig": "02fc3facf6621196c32912b1ef53bac8f8bfe9db51c0e7102c073103586b0d29c3f39bdaa1e62856c20e90b6c7cc5dc34ca8bb6a528872cf6e65e6284519ad73"
142}
143```
144
145## 3. Wrap the seal
146
147Encrypt the JSON-encoded `kind 13` event with your ephemeral, single-use random key. Place the result
148in the `content` field of a `kind 1059`. Add a single `p` tag containing the recipient's public key.
149Sign the `gift wrap` using the random key generated in the previous step.
150
151```json
152{
153 "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",
154 "kind": 1059,
155 "created_at": 1703021488,
156 "pubkey": "18b1a75918f1f2c90c23da616bce317d36e348bcf5f7ba55e75949319210c87c",
157 "id": "5c005f3ccf01950aa8d131203248544fb1e41a0d698e846bd419cec3890903ac",
158 "sig": "35fabdae4634eb630880a1896a886e40fd6ea8a60958e30b89b33a93e6235df750097b04f9e13053764251b8bc5dd7e8e0794a3426a90b6bcc7e5ff660f54259"
159 "tags": [["p", "166bf3765ebd1fc55decfe395beff2ea3b2a4e0a8946e7eb578512b555737c99"]],
160}
161```
162
163## 4. Broadcast Selectively
164
165Broadcast the `kind 1059` event to the recipient's relays only. Delete all the other events.
166
167# Code Samples
168
169## JavaScript
170
171```javascript
172import {bytesToHex} from "@noble/hashes/utils"
173import type {EventTemplate, UnsignedEvent, Event} from "nostr-tools"
174import {getPublicKey, getEventHash, nip19, nip44, finalizeEvent, generateSecretKey} from "nostr-tools"
175
176type Rumor = UnsignedEvent & {id: string}
177
178const TWO_DAYS = 2 * 24 * 60 * 60
179
180const now = () => Math.round(Date.now() / 1000)
181const randomNow = () => Math.round(now() - (Math.random() * TWO_DAYS))
182
183const nip44ConversationKey = (privateKey: Uint8Array, publicKey: string) =>
184 nip44.v2.utils.getConversationKey(bytesToHex(privateKey), publicKey)
185
186const nip44Encrypt = (data: EventTemplate, privateKey: Uint8Array, publicKey: string) =>
187 nip44.v2.encrypt(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey))
188
189const nip44Decrypt = (data: Event, privateKey: Uint8Array) =>
190 JSON.parse(nip44.v2.decrypt(data.content, nip44ConversationKey(privateKey, data.pubkey)))
191
192const createRumor = (event: Partial<UnsignedEvent>, privateKey: Uint8Array) => {
193 const rumor = {
194 created_at: now(),
195 content: "",
196 tags: [],
197 ...event,
198 pubkey: getPublicKey(privateKey),
199 } as any
200
201 rumor.id = getEventHash(rumor)
202
203 return rumor as Rumor
204}
205
206const createSeal = (rumor: Rumor, privateKey: Uint8Array, recipientPublicKey: string) => {
207 return finalizeEvent(
208 {
209 kind: 13,
210 content: nip44Encrypt(rumor, privateKey, recipientPublicKey),
211 created_at: randomNow(),
212 tags: [],
213 },
214 privateKey
215 ) as Event
216}
217
218const createWrap = (event: Event, recipientPublicKey: string) => {
219 const randomKey = generateSecretKey()
220
221 return finalizeEvent(
222 {
223 kind: 1059,
224 content: nip44Encrypt(event, randomKey, recipientPublicKey),
225 created_at: randomNow(),
226 tags: [["p", recipientPublicKey]],
227 },
228 randomKey
229 ) as Event
230}
231
232// Test case using the above example
233const senderPrivateKey = nip19.decode(`nsec1p0ht6p3wepe47sjrgesyn4m50m6avk2waqudu9rl324cg2c4ufesyp6rdg`).data
234const recipientPrivateKey = nip19.decode(`nsec1uyyrnx7cgfp40fcskcr2urqnzekc20fj0er6de0q8qvhx34ahazsvs9p36`).data
235const recipientPublicKey = getPublicKey(recipientPrivateKey)
236
237const rumor = createRumor(
238 {
239 kind: 1,
240 content: "Are you going to the party tonight?",
241 },
242 senderPrivateKey
243)
244
245const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey)
246const wrap = createWrap(seal, recipientPublicKey)
247
248// Recipient unwraps with his/her private key.
249
250const unwrappedSeal = nip44Decrypt(wrap, recipientPrivateKey)
251const unsealedRumor = nip44Decrypt(unwrappedSeal, recipientPrivateKey)
252```