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

Shared Key DM
-------------

`draft` `optional`

This direct message (DM) scheme between two participants aims to hide metadata while keeping the regular client-relay filtering experience.

## Chat Request

A `kind:1043` "Chat Request" event is sent from user A to B. 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.

The chat request uses a random pubkey to hide who's the requester. It wraps an encrypted `kind:30043` "Pubkey Owner Proof" event
signed by user A's privkey, proving it owns the key as long as the event has a `created_at` not too far in the past.

The inner event is a parameterized replaceable one with no `d` tag to make it hard to be accepted by relays if the receiver tries to publish it.
It has a `p` tag with the receiver pubkey to make it focused to this specific interaction, to avoid its reuse.
It has an empty `.content` so that it has little value for spammers (there is no attached message to show to the receiver).

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

```js
{
"kind": 1043,
"pubkey": "<random>",
"tags": [
["p", "<pubkey-B>"]
]
"content": "<nip44Encrypt(JSON.stringify({
"kind": 30043,
"pubkey": "<pubkey-A>",
// No "d" tag
"tags": [
["p", "<pubkey-B>"]
],
"content": "",
"created_at": 1702711000 // now
// ...other fields
}))>",
"created_at": 1702711000 // now
// ...other fields
}
```

## Active 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 `kind:10043` "Active Chat" event's list of encrypted `p` tags.

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

When sending a chat request, the sender auto adds the receiver's pubkey to its own "Active Chat" event.

The user should publish the event to all of its [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

After sending a chat request and updating its own active chats, user A is able to send `kind:14` Shared Key DM events
even though it doesn't know if B will ever fetch and read them.

Both A and B use their [NIP-44](44.md) "Conversation Key" 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 "Conversation Key" is used to encrypt the inner events.

There is no `p` tag on the wrapper event because the two participants will fetch messages by the pubkey derived from the "Conversation Key".
This way no spam can be sent to any of the participants and the new messages download is finished faster.

The correct current date is used on the wrapper event.

The inner `kind:14` event MUST NOT include a `.sig` field to avoid it being published to relays.

```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": []
"content": "<nip44Encrypt(JSON.stringify({
"kind": 13, // seal
"pubkey" "<pubkey-A>",
"tags": [],
"content": "<nip44Encrypt(JSON.stringify({
"kind": 14, // DM
"pubkey" "<pubkey-A>",
"tags": [],
"content": "Hello, User B",
"created_at": 1702711000 // now
// ....other fields without .sig
}))>",
"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 its own set of `read` relays.