Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NIP-44: Encrypted Direct Message (Versioned), replaces NIP-4 #574

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions 44.md
Original file line number Diff line number Diff line change
@@ -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", "<pubkey, as a hex string>"]`.

**`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", "<event_id>"]`.

**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.
Copy link
Contributor

@mikedilger mikedilger Jul 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about mentioning that these are Alice's read relays, the ones she reads from (in case she uses relays for just read or just write, instead of both).

... or better yet, since people are concerned about metadata, we extend NIP-65 to add a flag indicating a DM relay. That way it would be possible for people concerned with metadata leakage to setup a personal relay for receiving DMs and be confident that those DMs are not being read by interlopers (at least the receipient is confident it doesn't leak from their specified read relay, but not that the sender didn't put it elsewhere; and the sender can't really be confident of this, so this is not much of a solution for metadata leakage but it helps a smidgen).

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
}
```
paulmillr marked this conversation as resolved.
Show resolved Hide resolved