Skip to content
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
209 changes: 209 additions & 0 deletions 43.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
NIP-43
======

CK-based DM
-----------

`draft` `optional`

This direct message (DM) scheme between two participants is
based on the [NIP-44](44.md) conversation key (CK), encrypted and hides
all metadata but the DM's `.created_at` field for retaining correct
filtering sort order.

## Flow Summary

- An user can freely send any number of DMs to another user by signing [NIP-59](59.md) gift-wrapped DM events, with the CK
acting like a private key. However, the inner event is signed by the sender's main private key;

- At any time, specially when sending a DM to someone for the first time, an user informs the other party that the latter should
fetch messages from the first user. It is done by publishing a "Chat Request" event with a `p` tag set to the receiver's pubkey;

- The receiver of the "Chat Request" event can choose to fetch messages from the sender by using the pubkey generated from the CK they have in common as an `{ authors: [<pubkey>] }` filter. The sender uses the same filter because as the CK is the same for both users, the receiver will also use it to gift-wrap reply DM events;

- Each DM participant stores the list of individual DM chats they are tracking by updating their "Approved Chats" event.
The user client will only fetch DMs from approved chats.

## Chat Request

When an user sends a DM to someone for the first time, a `kind:1043` "Chat Request" event is also sent from user A to B.

If user A has never received a reply DM from B after some time,
or a new DM from user A was sent after a long period of chat inactivity,
user A's client may send another "Chat Request" event to B just in case a previous chat request was missed
due to being mixed with a lot of other chat requests by different people.

The chat request uses a random pubkey to hide from the public who's the requester, like a [NIP-59](59.md) Gift Wrap
but with a specific kind number to enable targeted fetch. It wraps a `kind:13` event that seals a `kind:1044` "Chat Request Intent" event.

The seal can only be opened using A and B keys, a fact that sets the participants.
While the most inner event kind number confirms that the intention is to chat.

The `kind:1043` event require adding [NIP-13](13.md) proof of work (PoW)
with a minimum of 16 difficulty.

The `kind:1044` event doesn't have a `.sig` field to prevent the event without seal
from proving authorship or being republished on relays.

It has an empty `.content`.

It must have the same `created_at` of the `kind:1043` one to prevent
the latter's content from being replayed inside a new `kind:1043` event.

The "Chat Request" event should be sent to atleast one of the receiver's [NIP-65](65.md) `read` relays.

```js
{
"kind": 1043,
"pubkey": "<random-pubkey>",
"tags": [
["p", "<pubkey-B>"],
["nonce", "65962", "16"] // atleast 16 PoW difficulty
],
"content": "<nip44Encrypt(JSON.stringify({
"kind": 13, // seal
"pubkey" "<pubkey-A>",
"tags": [],
"content": "<nip44Encrypt(JSON.stringify({
"id": "<...>",
"kind": 1044,
"pubkey" "<pubkey-A>",
"tags": [],
"content": "",
"created_at": 1702711000 // now
// no .sig field
}))>",
"created_at": 0
// ....other fields
}))>",
"created_at": 1702711000 // now
// ...other fields
}
```

### Spam Mitigation

Because the `kind:1043` includes a `p` tag set to the recipient, anyone can send such
events to an user at a high frequency to fill their "Chat Requests" inbox.

Below details and conventions ease the problem:

#### Misc

- The event has an empty `.content` to reduce value for spammers
(there is no attached message to show to the receiver,
so the spammer can't advertise URLs, for example);

- The way this NIP splits the events into "Chat Requests" and "DMs" makes it possible to
download DMs of already approved chats freely while being hit by a Denial
of Service (DoS) attack (when someone sends a high volume of "Chat Request" events
to an user to fill their inbox), without chance of missing messages.

#### Client

- Receiving clients MUST ignore chat requests with [NIP-13](13.md) PoW
difficulty lower than 16. It wouldn't be possible to require this from DM events,
but as chat requests are expected to have a much lower volume than DMs, it's ok;

- Receiving Clients should NOT strive to download and sync the complete set of chat requests,
because many may be spam. Instead, they may choose to download only the last `n` events
(`n` value depends on how long the user was offline)
and keep websocket open to receive new ones;

- During a DoS attack (too many chat requests with close timestamps being received
while the websocket is open), the client should stop listening for new ones.
After some time, it should restart above steps;

- Sending clients should retry chat requests by publishing new ones whenever they
consider it necessary, to account for the receiving user missing some due to previous
chat requests getting mixed with spam.

#### Relay

- Relays should block chat request events with byte size higher than 3KB (1543 is the expected upper limit of the byte size of such events but we leave room to size
increase due to possible changes to NIP-44) to prevent big event attack attempts.
This is possible because "Chat Request" events have a fixed number of tags
and empty content, which isn't the case with DM events;

- Relays are encouraged to require increasing [NIP-13](13.md) PoW difficulty the more chat requests are sent to the same pubkey. The min PoW difficulty should be 16.

## Approved Chats

Clients are expected to fetch DMs just from approved chats.

When receiving a chat request from user A, an user B can accept it or ignore it.
The user accepts it by adding the sender's pubkey to its [NIP-51](51.md) `kind:10043`
"Approved Chats" event's list of encrypted `p` tags.

B's client should auto accept it if A is one of their follows or contact. Else
the client should show A's `kind:0` metadata and link to their profile to help with B's decision.

When sending a chat request, the sender auto adds the receiver's pubkey to their own "Approved Chats" event if they weren't already in the list.

The user should publish the event to all of their own [NIP-65](65.md) `write` relays.

```js
{
"kind": 10043,
"pubkey": "<pubkey-B>"
"content": "<nip44Encrypt(JSON.stringify([
["p", "<pubkey-A>"],
// other "p" tags
]))>",
// ...other fields
}
```

## Direct Message

User A may send `kind:14` DM events to user B even though they don't
know if B will ever fetch and read them.

Both A and B use their [NIP-44](44.md) "Conversation Key" (CK) they have in common as a private key to sign a [NIP-59](59.md) `kind:1059` wrapper event
(with `kind:13` seal). It wraps the encrypted DM event authored by the real user's pubkey.

The CK is also used to encrypt the most inner event.

The wrapper event has a random pubkey on the `p` tag just to look like a regular gift wrap.
In reality the two participants will fetch messages by `author`, using the pubkey derived from the CK effectivelly as a private communication channel between them.
This way no fake events can be sent to any of the participants to flood them with a DoS attack and the new messages set is downloaded quicker.

The correct current date is used on the wrapper event.

The inner `kind:14` event MUST NOT include a `.sig` field.

It has a `gwsk` tag set to the private key used to sign the gift wrap,
encrypted using A's keypair as arguments when generating the CK
used just for this specific encryption task.
This way, the sender is able to delete the event.

```js
{
"kind": 1059, // gift wrap
"pubkey": "<conversation-key-AB-derived-pubkey>",
Copy link
Collaborator

@vitorpamplona vitorpamplona Dec 28, 2023

Choose a reason for hiding this comment

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

I wonder if it is better to keep things consistent and use random keys to sign but p-tag the conversation key...

In that way, it's impossible for the public to know if this is somebody's "nym"/shared key or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The p tag downside is someone can flood the 2 participants with bogus gift wraps, forcing them to download garbage just for fun.

But you have a good point.

While we don't decide the best approach I will just add a random p tag instead to make it look like a regular gift wrap. Of course this is rather weak as repeating the pubkey field value on gift wraps is supposed to be very hard to happen.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The p tag downside is someone can flood the 2 participants with bogus gift wraps, forcing them to download garbage just for fun.

They can do the same with the pubkey... With or without the random p tag. They just need to see many wraps shipped to the same pubkey and blast it.

Copy link
Contributor Author

@arthurfranca arthurfranca Dec 28, 2023

Choose a reason for hiding this comment

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

Not in the case of DMs cause the filter to fetch them will be { authors: ["<conversation-key-AB-derived-pubkey>"], kinds: [1059] } and only the participants can sign valid events with that key (relays aren't supposed to store events with wrong sig).

While if using { #p: ["<conversation-key-AB-derived-pubkey>"], kinds: [1059] } anyone can send a valid event with that p tag.

Copy link
Collaborator

Choose a reason for hiding this comment

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

oh yes, makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes "Chat Requests" can be spammed though I think it's a lesser problem to the user cause 1) the spam won't pollute the active chats list; 2) it may not be that tasty of a target to spammers cause it won't carry a message with links and such; 3) The client may not need to download all chat requests (read below).

With number 3 I mean the receiver client could just fetch the last N chat requests and ignore the rest. N could be a number relative to the number of days the user hasn't opened the app or simply a fixed number like 500. I wouldn't store/sync chat requests on the local DB at all, just always fetch a recent list from relays.
This works ok if the sender client is wise enough to send duplicate chat requests to the same user that has never replied with a DM just in case the receiver missed the previous request.

This NIP would force the client to have one tab for approving chat requests (may be flooded yeah) and another tab for all (approved) DMs with no spam.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This works ok if the sender client is wise enough to send duplicate chat requests...

I tried to highlight this a bit when I wrote on the NIP this: "If user A has sent a considerable amount of messages with no reply from user B, it can send to B another "Chat Request" event."

Copy link
Collaborator

Choose a reason for hiding this comment

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

This NIP would force the client to have one tab for approving chat requests (may be flooded yeah) and another tab for all (approved) DMs with no spam.

Which is the same we do for regular GiftWrapped DMs.

My point is that if you are trying to solve the spam bloat, this is not doing it. The client still needs to download all requests (which can be made huge -- 100KB each) to see which ones are real and which ones are not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes someone can flood your chat request inbox and unfortunately this can't be avoided but just rate limited by relays =(

There's a slight difference though on regular non-malicious usage: An user A is supposed to send one or a few chat requests to user B and send an unlimited number of DMs to B. But the difference is that if the chat request wasn't approved, the B client will never download any of A's DMs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The client still needs to download all requests (which can be made huge -- 100KB each)

@vitorpamplona I've added suggestion for relays to block chat request events higher than 3KB.

Added PoW requirement to Chat Requests (enforced by the receiving clients). From the user point of view, sender client can send DMs freely (while it is mining the Chat Request PoW in the background).

Receiving client won't ever download any of someone's DMs if the receiving user hasn't approved the specific chat request.

"tags": [
["p", "<random-pubkey>"] // just to look like a regular gift wrap
],
"content": "<nip44Encrypt(JSON.stringify({
"kind": 13, // seal
"pubkey" "<pubkey-A>",
"tags": [],
"content": "<nip44Encrypt(JSON.stringify({
"id": "<...>",
"kind": 14, // DM
"pubkey" "<pubkey-A>",
"tags": ["gwsk", "<nip44Encrypt(<gift-wrap's-secret-key>)>"], // encrypted to A only
"content": "Hello, User B",
"created_at": 1702711000 // now
// no .sig field
}))>",
"created_at": 0
// ....other fields
}))>",
"created_at": 1702711000 // now
// ...other fields
}
```

This event should be sent to atleast one of the receiver's [NIP-65](65.md) `read` relays and also to one or more of the sender's `read` relays.
This way, an user will only have to fetch DMs from their own set of `read` relays.