diff options
| -rw-r--r-- | 01.md | 13 | ||||
| -rw-r--r-- | 30.md | 2 | ||||
| -rw-r--r-- | 47.md | 280 | ||||
| -rw-r--r-- | 49.md | 112 | ||||
| -rw-r--r-- | 52.md | 4 | ||||
| -rw-r--r-- | 72.md | 10 | ||||
| -rw-r--r-- | 96.md | 2 | ||||
| -rw-r--r-- | README.md | 5 |
8 files changed, 409 insertions, 19 deletions
| @@ -14,7 +14,7 @@ Each user has a keypair. Signatures, public key, and encodings are done accordin | |||
| 14 | 14 | ||
| 15 | The only object type that exists is the `event`, which has the following format on the wire: | 15 | The only object type that exists is the `event`, which has the following format on the wire: |
| 16 | 16 | ||
| 17 | ```json | 17 | ```jsonc |
| 18 | { | 18 | { |
| 19 | "id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>, | 19 | "id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>, |
| 20 | "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, | 20 | "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, |
| @@ -22,7 +22,7 @@ The only object type that exists is the `event`, which has the following format | |||
| 22 | "kind": <integer between 0 and 65535>, | 22 | "kind": <integer between 0 and 65535>, |
| 23 | "tags": [ | 23 | "tags": [ |
| 24 | [<arbitrary string>...], | 24 | [<arbitrary string>...], |
| 25 | ... | 25 | // ... |
| 26 | ], | 26 | ], |
| 27 | "content": <arbitrary string>, | 27 | "content": <arbitrary string>, |
| 28 | "sig": <64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field> | 28 | "sig": <64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field> |
| @@ -58,17 +58,16 @@ To prevent implementation differences from creating a different event ID for the | |||
| 58 | 58 | ||
| 59 | Each tag is an array of strings of arbitrary size, with some conventions around them. Take a look at the example below: | 59 | Each tag is an array of strings of arbitrary size, with some conventions around them. Take a look at the example below: |
| 60 | 60 | ||
| 61 | ```json | 61 | ```jsonc |
| 62 | { | 62 | { |
| 63 | ..., | ||
| 64 | "tags": [ | 63 | "tags": [ |
| 65 | ["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com"], | 64 | ["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com"], |
| 66 | ["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"], | 65 | ["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"], |
| 67 | ["a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com"], | 66 | ["a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com"], |
| 68 | ["alt", "reply"], | 67 | ["alt", "reply"], |
| 69 | ... | 68 | // ... |
| 70 | ], | 69 | ], |
| 71 | ... | 70 | // ... |
| 72 | } | 71 | } |
| 73 | ``` | 72 | ``` |
| 74 | 73 | ||
| @@ -125,7 +124,7 @@ Clients can send 3 types of messages, which must be JSON arrays, according to th | |||
| 125 | "ids": <a list of event ids>, | 124 | "ids": <a list of event ids>, |
| 126 | "authors": <a list of lowercase pubkeys, the pubkey of an event must be one of these>, | 125 | "authors": <a list of lowercase pubkeys, the pubkey of an event must be one of these>, |
| 127 | "kinds": <a list of a kind numbers>, | 126 | "kinds": <a list of a kind numbers>, |
| 128 | "#<single-letter (a-zA-Z)>": <a list of tag values, for #e — a list of event ids, for #p — a list of event pubkeys etc>, | 127 | "#<single-letter (a-zA-Z)>": <a list of tag values, for #e — a list of event ids, for #p — a list of pubkeys, etc.>, |
| 129 | "since": <an integer unix timestamp in seconds, events must be newer than this to pass>, | 128 | "since": <an integer unix timestamp in seconds, events must be newer than this to pass>, |
| 130 | "until": <an integer unix timestamp in seconds, events must be older than this to pass>, | 129 | "until": <an integer unix timestamp in seconds, events must be older than this to pass>, |
| 131 | "limit": <maximum number of events relays SHOULD return in the initial query> | 130 | "limit": <maximum number of events relays SHOULD return in the initial query> |
| @@ -6,7 +6,7 @@ Custom Emoji | |||
| 6 | 6 | ||
| 7 | `draft` `optional` | 7 | `draft` `optional` |
| 8 | 8 | ||
| 9 | Custom emoji may be added to **kind 0** and **kind 1** events by including one or more `"emoji"` tags, in the form: | 9 | Custom emoji may be added to **kind 0**, **kind 1**, **kind 7** ([NIP-25](25.md)) and **kind 30315** ([NIP-38](38.md)) events by including one or more `"emoji"` tags, in the form: |
| 10 | 10 | ||
| 11 | ``` | 11 | ``` |
| 12 | ["emoji", <shortcode>, <image-url>] | 12 | ["emoji", <shortcode>, <image-url>] |
| @@ -17,7 +17,7 @@ This NIP describes a way for clients to access a remote Lightning wallet through | |||
| 17 | * **wallet service**: Nostr app that typically runs on an always-on computer (eg. in the cloud or on a Raspberry Pi). This app has access to the APIs of the wallets it serves. | 17 | * **wallet service**: Nostr app that typically runs on an always-on computer (eg. in the cloud or on a Raspberry Pi). This app has access to the APIs of the wallets it serves. |
| 18 | 18 | ||
| 19 | ## Theory of Operation | 19 | ## Theory of Operation |
| 20 | 1. **Users** who which to use this NIP to send lightning payments to other nostr users must first acquire a special "connection" URI from their NIP-47 compliant wallet application. The wallet application may provide this URI using a QR screen, or a pasteable string, or some other means. | 20 | 1. **Users** who wish to use this NIP to send lightning payments to other nostr users must first acquire a special "connection" URI from their NIP-47 compliant wallet application. The wallet application may provide this URI using a QR screen, or a pasteable string, or some other means. |
| 21 | 21 | ||
| 22 | 2. The **user** should then copy this URI into their **client(s)** by pasting, or scanning the QR, etc. The **client(s)** should save this URI and use it later whenever the **user** makes a payment. The **client** should then request an `info` (13194) event from the relay(s) specified in the URI. The **wallet service** will have sent that event to those relays earlier, and the relays will hold it as a replaceable event. | 22 | 2. The **user** should then copy this URI into their **client(s)** by pasting, or scanning the QR, etc. The **client(s)** should save this URI and use it later whenever the **user** makes a payment. The **client** should then request an `info` (13194) event from the relay(s) specified in the URI. The **wallet service** will have sent that event to those relays earlier, and the relays will hold it as a replaceable event. |
| 23 | 23 | ||
| @@ -36,6 +36,7 @@ The info event should be a replaceable event that is published by the **wallet s | |||
| 36 | a plaintext string with the supported commands, space-separated, eg. `pay_invoice get_balance`. Only the `pay_invoice` command is described in this NIP, but other commands might be defined in different NIPs. | 36 | a plaintext string with the supported commands, space-separated, eg. `pay_invoice get_balance`. Only the `pay_invoice` command is described in this NIP, but other commands might be defined in different NIPs. |
| 37 | 37 | ||
| 38 | Both the request and response events SHOULD contain one `p` tag, containing the public key of the **wallet service** if this is a request, and the public key of the **user** if this is a response. The response event SHOULD contain an `e` tag with the id of the request event it is responding to. | 38 | Both the request and response events SHOULD contain one `p` tag, containing the public key of the **wallet service** if this is a request, and the public key of the **user** if this is a response. The response event SHOULD contain an `e` tag with the id of the request event it is responding to. |
| 39 | Optionally, a request can have an `expiration` tag that has a unix timestamp in seconds. If the request is received after this timestamp, it should be ignored. | ||
| 39 | 40 | ||
| 40 | The content of requests and responses is encrypted with [NIP04](https://github.com/nostr-protocol/nips/blob/master/04.md), and is a JSON-RPCish object with a semi-fixed structure: | 41 | The content of requests and responses is encrypted with [NIP04](https://github.com/nostr-protocol/nips/blob/master/04.md), and is a JSON-RPCish object with a semi-fixed structure: |
| 41 | 42 | ||
| @@ -108,7 +109,8 @@ Request: | |||
| 108 | { | 109 | { |
| 109 | "method": "pay_invoice", | 110 | "method": "pay_invoice", |
| 110 | "params": { | 111 | "params": { |
| 111 | "invoice": "lnbc50n1..." // bolt11 invoice | 112 | "invoice": "lnbc50n1...", // bolt11 invoice |
| 113 | "amount": 123, // invoice amount in msats, optional | ||
| 112 | } | 114 | } |
| 113 | } | 115 | } |
| 114 | ``` | 116 | ``` |
| @@ -117,7 +119,7 @@ Response: | |||
| 117 | ```jsonc | 119 | ```jsonc |
| 118 | { | 120 | { |
| 119 | "result_type": "pay_invoice", | 121 | "result_type": "pay_invoice", |
| 120 | "result": { | 122 | "result": { |
| 121 | "preimage": "0123456789abcdef..." // preimage of the payment | 123 | "preimage": "0123456789abcdef..." // preimage of the payment |
| 122 | } | 124 | } |
| 123 | } | 125 | } |
| @@ -126,6 +128,278 @@ Response: | |||
| 126 | Errors: | 128 | Errors: |
| 127 | - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. | 129 | - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. |
| 128 | 130 | ||
| 131 | ### `multi_pay_invoice` | ||
| 132 | |||
| 133 | Description: Requests payment of multiple invoices. | ||
| 134 | |||
| 135 | Request: | ||
| 136 | ```jsonc | ||
| 137 | { | ||
| 138 | "method": "multi_pay_invoice", | ||
| 139 | "params": { | ||
| 140 | "invoices": [ | ||
| 141 | {"id":"4da52c32a1", "invoice": "lnbc1...", "amount": 123}, // bolt11 invoice and amount in msats, amount is optional | ||
| 142 | {"id":"3da52c32a1", "invoice": "lnbc50n1..."}, | ||
| 143 | ], | ||
| 144 | } | ||
| 145 | } | ||
| 146 | ``` | ||
| 147 | |||
| 148 | Response: | ||
| 149 | |||
| 150 | For every invoice in the request, a separate response event is sent. To differentiate between the responses, each | ||
| 151 | response event contains an `d` tag with the id of the invoice it is responding to, if no id was given, then the | ||
| 152 | payment hash of the invoice should be used. | ||
| 153 | |||
| 154 | ```jsonc | ||
| 155 | { | ||
| 156 | "result_type": "multi_pay_invoice", | ||
| 157 | "result": { | ||
| 158 | "preimage": "0123456789abcdef..." // preimage of the payment | ||
| 159 | } | ||
| 160 | } | ||
| 161 | ``` | ||
| 162 | |||
| 163 | Errors: | ||
| 164 | - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. | ||
| 165 | |||
| 166 | ### `pay_keysend` | ||
| 167 | |||
| 168 | Request: | ||
| 169 | ```jsonc | ||
| 170 | { | ||
| 171 | "method": "pay_keysend", | ||
| 172 | "params": { | ||
| 173 | "amount": 123, // invoice amount in msats, required | ||
| 174 | "pubkey": "03...", // payee pubkey, required | ||
| 175 | "preimage": "0123456789abcdef...", // preimage of the payment, optional | ||
| 176 | "tlv_records: [ // tlv records, optional | ||
| 177 | { | ||
| 178 | "type": 5482373484, // tlv type | ||
| 179 | "value": "0123456789abcdef" // hex encoded tlv value | ||
| 180 | } | ||
| 181 | ] | ||
| 182 | } | ||
| 183 | } | ||
| 184 | ``` | ||
| 185 | |||
| 186 | Response: | ||
| 187 | ```jsonc | ||
| 188 | { | ||
| 189 | "result_type": "pay_keysend", | ||
| 190 | "result": { | ||
| 191 | "preimage": "0123456789abcdef...", // preimage of the payment | ||
| 192 | } | ||
| 193 | } | ||
| 194 | ``` | ||
| 195 | |||
| 196 | Errors: | ||
| 197 | - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. | ||
| 198 | |||
| 199 | ### `multi_pay_keysend` | ||
| 200 | |||
| 201 | Description: Requests multiple keysend payments. | ||
| 202 | |||
| 203 | Has an array of keysends, these follow the same semantics as `pay_keysend`, just done in a batch | ||
| 204 | |||
| 205 | Request: | ||
| 206 | ```jsonc | ||
| 207 | { | ||
| 208 | "method": "multi_pay_keysend", | ||
| 209 | "params": { | ||
| 210 | "keysends": [ | ||
| 211 | {"id": "4c5b24a351", pubkey": "03...", "amount": 123}, | ||
| 212 | {"id": "3da52c32a1", "pubkey": "02...", "amount": 567, "preimage": "abc123..", "tlv_records": [{"type": 696969, "value": "77616c5f6872444873305242454d353736"}]}, | ||
| 213 | ], | ||
| 214 | } | ||
| 215 | } | ||
| 216 | ``` | ||
| 217 | |||
| 218 | Response: | ||
| 219 | |||
| 220 | For every keysend in the request, a separate response event is sent. To differentiate between the responses, each | ||
| 221 | response event contains an `d` tag with the id of the keysend it is responding to, if no id was given, then the | ||
| 222 | pubkey should be used. | ||
| 223 | |||
| 224 | ```jsonc | ||
| 225 | { | ||
| 226 | "result_type": "multi_pay_keysend", | ||
| 227 | "result": { | ||
| 228 | "preimage": "0123456789abcdef..." // preimage of the payment | ||
| 229 | } | ||
| 230 | } | ||
| 231 | ``` | ||
| 232 | |||
| 233 | Errors: | ||
| 234 | - `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. | ||
| 235 | |||
| 236 | ### `make_invoice` | ||
| 237 | |||
| 238 | Request: | ||
| 239 | ```jsonc | ||
| 240 | { | ||
| 241 | "method": "make_invoice", | ||
| 242 | "params": { | ||
| 243 | "amount": 123, // value in msats | ||
| 244 | "description": "string", // invoice's description, optional | ||
| 245 | "description_hash": "string", // invoice's description hash, optional | ||
| 246 | "expiry": 213 // expiry in seconds from time invoice is created, optional | ||
| 247 | } | ||
| 248 | } | ||
| 249 | ``` | ||
| 250 | |||
| 251 | Response: | ||
| 252 | ```jsonc | ||
| 253 | { | ||
| 254 | "result_type": "make_invoice", | ||
| 255 | "result": { | ||
| 256 | "type": "incoming", // "incoming" for invoices, "outgoing" for payments | ||
| 257 | "invoice": "string", // encoded invoice, optional | ||
| 258 | "description": "string", // invoice's description, optional | ||
| 259 | "description_hash": "string", // invoice's description hash, optional | ||
| 260 | "preimage": "string", // payment's preimage, optional if unpaid | ||
| 261 | "payment_hash": "string", // Payment hash for the payment | ||
| 262 | "amount": 123, // value in msats | ||
| 263 | "fees_paid": 123, // value in msats | ||
| 264 | "created_at": unixtimestamp, // invoice/payment creation time | ||
| 265 | "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable | ||
| 266 | "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. | ||
| 267 | } | ||
| 268 | } | ||
| 269 | ``` | ||
| 270 | |||
| 271 | ### `lookup_invoice` | ||
| 272 | |||
| 273 | Request: | ||
| 274 | ```jsonc | ||
| 275 | { | ||
| 276 | "method": "lookup_invoice", | ||
| 277 | "params": { | ||
| 278 | "payment_hash": "31afdf1..", // payment hash of the invoice, one of payment_hash or invoice is required | ||
| 279 | "invoice": "lnbc50n1..." // invoice to lookup | ||
| 280 | } | ||
| 281 | } | ||
| 282 | ``` | ||
| 283 | |||
| 284 | Response: | ||
| 285 | ```jsonc | ||
| 286 | { | ||
| 287 | "result_type": "lookup_invoice", | ||
| 288 | "result": { | ||
| 289 | "type": "incoming", // "incoming" for invoices, "outgoing" for payments | ||
| 290 | "invoice": "string", // encoded invoice, optional | ||
| 291 | "description": "string", // invoice's description, optional | ||
| 292 | "description_hash": "string", // invoice's description hash, optional | ||
| 293 | "preimage": "string", // payment's preimage, optional if unpaid | ||
| 294 | "payment_hash": "string", // Payment hash for the payment | ||
| 295 | "amount": 123, // value in msats | ||
| 296 | "fees_paid": 123, // value in msats | ||
| 297 | "created_at": unixtimestamp, // invoice/payment creation time | ||
| 298 | "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable | ||
| 299 | "settled_at": unixtimestamp, // invoice/payment settlement time, optional if unpaid | ||
| 300 | "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. | ||
| 301 | } | ||
| 302 | } | ||
| 303 | ``` | ||
| 304 | |||
| 305 | Errors: | ||
| 306 | - `NOT_FOUND`: The invoice could not be found by the given parameters. | ||
| 307 | |||
| 308 | ### `list_transactions` | ||
| 309 | |||
| 310 | Lists invoices and payments. If `type` is not specified, both invoices and payments are returned. | ||
| 311 | The `from` and `until` parameters are timestamps in seconds since epoch. If `from` is not specified, it defaults to 0. | ||
| 312 | If `until` is not specified, it defaults to the current time. Transactions are returned in descending order of creation | ||
| 313 | time. | ||
| 314 | |||
| 315 | Request: | ||
| 316 | ```jsonc | ||
| 317 | { | ||
| 318 | "method": "list_transactions", | ||
| 319 | "params": { | ||
| 320 | "from": 1693876973, // starting timestamp in seconds since epoch (inclusive), optional | ||
| 321 | "until": 1703225078, // ending timestamp in seconds since epoch (inclusive), optional | ||
| 322 | "limit": 10, // maximum number of invoices to return, optional | ||
| 323 | "offset": 0, // offset of the first invoice to return, optional | ||
| 324 | "unpaid": true, // include unpaid invoices, optional, default false | ||
| 325 | "type": "incoming", // "incoming" for invoices, "outgoing" for payments, undefined for both | ||
| 326 | } | ||
| 327 | } | ||
| 328 | ``` | ||
| 329 | |||
| 330 | Response: | ||
| 331 | ```jsonc | ||
| 332 | { | ||
| 333 | "result_type": "list_transactions", | ||
| 334 | "result": { | ||
| 335 | "transactions": [ | ||
| 336 | { | ||
| 337 | "type": "incoming", // "incoming" for invoices, "outgoing" for payments | ||
| 338 | "invoice": "string", // encoded invoice, optional | ||
| 339 | "description": "string", // invoice's description, optional | ||
| 340 | "description_hash": "string", // invoice's description hash, optional | ||
| 341 | "preimage": "string", // payment's preimage, optional if unpaid | ||
| 342 | "payment_hash": "string", // Payment hash for the payment | ||
| 343 | "amount": 123, // value in msats | ||
| 344 | "fees_paid": 123, // value in msats | ||
| 345 | "created_at": unixtimestamp, // invoice/payment creation time | ||
| 346 | "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable | ||
| 347 | "settled_at": unixtimestamp, // invoice/payment settlement time, optional if unpaid | ||
| 348 | "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. | ||
| 349 | } | ||
| 350 | ], | ||
| 351 | }, | ||
| 352 | } | ||
| 353 | ``` | ||
| 354 | |||
| 355 | ### `get_balance` | ||
| 356 | |||
| 357 | Request: | ||
| 358 | ```jsonc | ||
| 359 | { | ||
| 360 | "method": "get_balance", | ||
| 361 | "params": { | ||
| 362 | } | ||
| 363 | } | ||
| 364 | ``` | ||
| 365 | |||
| 366 | Response: | ||
| 367 | ```jsonc | ||
| 368 | { | ||
| 369 | "result_type": "get_balance", | ||
| 370 | "result": { | ||
| 371 | "balance": 10000, // user's balance in msats | ||
| 372 | } | ||
| 373 | } | ||
| 374 | ``` | ||
| 375 | |||
| 376 | ### `get_info` | ||
| 377 | |||
| 378 | Request: | ||
| 379 | ```jsonc | ||
| 380 | { | ||
| 381 | "method": "get_info", | ||
| 382 | "params": { | ||
| 383 | } | ||
| 384 | } | ||
| 385 | ``` | ||
| 386 | |||
| 387 | Response: | ||
| 388 | ```jsonc | ||
| 389 | { | ||
| 390 | "result_type": "get_info", | ||
| 391 | "result": { | ||
| 392 | "alias": "string", | ||
| 393 | "color": "hex string", | ||
| 394 | "pubkey": "hex string", | ||
| 395 | "network": "string", // mainnet, testnet, signet, or regtest | ||
| 396 | "block_height": 1, | ||
| 397 | "block_hash": "hex string", | ||
| 398 | "methods": ["pay_invoice", "get_balance", "make_invoice", "lookup_invoice", "list_transactions", "get_info"], // list of supported methods for this connection | ||
| 399 | } | ||
| 400 | } | ||
| 401 | ``` | ||
| 402 | |||
| 129 | ## Example pay invoice flow | 403 | ## Example pay invoice flow |
| 130 | 404 | ||
| 131 | 0. The user scans the QR code generated by the **wallet service** with their **client** application, they follow a `nostr+walletconnect:` deeplink or configure the connection details manually. | 405 | 0. The user scans the QR code generated by the **wallet service** with their **client** application, they follow a `nostr+walletconnect:` deeplink or configure the connection details manually. |
| @@ -0,0 +1,112 @@ | |||
| 1 | |||
| 2 | NIP-49 | ||
| 3 | ====== | ||
| 4 | |||
| 5 | Private Key Encryption | ||
| 6 | ---------------------- | ||
| 7 | |||
| 8 | `draft` `optional` | ||
| 9 | |||
| 10 | This NIP defines a method by which clients can encrypt (and decrypt) a user's private key with a passphrase. | ||
| 11 | |||
| 12 | Symmetric Encryption Key derivation | ||
| 13 | ----------------------------------- | ||
| 14 | |||
| 15 | PASSPHRASE = read from the user | ||
| 16 | |||
| 17 | LOG\_N = Let the user or implementer choose one byte representing a power of 2 (e.g. 18 represents 262,144) which is used as the number of rounds for scrypt. Larger numbers take more time and more memory, and offer better protection: | ||
| 18 | |||
| 19 | | LOG\_N | MEMORY REQUIRED | APPROX TIME ON FAST COMPUTER | | ||
| 20 | |--------|-----------------|----------------------------- | | ||
| 21 | | 16 | 64 MiB | 100 ms | | ||
| 22 | | 18 | 256 MiB | | | ||
| 23 | | 20 | 1 GiB | 2 seconds | | ||
| 24 | | 21 | 2 GiB | | | ||
| 25 | | 22 | 4 GiB | | | ||
| 26 | |||
| 27 | SALT = 16 random bytes | ||
| 28 | |||
| 29 | SYMMETRIC_KEY = scrypt(passphrase=PASSPHRASE, salt=SALT, log\_n=LOG\_N, r=8, p=1) | ||
| 30 | |||
| 31 | The symmetric key should be 32 bytes long. | ||
| 32 | |||
| 33 | This symmetric encryption key is temporary and should be zeroed and discarded after use and not stored or reused for any other purpose. | ||
| 34 | |||
| 35 | |||
| 36 | Encrypting a private key | ||
| 37 | ------------------------ | ||
| 38 | |||
| 39 | The private key encryption process is as follows: | ||
| 40 | |||
| 41 | PRIVATE\_KEY = User's private (secret) secp256k1 key as 32 raw bytes (not hex or bech32 encoded!) | ||
| 42 | |||
| 43 | KEY\_SECURITY\_BYTE = one of: | ||
| 44 | |||
| 45 | * 0x00 - if the key has been known to have been handled insecurely (stored unencrypted, cut and paste unencrypted, etc) | ||
| 46 | * 0x01 - if the key has NOT been known to have been handled insecurely (stored unencrypted, cut and paste unencrypted, etc) | ||
| 47 | * 0x02 - if the client does not track this data | ||
| 48 | |||
| 49 | ASSOCIATED\_DATA = KEY\_SECURITY\_BYTE | ||
| 50 | |||
| 51 | NONCE = 24 byte random nonce | ||
| 52 | |||
| 53 | CIPHERTEXT = XChaCha20-Poly1305( | ||
| 54 | plaintext=PRIVATE\_KEY, | ||
| 55 | associated_data=ASSOCIATED\_DATA, | ||
| 56 | nonce=NONCE, | ||
| 57 | key=SYMMETRIC\_KEY | ||
| 58 | ) | ||
| 59 | |||
| 60 | VERSION\_NUMBER = 0x02 | ||
| 61 | |||
| 62 | CIPHERTEXT_CONCATENATION = concat( | ||
| 63 | VERSION\_NUMBER, | ||
| 64 | LOG\_N, | ||
| 65 | SALT, | ||
| 66 | NONCE, | ||
| 67 | ASSOCIATED\_DATA, | ||
| 68 | CIPHERTEXT | ||
| 69 | ) | ||
| 70 | |||
| 71 | ENCRYPTED\_PRIVATE\_KEY = bech32_encode('ncryptsec', CIPHERTEXT\_CONCATENATION) | ||
| 72 | |||
| 73 | The output prior to bech32 encoding should be 91 bytes long. | ||
| 74 | |||
| 75 | The decryption process operates in the reverse. | ||
| 76 | |||
| 77 | |||
| 78 | Test Data | ||
| 79 | --------- | ||
| 80 | |||
| 81 | The following encrypted private key: | ||
| 82 | |||
| 83 | `ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p` | ||
| 84 | |||
| 85 | When decrypted with password='nostr' and log_n=16 yields the following hex-encoded private key: | ||
| 86 | |||
| 87 | `3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683` | ||
| 88 | |||
| 89 | The reverse process is non-deterministic due to the random nonce. | ||
| 90 | |||
| 91 | Discussion | ||
| 92 | ---------- | ||
| 93 | |||
| 94 | ### On Key Derivation | ||
| 95 | |||
| 96 | Passwords make poor cryptographic keys. Prior to use as a cryptographic key, two things need to happen: | ||
| 97 | |||
| 98 | 1. An encryption key needs to be deterministically created from the password such that is has a uniform functionally random distribution of bits, such that the symmetric encryption algorithm's assumptions are valid, and | ||
| 99 | 2. A slow irreversible algorithm should be injected into the process, so that brute-force attempts to decrypt by trying many passwords are severely hampered. | ||
| 100 | |||
| 101 | These are achieved using a password-based key derivation function. We use scrypt, which has been proven to be maximally memory hard and which several cryptographers have indicated to the author is better than argon2 even though argon2 won a competition in 2015. | ||
| 102 | |||
| 103 | ### On the symmetric encryption algorithm | ||
| 104 | |||
| 105 | XChaCha20-Poly1305 is typically favored by cryptographers over AES and is less associated with the U.S. government. It (or it's earlier variant without the 'X') is gaining wide usage, is used in TLS and OpenSSH, and is available in most modern crypto libraries. | ||
| 106 | |||
| 107 | Recommendations | ||
| 108 | --------- | ||
| 109 | |||
| 110 | It is not recommended that users publish these encrypted private keys to nostr, as cracking a key may become easier when an attacker can amass many encrypted private keys. | ||
| 111 | |||
| 112 | It is recommended that clients zero out the memory of passwords and private keys before freeing that memory. | ||
| @@ -38,7 +38,7 @@ The list of tags are as follows: | |||
| 38 | The following tags are deprecated: | 38 | The following tags are deprecated: |
| 39 | * `name` name of the calendar event. Use only if `title` is not available. | 39 | * `name` name of the calendar event. Use only if `title` is not available. |
| 40 | 40 | ||
| 41 | ```json | 41 | ```jsonc |
| 42 | { | 42 | { |
| 43 | "id": <32-bytes lowercase hex-encoded SHA-256 of the the serialized event data>, | 43 | "id": <32-bytes lowercase hex-encoded SHA-256 of the the serialized event data>, |
| 44 | "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, | 44 | "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, |
| @@ -99,7 +99,7 @@ The list of tags are as follows: | |||
| 99 | The following tags are deprecated: | 99 | The following tags are deprecated: |
| 100 | * `name` name of the calendar event. Use only if `title` is not available. | 100 | * `name` name of the calendar event. Use only if `title` is not available. |
| 101 | 101 | ||
| 102 | ```json | 102 | ```jsonc |
| 103 | { | 103 | { |
| 104 | "id": <32-bytes lowercase hex-encoded SHA-256 of the the serialized event data>, | 104 | "id": <32-bytes lowercase hex-encoded SHA-256 of the the serialized event data>, |
| 105 | "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, | 105 | "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, |
| @@ -12,7 +12,7 @@ The goal of this NIP is to create moderator-approved public communities around a | |||
| 12 | 12 | ||
| 13 | `kind:34550` SHOULD include any field that helps define the community and the set of moderators. `relay` tags MAY be used to describe the preferred relay to download requests and approvals. | 13 | `kind:34550` SHOULD include any field that helps define the community and the set of moderators. `relay` tags MAY be used to describe the preferred relay to download requests and approvals. |
| 14 | 14 | ||
| 15 | ```json | 15 | ```jsonc |
| 16 | { | 16 | { |
| 17 | "created_at": <Unix timestamp in seconds>, | 17 | "created_at": <Unix timestamp in seconds>, |
| 18 | "kind": 34550, | 18 | "kind": 34550, |
| @@ -42,14 +42,14 @@ The goal of this NIP is to create moderator-approved public communities around a | |||
| 42 | 42 | ||
| 43 | Any Nostr event can be submitted to a community by anyone for approval. Clients MUST add the community's `a` tag to the new post event in order to be presented for the moderator's approval. | 43 | Any Nostr event can be submitted to a community by anyone for approval. Clients MUST add the community's `a` tag to the new post event in order to be presented for the moderator's approval. |
| 44 | 44 | ||
| 45 | ```json | 45 | ```jsonc |
| 46 | { | 46 | { |
| 47 | "kind": 1, | 47 | "kind": 1, |
| 48 | "tags": [ | 48 | "tags": [ |
| 49 | ["a", "34550:<community event author pubkey>:<community-d-identifier>", "<optional-relay-url>"], | 49 | ["a", "34550:<community event author pubkey>:<community-d-identifier>", "<optional-relay-url>"], |
| 50 | ], | 50 | ], |
| 51 | "content": "hello world", | 51 | "content": "hello world", |
| 52 | ... | 52 | // ... |
| 53 | } | 53 | } |
| 54 | ``` | 54 | ``` |
| 55 | 55 | ||
| @@ -59,7 +59,7 @@ Community management clients MAY filter all mentions to a given `kind:34550` eve | |||
| 59 | 59 | ||
| 60 | The post-approval event MUST include `a` tags of the communities the moderator is posting into (one or more), the `e` tag of the post and `p` tag of the author of the post (for approval notifications). The event SHOULD also include the stringified `post request` event inside the `.content` ([NIP-18-style](18.md)) and a `k` tag with the original post's event kind to allow filtering of approved posts by kind. | 60 | The post-approval event MUST include `a` tags of the communities the moderator is posting into (one or more), the `e` tag of the post and `p` tag of the author of the post (for approval notifications). The event SHOULD also include the stringified `post request` event inside the `.content` ([NIP-18-style](18.md)) and a `k` tag with the original post's event kind to allow filtering of approved posts by kind. |
| 61 | 61 | ||
| 62 | ```json | 62 | ```jsonc |
| 63 | { | 63 | { |
| 64 | "pubkey": "<32-bytes lowercase hex-encoded public key of the event creator>", | 64 | "pubkey": "<32-bytes lowercase hex-encoded public key of the event creator>", |
| 65 | "kind": 4550, | 65 | "kind": 4550, |
| @@ -70,7 +70,7 @@ The post-approval event MUST include `a` tags of the communities the moderator i | |||
| 70 | ["k", "<post-request-kind>"] | 70 | ["k", "<post-request-kind>"] |
| 71 | ], | 71 | ], |
| 72 | "content": "<the full approved event, JSON-encoded>", | 72 | "content": "<the full approved event, JSON-encoded>", |
| 73 | ... | 73 | // ... |
| 74 | } | 74 | } |
| 75 | ``` | 75 | ``` |
| 76 | 76 | ||
| @@ -273,7 +273,7 @@ The `server` should reject deletes from users other than the original uploader. | |||
| 273 | It should be noted that more than one user may have uploaded the same file (with the same hash). In this case, a delete must not really delete the file but just remove the user's `pubkey` from the file owners list (considering the server keeps just one copy of the same file, because multiple uploads of the same file results | 273 | It should be noted that more than one user may have uploaded the same file (with the same hash). In this case, a delete must not really delete the file but just remove the user's `pubkey` from the file owners list (considering the server keeps just one copy of the same file, because multiple uploads of the same file results |
| 274 | in the same file hash). | 274 | in the same file hash). |
| 275 | 275 | ||
| 276 | The successfull response is a 200 OK one with just basic JSON fields: | 276 | The successful response is a 200 OK one with just basic JSON fields: |
| 277 | 277 | ||
| 278 | ``` | 278 | ``` |
| 279 | { | 279 | { |
| @@ -57,6 +57,7 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos | |||
| 57 | - [NIP-46: Nostr Connect](46.md) | 57 | - [NIP-46: Nostr Connect](46.md) |
| 58 | - [NIP-47: Wallet Connect](47.md) | 58 | - [NIP-47: Wallet Connect](47.md) |
| 59 | - [NIP-48: Proxy Tags](48.md) | 59 | - [NIP-48: Proxy Tags](48.md) |
| 60 | - [NIP-49: Private Key Encryption](49.md) | ||
| 60 | - [NIP-50: Search Capability](50.md) | 61 | - [NIP-50: Search Capability](50.md) |
| 61 | - [NIP-51: Lists](51.md) | 62 | - [NIP-51: Lists](51.md) |
| 62 | - [NIP-52: Calendar Events](52.md) | 63 | - [NIP-52: Calendar Events](52.md) |
| @@ -94,6 +95,8 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos | |||
| 94 | | `42` | Channel Message | [28](28.md) | | 95 | | `42` | Channel Message | [28](28.md) | |
| 95 | | `43` | Channel Hide Message | [28](28.md) | | 96 | | `43` | Channel Hide Message | [28](28.md) | |
| 96 | | `44` | Channel Mute User | [28](28.md) | | 97 | | `44` | Channel Mute User | [28](28.md) | |
| 98 | | `1021` | Bid | [15](15.md) | | ||
| 99 | | `1022` | Bid confirmation | [15](15.md) | | ||
| 97 | | `1040` | OpenTimestamps | [03](03.md) | | 100 | | `1040` | OpenTimestamps | [03](03.md) | |
| 98 | | `1063` | File Metadata | [94](94.md) | | 101 | | `1063` | File Metadata | [94](94.md) | |
| 99 | | `1311` | Live Chat Message | [53](53.md) | | 102 | | `1311` | Live Chat Message | [53](53.md) | |
| @@ -136,6 +139,8 @@ They exist to document what may be implemented by [Nostr](https://github.com/nos | |||
| 136 | | `30015` | Interest sets | [51](51.md) | | 139 | | `30015` | Interest sets | [51](51.md) | |
| 137 | | `30017` | Create or update a stall | [15](15.md) | | 140 | | `30017` | Create or update a stall | [15](15.md) | |
| 138 | | `30018` | Create or update a product | [15](15.md) | | 141 | | `30018` | Create or update a product | [15](15.md) | |
| 142 | | `30019` | Marketplace UI/UX | [15](15.md) | | ||
| 143 | | `30020` | Product sold as an auction | [15](15.md) | | ||
| 139 | | `30023` | Long-form Content | [23](23.md) | | 144 | | `30023` | Long-form Content | [23](23.md) | |
| 140 | | `30024` | Draft Long-form Content | [23](23.md) | | 145 | | `30024` | Draft Long-form Content | [23](23.md) | |
| 141 | | `30030` | Emoji sets | [51](51.md) | | 146 | | `30030` | Emoji sets | [51](51.md) | |