diff --git a/04.md b/04.md index 6e45b74b5e..abd79ef2aa 100644 --- a/04.md +++ b/04.md @@ -8,27 +8,50 @@ Encrypted Direct Message A special event with kind `4`, meaning "encrypted direct message". It is supposed to have the following attributes: -**`content`** MUST be equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a shared cipher generated by combining the recipient's public-key with the sender's private-key; this appended by the base64-encoded initialization vector as if it was a querystring parameter named "iv". The format is the following: `"content": "?iv="`. +**`content`** MUST be equal to the base64-encoded, aes-256-gcm encrypted string of anything a user wants to write, encrypted using a shared cipher generated by combining the recipient's public-key with the sender's private-key, which is then processed with an hkdf using the normalized key and the salt; this appended by the base64-encoded initialization vector with a "%" delimiter. The format is the following: `"content": "%%"`. NOTE: pscs7 padding is used by default in webcrypto libraries, and is assumed to be part of the standard. **`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). In Nostr, only the X coordinate of the shared point is used as the secret and it is NOT hashed. If using libsecp256k1, a custom function that copies the X coordinate must be passed as the `hashfp` argument in `secp256k1_ecdh`. See [here](https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/ecdh/main_impl.h#L29). +**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). In version 0 of Nostr NIP4, only the X coordinate of the shared point is used as the secret and it is NOT hashed. If using libsecp256k1, a custom function that copies the X coordinate must be passed as the `hashfp` argument in `secp256k1_ecdh`. See [here](https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/ecdh/main_impl.h#L29). Failing to salt and hash this correctly in version zero causes a fixed-aes-key, and a 12 byte iv (4 bytes of the iv are ignored by AES-GCM). In version 1 (below), these are salted, hashed, and gcm is used, a safer encryption standard. + +Clients SHOULD show messages received where the version number is not one they support, so that the user knows that they may want to attempt viewing the message on a compatible client. + +### Wrapped signing + +Clients MAY optionally sign messages using the NORMALIZED SHARED SECRET instead of signing with the sender's private key. In this case, a "p" tag need not be present on the message and the content should be a JSON string containing `{content, pubkey, tags?}` for the author. The relay and public will see this as a "different sender" with no linkage to the sender. The recipient can subscribe to this channel by requesting messages using the shared-secret-public-key, and can verify the message was sent this way after decrypting the content and observing that it is a JSON encoded message containing: `{pubkey, content, tags?}`. + +There is no way to prove who the sender of these messages is, the only way to distingish the sender is to extract the pubkey from the inner content. Additonal tags can, optionally, be encoded in the message. A signature may, optionally, be present on the inner message. This can prove who the sender is, if that is desirable. Otherwise, the sender can deny sending the message and there is no way to prove who sent it. + +It is not possible to subscribe to these messages without knowing who the sender is. These SHOULD be used for all conversations between known sender and recipients. + +Unsolicited messages from people who you do not follow cannot be sent this way, and may only be sent "unwrapped". If the sender is not "followed" by the recipient in some way, the receipient will not receive a notification, or see messages from the sender, since there is no "p" tag. Clients may optionally add a p tag to wrapped messages to aid in unsolicited delivery. + +### Version Requirements + +Clients MUST be capable of decrypting VERSION 0, VERSION 1, and optionally WRAPPED-SIGNED kind-04 messages. + +Clients MUST NOT continue sending VERSION 0 messages, since these can be attacked due to key-reuse, key-structure and weak iv's. + +Clients SHOULD allow blind signing in as many situations as possible, however since the sender of the message can not be proven, there may be use-cases where this is not desirable. + +### Code Code sample for generating such an event in JavaScript: ```js import crypto from 'crypto' import * as secp from '@noble/secp256k1' +import { hkdf } from '@noble/hashes/hkdf'; let sharedPoint = secp.getSharedSecret(ourPrivateKey, '02' + theirPublicKey) let sharedX = sharedPoint.slice(1, 33) - let iv = crypto.randomFillSync(new Uint8Array(16)) +let key = hkdf(sha256, sharedX, iv, undefined, 32) var cipher = crypto.createCipheriv( - 'aes-256-cbc', - Buffer.from(sharedX), + 'aes-256-gcm', + Buffer.from(key), iv ) let encryptedMessage = cipher.update(text, 'utf8', 'base64') @@ -40,13 +63,41 @@ let event = { created_at: Math.floor(Date.now() / 1000), kind: 4, tags: [['p', theirPublicKey]], - content: encryptedMessage + '?iv=' + ivBase64 + content: encryptedMessage + '%' + ivBase64 + "%1" } ``` + +Code sample for decrypting both version 0 and version 1 events in JavaScript: + +```js +import crypto from 'crypto' +import * as secp from '@noble/secp256k1' +import { hkdf } from '@noble/hashes/hkdf'; + +let [ctb64, ivb64, version] = data.split('%') +if (version != "1") + [ctb64, ivb64] = data.split('?iv=') +let iv = base64.decode(ivb64) +let sharedPoint = secp.getSharedSecret(ourPrivateKey, '02' + theirPublicKey) +let sharedX = sharedPoint.slice(1, 33) +let key +if (version == "1") + key = hkdf(sha256, sharedX, iv, undefined, 32) +else + key = sharedX + +var cipher = crypto.createDecipheriv( + 'aes-256-gcm', + Buffer.from(key), + iv +) +let decryptedMessage = cipher.update(ctb64, 'base64', 'utf8') +``` + ## 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. +This standard adhere to standards for encrypted archival stoage of data. Without blind-signing, this standard leaks metadata in the events, consider only using in situations where metadata leakage (specifically the time, sender, recipient, and the approximate 16-byte padded size is not a concern. With blind-signing, and no p tag, this standard can be considered a reasonably secure stateless communications system. It is important to understand that nostr is stateless and censorship resistant first. Use of TOR is recommended to further obscure the sender, even when using wrapped-signing. ## Client Implementation Warning