upleb.uk

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

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