diff --git a/44.md b/44.md new file mode 100644 index 0000000000..1b7ce0b5ff --- /dev/null +++ b/44.md @@ -0,0 +1,96 @@ +NIP-44 +====== + +Encrypted Direct Message (Versioned) +------------------------------------ + +`optional` `author:paulmillr` + +The NIP replaces NIP4, which is deficient with regards to algorithm choices, and introduces cryptography versioning. NIP4 is potentially vulnerable to [padding oracle attacks](https://en.wikipedia.org/wiki/Padding_oracle_attack) and uses keys which are not indistinguishable from random. + +A special event with kind `44`, meaning "encrypted direct message". It is supposed to have the following attributes: + +**`content`** MUST be equal to the CSV structure `v,param1,param2...`. Different versions may have different param count. First part is always algorithm version, a number. Params of version 1: + + 1. `nonce`: base64-encoded xchacha nonce + 2. `ciphertext`: base64-encoded xchacha ciphertext, created from (key, nonce) against `plaintext`. + Example: `1,3dBKd83Pg2Q4Tu2A2e8N++c+ZW2IBc2f,FvQi1H4atMwU+FzUR/0CJ7kowjs+` + +**`tags`** MUST contain an entry identifying the receiver of the message (such that relays may naturally forward this event to them), in the form `["p", ""]`. + +**`tags`** MAY contain an entry identifying the previous message in a conversation or a message we are explicitly replying to (such that contextual, more organized conversations may happen), in the form `["e", ""]`. + +**Note**: By default in the [libsecp256k1](https://github.com/bitcoin-core/secp256k1) ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). We are using this exact implementation. In NIP4, unhashed shared point was used. + +## Metadata leakage + +All nostr events are passed over the internet, which means some metadata would always leak. + +All compatible clients MUST implement following flow to limit leakage: + +1. Alice authenticates on `wss://nostr.example.com` using NIP-42. This would mean sending and receiving + messages would only be available to proper, authenticated users. +2. On success, Alice creates NIP-65 event specifying `wss://nostr.example.com` as her preferred relay. + The setting is public, and the network would see Alice's preferred DM relays. + Whenever a new user wants to start a conversation with Alice, they would use one of Alice's relays. + +If user did not specify NIP-65 preferred relays, they SHOULD NOT receive direct messages. + +## Versioning + +Clients MUST throw a descriptive error if they receive NIP44 message, version of which they don't support. The error must mention the message version is not supported and suggest appropriate action, such as upgrading, or switching to a different client. + +Currently defined encryption algorithms: + +- `0x00` - RESERVED +- `0x01` - XChaCha with same key `sha256(ecdh)` per conversation + +## Security Warning + +This standard does not go anywhere near what is considered the state-of-the-art in encrypted communication between peers, and it leaks metadata in the events, therefore it must not be used for anything you really need to keep secret, and only with relays that use `AUTH` to restrict who can fetch your `kind:4` events. + +## Client Implementation Warning + +Clients *should not* search and replace public key or note references from the `.content`. If processed like a regular text note (where `@npub...` is replaced with `#[0]` with a `["p", "..."]` tag) the tags are leaked and the mentioned user will receive the message in their inbox. + +## Algorithm + +```js +// npm install @noble/curves @noble/hashes @scure/base @stablelib/xchacha20 +import {xchacha20} from '@noble/ciphers/chacha' +import {secp256k1} from '@noble/curves/secp256k1' +import {sha256} from '@noble/hashes/sha256' +import {randomBytes} from '@noble/hashes/utils' +import {base64} from '@scure/base' +import {utf8Decoder, utf8Encoder} from './utils.ts' + +export function getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { + const key = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB) + return sha256(key.subarray(1, 33)) +} + +export function encrypt( + key: Uint8Array, + text: string, + ver = 1 +): string { + if (ver !== 1) throw new Error('NIP44: unknown encryption version') + let nonce = randomBytes(24) + let plaintext = utf8Encoder.encode(text) + let ciphertext = xchacha20(key, nonce, plaintext, plaintext) + return `1,${base64.encode(nonce)},${base64.encode(ciphertext)}` +} + +export function decrypt(key: Uint8Array, data: string): string { + let dt = data.split(',') + if (dt.length !== 3) throw new Error('NIP44: unknown encryption version'); + let v = Number.parseInt(dt[0]) + if (v !== 1) throw new Error('NIP44: unknown encryption version') + + let nonce = base64.decode(dt[1]) + let ciphertext = base64.decode(dt[2]) + let plaintext = xchacha20(key, nonce, ciphertext) + let text = utf8Decoder.decode(plaintext) + return text +} +```