diff options
| author | fiatjaf_ <fiatjaf@gmail.com> | 2024-01-29 13:06:25 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-01-29 13:06:25 -0300 |
| commit | 9efafe22940c4465daf20cb388d00c492bc2004c (patch) | |
| tree | ee35b26fd37eba90e72903e4a1a9ba282c611720 /59.md | |
| parent | 7ec060375c5aa8697e8b26c2d1088c9077bfa910 (diff) | |
| parent | 1a2b21b67eeae2eba8c03215826a32d4a03ae172 (diff) | |
Merge pull request #716 from coracle-social/NIP-59
Introduce NIP-59 gift wrap
Diffstat (limited to '59.md')
| -rw-r--r-- | 59.md | 252 |
1 files changed, 252 insertions, 0 deletions
| @@ -0,0 +1,252 @@ | |||
| 1 | NIP-59 | ||
| 2 | ====== | ||
| 3 | |||
| 4 | Gift Wrap | ||
| 5 | --------- | ||
| 6 | |||
| 7 | `optional` | ||
| 8 | |||
| 9 | This NIP defines a protocol for encapsulating any nostr event. This makes it possible to obscure most metadata | ||
| 10 | for a given event, perform collaborative signing, and more. | ||
| 11 | |||
| 12 | This NIP *does not* define any messaging protocol. Applications of this NIP should be defined separately. | ||
| 13 | |||
| 14 | This NIP relies on [NIP-44](./44.md)'s versioned encryption algorithms. | ||
| 15 | |||
| 16 | # Overview | ||
| 17 | |||
| 18 | This 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 | |||
| 25 | This 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 | |||
| 36 | A `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 | |||
| 40 | A `seal` is a `kind:13` event that wraps a `rumor` with the sender's regular key. The `seal` is **always** encrypted | ||
| 41 | to 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 | ||
| 42 | without 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 | |||
| 56 | Tags MUST must always be empty in a `kind:13`. The inner event MUST always be unsigned. | ||
| 57 | |||
| 58 | ## 3. Gift Wrap Event Kind | ||
| 59 | |||
| 60 | A `gift wrap` event is a `kind:1059` event that wraps any other event. `tags` SHOULD include any information | ||
| 61 | needed 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 | |||
| 77 | Encryption is done following NIP-44 on the JSON-encoded event. Place the the encryption payload in the `.content` | ||
| 78 | of the wrapper event (either a `seal` or a `gift wrap`). | ||
| 79 | |||
| 80 | # Other Considerations | ||
| 81 | |||
| 82 | If 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 | |||
| 85 | The canonical `created_at` time belongs to the `rumor`. All other timestamps SHOULD be tweaked to thwart | ||
| 86 | time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps | ||
| 87 | SHOULD be in the past. | ||
| 88 | |||
| 89 | Relays may choose not to store gift wrapped events due to them not being publicly useful. Clients MAY choose | ||
| 90 | to attach a certain amount of proof-of-work to the wrapper event per NIP-13 in a bid to demonstrate that | ||
| 91 | the event is not spam or a denial-of-service attack. | ||
| 92 | |||
| 93 | To protect recipient metadata, relays SHOULD guard access to kind 1059 events based on user AUTH. When | ||
| 94 | possible, clients should only send wrapped events to relays that offer this protection. | ||
| 95 | |||
| 96 | To protect recipient metadata, relays SHOULD only serve kind 1059 events intended for the marked recipient. | ||
| 97 | When possible, clients should only send wrapped events to `read` relays for the recipient that implement | ||
| 98 | AUTH, and refuse to serve wrapped events to non-recipients. | ||
| 99 | |||
| 100 | # An Example | ||
| 101 | |||
| 102 | Let'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 | |||
| 108 | Note that this messaging protocol should not be used in practice, this is just an example. Refer to other | ||
| 109 | NIPs for concrete messaging protocols that depend on gift wraps. | ||
| 110 | |||
| 111 | ## 1. Create an event | ||
| 112 | |||
| 113 | Create a `kind 1` event with the message, the receivers, and any other tags you want, signed by the author. | ||
| 114 | Do 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 | |||
| 129 | Encrypt the JSON-encoded `rumor` with a conversation key derived using the author's private key and | ||
| 130 | the recipient's public key. Place the result in the `content` field of a `kind 13` `seal` event. Sign | ||
| 131 | it 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 | |||
| 147 | Encrypt the JSON-encoded `kind 13` event with your ephemeral, single-use random key. Place the result | ||
| 148 | in the `content` field of a `kind 1059`. Add a single `p` tag containing the recipient's public key. | ||
| 149 | Sign 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 | |||
| 165 | Broadcast 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 | ||
| 172 | import {bytesToHex} from "@noble/hashes/utils" | ||
| 173 | import type {EventTemplate, UnsignedEvent, Event} from "nostr-tools" | ||
| 174 | import {getPublicKey, getEventHash, nip19, nip44, finalizeEvent, generateSecretKey} from "nostr-tools" | ||
| 175 | |||
| 176 | type Rumor = UnsignedEvent & {id: string} | ||
| 177 | |||
| 178 | const TWO_DAYS = 2 * 24 * 60 * 60 | ||
| 179 | |||
| 180 | const now = () => Math.round(Date.now() / 1000) | ||
| 181 | const randomNow = () => Math.round(now() - (Math.random() * TWO_DAYS)) | ||
| 182 | |||
| 183 | const nip44ConversationKey = (privateKey: Uint8Array, publicKey: string) => | ||
| 184 | nip44.v2.utils.getConversationKey(bytesToHex(privateKey), publicKey) | ||
| 185 | |||
| 186 | const nip44Encrypt = (data: EventTemplate, privateKey: Uint8Array, publicKey: string) => | ||
| 187 | nip44.v2.encrypt(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey)) | ||
| 188 | |||
| 189 | const nip44Decrypt = (data: Event, privateKey: Uint8Array) => | ||
| 190 | JSON.parse(nip44.v2.decrypt(data.content, nip44ConversationKey(privateKey, data.pubkey))) | ||
| 191 | |||
| 192 | const 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 | |||
| 206 | const 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 | |||
| 218 | const 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 | ||
| 233 | const senderPrivateKey = nip19.decode(`nsec1p0ht6p3wepe47sjrgesyn4m50m6avk2waqudu9rl324cg2c4ufesyp6rdg`).data | ||
| 234 | const recipientPrivateKey = nip19.decode(`nsec1uyyrnx7cgfp40fcskcr2urqnzekc20fj0er6de0q8qvhx34ahazsvs9p36`).data | ||
| 235 | const recipientPublicKey = getPublicKey(recipientPrivateKey) | ||
| 236 | |||
| 237 | const rumor = createRumor( | ||
| 238 | { | ||
| 239 | kind: 1, | ||
| 240 | content: "Are you going to the party tonight?", | ||
| 241 | }, | ||
| 242 | senderPrivateKey | ||
| 243 | ) | ||
| 244 | |||
| 245 | const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey) | ||
| 246 | const wrap = createWrap(seal, recipientPublicKey) | ||
| 247 | |||
| 248 | // Recipient unwraps with his/her private key. | ||
| 249 | |||
| 250 | const unwrappedSeal = nip44Decrypt(wrap, recipientPrivateKey) | ||
| 251 | const unsealedRumor = nip44Decrypt(unwrappedSeal, recipientPrivateKey) | ||
| 252 | ``` | ||