diff --git a/43.md b/43.md new file mode 100644 index 0000000000..1fe6da02e9 --- /dev/null +++ b/43.md @@ -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": "", + "tags": [ + ["p", ""], + ["nonce", "65962", "16"] + ], + "content": "", + "tags": [ + // this is present when sealing specific rumors ("Chat Session Request" and "LID Request") + ["hashed_lid", "", "443"] + ], + "content": "", + "kind": 443, + "pubkey" "", + "tags": [ + ["lid", ""] + ], + "content": "", + "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": "", + "content": "", "", "1704525400", ""], + // 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": "", + "tags": [], + "content": "", + "tags": [], + "content": "", + "kind": 14, // DM + "pubkey" "", + "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 `` 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 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.