Skip to content
Closed
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2acbd64
Add Private DM
arthurfranca Jan 9, 2024
13177b2
Add info about where to publish events
arthurfranca Jan 9, 2024
43fe354
Add chat session confirmation event
arthurfranca Jan 9, 2024
49d1f73
Change wording from confirm to accept
arthurfranca Jan 9, 2024
d8e8ec1
Minor update
arthurfranca Jan 10, 2024
dc7ac14
Change dm status kind numbers
arthurfranca Jan 17, 2024
7e9083d
Rename session-privkey-A to session-privkey
arthurfranca Jan 17, 2024
d1f2199
Explain conversation keys generation
arthurfranca Jan 17, 2024
d5baa6e
Use descending order when resolving session conflict
arthurfranca Jan 18, 2024
f9921e6
Fix sealing arg when encrypting
arthurfranca Jan 29, 2024
ae1e3a6
Move the session acceptance rumor to the session channel
arthurfranca Jan 29, 2024
84dff71
Change from salt suffix to full salt
arthurfranca Feb 4, 2024
e732e56
Add AUTH suggestion
arthurfranca Feb 5, 2024
1ed44ba
Add summary flow
arthurfranca Feb 6, 2024
1d421b4
Minor fix
arthurfranca Feb 6, 2024
f50c650
Remove min PoW requirement
arthurfranca Feb 6, 2024
46ad13c
Add rate limit
arthurfranca Feb 6, 2024
1415d76
Expirement using private kind range
arthurfranca Feb 14, 2024
6ddce9f
Move rumor kinds to numbers no one is using
arthurfranca Feb 20, 2024
dbaa40d
Ditch private event kinds
arthurfranca Mar 8, 2024
f86bb32
Add closing rumor
arthurfranca Mar 19, 2024
40342cc
Detect when new device is used
arthurfranca Mar 20, 2024
2255a5c
Use lid_proof tag on LID Tracker event
arthurfranca Mar 22, 2024
995ee77
Remove PoW
arthurfranca Apr 26, 2024
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
320 changes: 320 additions & 0 deletions 43.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
NIP-43
======

Secure DM
---------

`draft` `optional`

This direct message (DM) scheme between two participants is end-to-end encrypted, hides
most metadata, has forward secrecy to an extent, can detect new device usage and minimizes spam
while aiming a low implementation complexity.

This NIP currently uses [NIP-44](44.md) v2 encryption.

## Summary Flow

- An user tells another one: "Hey, I'm gonna use this random `privkey` to talk to you for three weeks! You can also use it to talk to me for the same period. Get my DMs by filtering events by the corresponding `pubkey` and I will do the same to get yours";
- All this is stored by both users in replaceable **list** events: the other user identification (main pubkey), the above `privkey`, the timestamp when it gets stale (three weeks ahead) and the other user acknowledgment in using it.

## Detailed Flow

- Before sending a DM to someone (`B`), an user (`A`) picks a random privkey to be used as chat session (`session-privkey`);
- `A` signs DMs with `session-privkey` and publishes them to `A` and `B` [NIP-65](65.md) `read` relays;
- Meanwhile, `A` sends `session-privkey` to `B` through B `read` relays, representing the chat session;
- `A` also stores `session-privkey` to themselves linking it to `B` pubkey (representing `A-B` chat session);
- Upon receiving `session-privkey`, `B` stores the key linking it to `A` pubkey (also representing `A-B` chat session) and sends a chat session acceptance to `A`. Otherwise `B` ignores the key;
- `B` can fetch DMs from that session's pubkey, filtering by `author`;
- `B` can send back DMs by signing them with the same `session-privkey`.

Note that:
- `A` can resend the same session key (same `kind:443` rumor, **reusing its `.created_at`**) anytime they want while there is no acceptance from `B`, to make sure `B` didn't miss it;
- `B` could had sent a session to `A` almost at the same time.
In that case, the newest (`kind:443` rumor's `.created_at`) of the two sessions should be used by both participants. They may also check a single
time for DMs already sent from the older session before discarding it. If the `.created_at` of both sessions are exactly the same,
the users will pick the first one after sorting the `.id` (of the `kind:443` rumors) by descending order.

Important:
- By convention, after three weeks the session expires.
Either `A` or `B` will need to restart the flow with a new random privkey as session
when sending new DMs.

## Chat Session

The "Chat Session Envelope" is a regular [NIP-59](59.md) gift wrap event but with a specific kind: `1043`.

The NIP-44 encryption when wrapping use the random author's privkey and the recipient's pubkey to generate the conversation key (CK).
When sealing the encryption uses the main sender's privkey and the recipient's pubkey.
Both encryptions use the default salt to generate the CK.

As the main flood attack mitigation strategy,
relays SHOULD limit the rate at which `kind:1043` events are published with the same `p` tag per client IP.
Relays SHOULD block it if of byte size higher than 3KB.

It has a `p` tag set to the recipient's pubkey.
Relays SHOULD require users to [NIP-42](42.md) `AUTH` as the `p` tagged pubkey when requesting the event.

The NIP-59 **rumor** is a `kind:443` "Chat Session Request" event.
It's `.content` is the session's privkey and it must include a `lid` tag.

The **seal** has a `hashed_lid` tag set to the hex-encoded sha256 hash of the LID.

By convention, the session expires 3 weeks after the **rumor**'s `.created_at` value.

The "Chat Session Envelope" event should be sent to atleast one of the recipient's NIP-65 `read` relays.

### LID

The `kind:443` **rumors**'s `LID` (Local ID) tag is set to a random string generated by the client and
kept on its local database. An user has a different `LID` for each pubkey they are talking to
and should re-use it if starting a new session with the same user.

It's goal is to know when a new client/device is being used, to help detect unauthorized
account access.

If the `lid` tag is an empty string or is absent, the event is invalid.

The **seal** must have a `hashed_lid` tag set to the hex-encoded sha256 hash of the LID, which should be validated.
The 2nd tag value is "443" to identify the seal as a "Chat Session Request"'s one.
The seal must have the same `.created_at` of the rummor.

Event example:

```js
{
"kind": 1043,
"pubkey": "<random-pubkey>",
"tags": [
["p", "<pubkey-B>"],
["nonce", "65962", "16"]
],
"content": "<nip44EncryptV2(JSON.stringify({
"kind": 13, // seal
"pubkey" "<pubkey-A>",
"tags": [
// this is present when sealing specific rumors ("Chat Session Request" and "LID Request")
["hashed_lid", "<hex(sha(LID))>", "443"]
],
"content": "<nip44EncryptV2(JSON.stringify({
"id": "<...>",
"kind": 443,
"pubkey" "<pubkey-A>",
"tags": [
["lid", "<random_string>"]
],
"content": "<session-privkey>",
"created_at": 1702711000
// no .sig field
}))>",
"created_at": 1702711000 // same as rumor's created_at
// ....other fields
}))>",
"created_at": 1702711000 // now
// ...other fields
}
```

## Chat Session List

The [NIP-51](51.md) `kind:10043` is the user's "Chat Session List" event.

It encrypts `s` (session) tags using the author's own keypair with default salt
as arguments to generate the NIP-44 conversation key.

An `s` tag's 1st value is the `pubkey` of the other side of the chat.
The 2nd value is the session's `privkey`.
The 3rd value is the expiration timestamp in seconds and is three weeks ahead
of the `kind:443`'s `.created_at` value.
The 4th value if present is set to the other chat participant's `local id`, meaning the other chat participant accepted the chat session
(see ["Chat Session Acceptance" section](#chat-session-acceptance)).

`s` tags of expired sessions should be kept to indicate active chats.

Relays SHOULD require users to [NIP-42](42.md) `AUTH` as the event author when requesting the event.

The user should publish the event to all of their own NIP-65 `write` relays.

### Session Privkey Encryption

The `privkey` must be encrypted using a `local id` (a random string generated by the client and
kept on its local database) as NIP-44 salt.

Event example:

```js
{
"kind": 10043,
"pubkey": "<pubkey-B>",
"content": "<nip44EncryptV2(JSON.stringify([
["s", "<pubkey-A>", "<nip44EncryptV2(session-privkey, salt=lid)>", "1704525400", "<peer's-lid>"],
// other "s" tags
]))>",
// ...other fields
}
```

## Session Channel

The session channel is simply made of the set of NIP-59 `kind:1059` events
with the session pubkey as their author.

Either the sender or the recipient may delete these events because both of them know
the session's privkey. Therefore, clients should store them locally to keep their users in control
of the DMs lifecycle.

**Important**: The NIP-44 encryption when wrapping and when sealing session channel events
uses the first 32 characters of the session's `privkey` as salt
when generating the conversation key (CK).
This ensures forward secrecy when creating a new session because both users
are supposed to store just the last session's `privkey`
in the "Chat Session List" event.

Note that the CK generation does **NOT** use the session privkey nor pubkey as arguments.
Instead use the two users' keys.

### DM

The DM is a `kind:14` NIP-59 rumor.

This event should be sent to atleast one of the recipient's NIP-65 `read` relays
and to atleast one of the sender's `read` relays.
This way, an user will only have to fetch DMs from their own set of `read` relays.

DM event example:

```js
{
"kind": 1059, // gift wrap
"pubkey": "<session-pubkey>",
"tags": [],
"content": "<nip44EncryptV2(JSON.stringify({
"kind": 13, // seal
"pubkey" "<pubkey-A>",
"tags": [],
"content": "<nip44EncryptV2(JSON.stringify({
"id": "<...>",
"kind": 14, // DM
"pubkey" "<pubkey-A>",
"tags": [],
"content": "Hello, User B",
"created_at": 1702711000 // now
// no .sig field
}))>",
"created_at": 1702711000 // same as rumor's created_at
// ....other fields
}))>",
"created_at": 1702711000 // now
// ...other fields
}
```

### Chat Session Acceptance

It is a `kind:414` rumor.

It's `.content` is set to the received "Chat Session Request"'s content which is enough to
accept the session identified by the `.pubkey` and the `.content` (session privkey). It must
also carry a `lid` tag with the recipient's `LID` string.

Without the acceptance by the recipient, the sender may keep resending the same
`kind:443` session in case the recipient had missed it.
There is no chat session *rejection* rumor, though, to keep recipient's online
status private.

This event should be sent to atleast one of the chat session requester's NIP-65 `read` relays.

### Last Received At

Optionally, the `created_at` of a `kind:415` NIP-59 rumor
informs the last time an user's client received DMs from the other participant.

This event should be sent to atleast one of the DM author's NIP-65 `read` relays.

### Last Read At

Optionally, the `created_at` of a `kind:416` NIP-59 rumor
informs the last time an user read DMs from the other participant.

This event should be sent to atleast one of the DM author's NIP-65 `read` relays.

### Chat Session Closing

When removing a not yet expired entry from the "Chat Session List" event,
a `kind:417` NIP-59 rumor may be used to inform the other participant to
clear the `<session-privkey>` value from their own entry.

This event should be sent to atleast one of the peer's NIP-65 `read` relays.

## New Device Usage Detection

LIDs can be used to detect when an user has started using a new device (or client).
The client should display a notification to the user every time a new
device usage is detected. If the user hasn't really used a new device they own
near the notification moment, their privkey is probably compromised.

The notification can be something similar to `"You have logged in with a new device - <LID>"`.

### LID Tracker Event

When a recipient of a `kind:443` "Chat Session Request" accepts the request,
it must also send a `kind:444` "LID Tracker" rumor wrapped and sealed inside a `kind:1043` "Chat Session Envelope" event.
(It is also sent in reply to the below "LID Request" Event)

The `kind:444` rumor has a `lid_proof` tag set to the stringified JSON of the seal
of the above mentioned "Chat Session Request" or "LID Request",
which in turn contains the `hashed_lid` tag.

The client of the user receiving the "LID Tracker" event should compare the received hashed LID
with their own LID's hash and display a notification
of new device detection if values don't match.

Note that the LID Tracker Event must be sent to all of the `kind:443` recipient's `write` relays, because the
sender of the `kind:443` event, whose key may be compromised, has control over its own relay list.

### LID Request Event

Client should request the unhashed LID to their peer if it can't decrypt the current session's privkey stored
on the "Chat Session List", that's why it should watch for "Chat Session List" updates.

A `kind:445` rumor wrapped and sealed inside a `kind:1043` "Chat Session Envelope" event is
used.

The **seal** has a `hashed_lid` tag set to the hex-encoded sha256 hash of the user's LID
and the tag's second value is set to "445".
The seal must have the same `.created_at` of the rummor.

The **rumor** has just a `lid` tag with the user's LID and `.content` is set to a **temporary** session `privkey` the user
may use to send (not receive) DMs to the recipient while it hasn't received the "LID Tracker"'s unhashed LID.

This new temporary session should be recorded on the sender's `Chat Session List`
with an extra 5th value as the hex-encoded sha256 hash of it's own LID to flag
the entry as a temp session. It expires at the same time of the regular session.

This event should be sent to atleast one of the recipient's NIP-65 `read` relays.

### LID Copy Event

In reply to the above "LID Request" Event, the recipient should send two events.
One of them is a "LID Tracker" Event with the hash of the "LID Request"'s
LID.

The other one is a `kind:446` "LID Copy" rumor. It is wrapped and sealed in
a new `kind:1044` event that has the same properties of the `kind:1043` one,
except for the CKs generation that uses the "LID Request"'s LID as salt
both when wrapping and sealing, because it is meant to be decrypted by a specifc device
that has the right LID.

The "LID Copy" has a `lid` tag set to the current (not temporary) session's peer LID.

The "LID Copy" event should be sent to atleast one of the requester's NIP-65 `read` relays.

The "LID Copy" author's client should fetch all of the temporary session's messages
before discarding it.

After checking that the LID is able to decrypt the session privkey,
the LID Copy receiver's client **MUST overwrite its own LID with the received one**.

It must also delete the temporary session it had added but, before that, it should
resend (to atleast one of their own `read` relays) the same DMs previously sent
with the temporary session but now using the regular session privkey, so that
the other user devices can access them.