nips/17.md
2025-02-08 16:25:22 +00:00

13 KiB

NIP-17

Private Direct Messages

draft optional

This NIP defines an encrypted direct messaging scheme using NIP-44 encryption and NIP-59 seals and gift wraps.

Direct Message Kind

Kind 14 is a chat message. p tags identify one or more receivers of the message.

{
  "id": "<usual hash>",
  "pubkey": "<sender-pubkey>",
  "created_at": "<current-time>",
  "kind": 14,
  "tags": [
    ["p", "<receiver-1-pubkey>", "<relay-url>"],
    ["p", "<receiver-2-pubkey>", "<relay-url>"],
    ["e", "<kind-14-id>", "<relay-url>", "reply"] // if this is a reply
    ["subject", "<conversation-title>"],
    // rest of tags...
  ],
  "content": "<message-in-plain-text>",
}

.content MUST be plain text. Fields id and created_at are required.

Tags that mention, quote and assemble threading structures MUST follow NIP-10.

Kind 14s MUST never be signed. If it is signed, the message might leak to relays and become fully public.

Chat Rooms

The set of pubkey + p tags defines a chat room. If a new p tag is added or a current one is removed, a new room is created with clean message history.

Clients SHOULD render messages of the same room in a continuous thread.

An optional subject tag defines the current name/topic of the conversation. Any member can change the topic by simply submitting a new subject to an existing pubkey + p-tags room. There is no need to send subject in every message. The newest subject in the thread is the subject of the conversation.

Encrypting

Following NIP-59, the unsigned kind:14 chat message must be sealed (kind:13) and then gift-wrapped (kind:1059) to each receiver and the sender individually.

{
  "id": "<usual hash>",
  "pubkey": randomPublicKey,
  "created_at": randomTimeUpTo2DaysInThePast(),
  "kind": 1059, // gift wrap
  "tags": [
    ["p", receiverPublicKey, "<relay-url>"] // receiver
  ],
  "content": nip44Encrypt(
    {
      "id": "<usual hash>",
      "pubkey": senderPublicKey,
      "created_at": randomTimeUpTo2DaysInThePast(),
      "kind": 13, // seal
      "tags": [], // no tags
      "content": nip44Encrypt(unsignedKind14, senderPrivateKey, receiverPublicKey),
      "sig": "<signed by senderPrivateKey>"
    },
    randomPrivateKey, receiverPublicKey
  ),
  "sig": "<signed by randomPrivateKey>"
}

The encryption algorithm MUST use the latest version of NIP-44.

Clients MUST verify if pubkey of the kind:13 is the same pubkey on the kind:14, otherwise any sender can impersonate others by simply changing the pubkey on kind:14.

Clients SHOULD randomize created_at in up to two days in the past in both the seal and the gift wrap to make sure grouping by created_at doesn't reveal any metadata.

The gift wrap's p-tag can be the receiver's main pubkey or an alias key created to receive DMs without exposing the receiver's identity.

Clients CAN offer disappearing messages by setting an expiration tag in the gift wrap of each receiver or by not generating a gift wrap to the sender's public key

Publishing

Kind 10050 indicates the user's preferred relays to receive DMs based on NIP-51. The event MUST include a list of relay tags with relay URIs.

{
  "kind": 10050,
  "tags": [
    ["relay", "wss://inbox.nostr.wine"],
    ["relay", "wss://myrelay.nostr1.com"],
  ],
  "content": "",
  // other fields...
}

Clients SHOULD publish kind 14 events to the 10050-listed relays. If that is not found that indicates the user is not ready to receive messages under this NIP and clients shouldn't try.

Seen status

A client MAY publish a kind 30010 which means saw messages with a "d" tag set to receivers pubkey.

The .content field has 4 sections separated by ::

  1. The number of bits in the bit array,
  2. The number of hash rounds applied.
  3. The Base64 encoded string of a bloom filter containing message ids (gift wrapped) saw by receiver.
  4. The salt encoded in the Base64.

The sender pubkey's client MAY query that specific event to check which messaged in this chat is seen by receiver to enhance user experience.

Bloom filters MUST use SHA256 functions applied to the concatenation of the key, salt, and index, as demonstrated in the pseudocode below:

class BloomFilter(size: Int, rounds: Int, buffer: ByteArray, salt: ByteArray) {
    val bits = BitArray(buffer)

    fun bitIndex(value: ByteArray, index: Byte) {
        return BigInt(sha256(value || salt || index)) % size
    }

    fun add(id: HexID) {
        val value = id.hexToByteArray()

        for (index in 0 until rounds) {
            bits[bitIndex(value, index)] = true 
        }
    }

    fun mightContains(id: HexID): Boolean {
        val value = id.hexToByteArray()

        for (index in 0 until rounds) {
            if (!bits[bitIndex(value, index)]) {
                return false
            }
        }

        return true
    }

    fun encode() {
        return size + ":" + rounds + ":" + base64Encode(bits.toByteArray()) + ":" + base64Encode(salt) 
    }

    fun decode(str: String): BloomFilter {
        val [sizeStr, roundsStr, bufferB64, saltB64] = str.split(":")
        return BloomFilter(sizeStr.toInt(), roundsStr.toInt(), base64Decode(bufferB64), base64Decode(saltB64))
    }
}

Example event for direct chats:

{
    "pubkey" : sha256(hkdf(private_key, salt: 'nip17') || "<badbdda507572b397852048ea74f2ef3ad92b1aac07c3d4e1dec174e8cdc962a>"),
    "kind": 30010,
    "created_at": 1738964056,
    "tags": [
        [
            "d",
            "bd4ae3e67e29964d494172261dc45395c89f6bd2e774642e366127171dfb81f5"
        ]
    ],
    "content": "100:10:AAAkAQANcYQFCQoB:hZkZYqqdxcE",
}

In this example the pubkey badbdda507572b397852048ea74f2ef3ad92b1aac07c3d4e1dec174e8cdc962a, has confirmed that they saw the messages with ids of ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b and 460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c from pubkey bd4ae3e67e29964d494172261dc45395c89f6bd2e774642e366127171dfb81f5.

d tag uses the hkdf defined in NIP-44.

Example event for group messages:

{
    "pubkey" : sha256(hkdf(private_key, salt: 'nip17') || "<badbdda507572b397852048ea74f2ef3ad92b1aac07c3d4e1dec174e8cdc962a>"),
    "kind": 30010,
    "created_at": 1738964056,
    "tags": [
        [
            "d",
            "bd4ae3e67e29964d494172261dc45395c89f6bd2e774642e366127171dfb81f5"
        ]
    ],
    "content": "100:10:AAAkAQANcYQFCQoB:hZkZYqqdxcE",
}

A client MAY encrypt the .content based on NIP-44 for more privacy.

Relays

It's advisable that relays do not serve kind:1059 to clients other than the ones tagged in them.

It's advisable that users choose relays that conform to these practices.

Clients SHOULD guide users to keep kind:10050 lists small (1-3 relays) and SHOULD spread it to as many relays as viable.

Benefits & Limitations

This NIP offers the following privacy and security features:

  1. No Metadata Leak: Participant identities, each message's real date and time, event kinds, and other event tags are all hidden from the public. Senders and receivers cannot be linked with public information alone.
  2. No Public Group Identifiers: There is no public central queue, channel or otherwise converging identifier to correlate or count all messages in the same group.
  3. No Moderation: There are no group admins: no invitations or bans.
  4. No Shared Secrets: No secret must be known to all members that can leak or be mistakenly shared
  5. Fully Recoverable: Messages can be fully recoverable by any client with the user's private key
  6. Optional Forward Secrecy: Users and clients can opt-in for "disappearing messages".
  7. Uses Public Relays: Messages can flow through public relays without loss of privacy. Private relays can increase privacy further, but they are not required.
  8. Cold Storage: Users can unilaterally opt-in to sharing their messages with a separate key that is exclusive for DM backup and recovery.

The main limitation of this approach is having to send a separate encrypted event to each receiver. Group chats with more than 100 participants should find a more suitable messaging scheme.

Implementation

Clients implementing this NIP should by default only connect to the set of relays found in their kind:10050 list. From that they should be able to load all messages both sent and received as well as get new live updates, making it for a very simple and lightweight implementation that should be fast.

When sending a message to anyone, clients must then connect to the relays in the receiver's kind:10050 and send the events there, but can disconnect right after unless more messages are expected to be sent (e.g. the chat tab is still selected). Clients should also send a copy of their outgoing messages to their own kind:10050 relay set.

Examples

This example sends the message Hola, que tal? from nsec1w8udu59ydjvedgs3yv5qccshcj8k05fh3l60k9x57asjrqdpa00qkmr89m to nsec12ywtkplvyq5t6twdqwwygavp5lm4fhuang89c943nf2z92eez43szvn4dt.

The two final GiftWraps, one to the receiver and the other to the sender, respectively, are:

{
   "id":"2886780f7349afc1344047524540ee716f7bdc1b64191699855662330bf235d8",
   "pubkey":"8f8a7ec43b77d25799281207e1a47f7a654755055788f7482653f9c9661c6d51",
   "created_at":1703128320,
   "kind":1059,
   "tags":[
      [ "p", "918e2da906df4ccd12c8ac672d8335add131a4cf9d27ce42b3bb3625755f0788"]
   ],
   "content":"AsqzdlMsG304G8h08bE67dhAR1gFTzTckUUyuvndZ8LrGCvwI4pgC3d6hyAK0Wo9gtkLqSr2rT2RyHlE5wRqbCOlQ8WvJEKwqwIJwT5PO3l2RxvGCHDbd1b1o40ZgIVwwLCfOWJ86I5upXe8K5AgpxYTOM1BD+SbgI5jOMA8tgpRoitJedVSvBZsmwAxXM7o7sbOON4MXHzOqOZpALpS2zgBDXSAaYAsTdEM4qqFeik+zTk3+L6NYuftGidqVluicwSGS2viYWr5OiJ1zrj1ERhYSGLpQnPKrqDaDi7R1KrHGFGyLgkJveY/45y0rv9aVIw9IWF11u53cf2CP7akACel2WvZdl1htEwFu/v9cFXD06fNVZjfx3OssKM/uHPE9XvZttQboAvP5UoK6lv9o3d+0GM4/3zP+yO3C0NExz1ZgFmbGFz703YJzM+zpKCOXaZyzPjADXp8qBBeVc5lmJqiCL4solZpxA1865yPigPAZcc9acSUlg23J1dptFK4n3Tl5HfSHP+oZ/QS/SHWbVFCtq7ZMQSRxLgEitfglTNz9P1CnpMwmW/Y4Gm5zdkv0JrdUVrn2UO9ARdHlPsW5ARgDmzaxnJypkfoHXNfxGGXWRk0sKLbz/ipnaQP/eFJv/ibNuSfqL6E4BnN/tHJSHYEaTQ/PdrA2i9laG3vJti3kAl5Ih87ct0w/tzYfp4SRPhEF1zzue9G/16eJEMzwmhQ5Ec7jJVcVGa4RltqnuF8unUu3iSRTQ+/MNNUkK6Mk+YuaJJs6Fjw6tRHuWi57SdKKv7GGkr0zlBUU2Dyo1MwpAqzsCcCTeQSv+8qt4wLf4uhU9Br7F/L0ZY9bFgh6iLDCdB+4iABXyZwT7Ufn762195hrSHcU4Okt0Zns9EeiBOFxnmpXEslYkYBpXw70GmymQfJlFOfoEp93QKCMS2DAEVeI51dJV1e+6t3pCSsQN69Vg6jUCsm1TMxSs2VX4BRbq562+VffchvW2BB4gMjsvHVUSRl8i5/ZSDlfzSPXcSGALLHBRzy+gn0oXXJ/447VHYZJDL3Ig8+QW5oFMgnWYhuwI5QSLEyflUrfSz+Pdwn/5eyjybXKJftePBD9Q+8NQ8zulU5sqvsMeIx/bBUx0fmOXsS3vjqCXW5IjkmSUV7q54GewZqTQBlcx+90xh/LSUxXex7UwZwRnifvyCbZ+zwNTHNb12chYeNjMV7kAIr3cGQv8vlOMM8ajyaZ5KVy7HpSXQjz4PGT2/nXbL5jKt8Lx0erGXsSsazkdoYDG3U",
   "sig":"a3c6ce632b145c0869423c1afaff4a6d764a9b64dedaf15f170b944ead67227518a72e455567ca1c2a0d187832cecbde7ed478395ec4c95dd3e71749ed66c480"
}
{
   "id":"162b0611a1911cfcb30f8a5502792b346e535a45658b3a31ae5c178465509721",
   "pubkey":"626be2af274b29ea4816ad672ee452b7cf96bbb4836815a55699ae402183f512",
   "created_at":1702711587,
   "kind":1059,
   "tags":[
      [ "p", "44900586091b284416a0c001f677f9c49f7639a55c3f1e2ec130a8e1a7998e1b"]
   ],
   "content":"AsTClTzr0gzXXji7uye5UB6LYrx3HDjWGdkNaBS6BAX9CpHa+Vvtt5oI2xJrmWLen+Fo2NBOFazvl285Gb3HSM82gVycrzx1HUAaQDUG6HI7XBEGqBhQMUNwNMiN2dnilBMFC3Yc8ehCJT/gkbiNKOpwd2rFibMFRMDKai2mq2lBtPJF18oszKOjA+XlOJV8JRbmcAanTbEK5nA/GnG3eGUiUzhiYBoHomj3vztYYxc0QYHOx0WxiHY8dsC6jPsXC7f6k4P+Hv5ZiyTfzvjkSJOckel1lZuE5SfeZ0nduqTlxREGeBJ8amOykgEIKdH2VZBZB+qtOMc7ez9dz4wffGwBDA7912NFS2dPBr6txHNxBUkDZKFbuD5wijvonZDvfWq43tZspO4NutSokZB99uEiRH8NAUdGTiNb25m9JcDhVfdmABqTg5fIwwTwlem5aXIy8b66lmqqz2LBzJtnJDu36bDwkILph3kmvaKPD8qJXmPQ4yGpxIbYSTCohgt2/I0TKJNmqNvSN+IVoUuC7ZOfUV9lOV8Ri0AMfSr2YsdZ9ofV5o82ClZWlWiSWZwy6ypa7CuT1PEGHzywB4CZ5ucpO60Z7hnBQxHLiAQIO/QhiBp1rmrdQZFN6PUEjFDloykoeHe345Yqy9Ke95HIKUCS9yJurD+nZjjgOxZjoFCsB1hQAwINTIS3FbYOibZnQwv8PXvcSOqVZxC9U0+WuagK7IwxzhGZY3vLRrX01oujiRrevB4xbW7Oxi/Agp7CQGlJXCgmRE8Rhm+Vj2s+wc/4VLNZRHDcwtfejogjrjdi8p6nfUyqoQRRPARzRGUnnCbh+LqhigT6gQf3sVilnydMRScEc0/YYNLWnaw9nbyBa7wFBAiGbJwO40k39wj+xT6HTSbSUgFZzopxroO3f/o4+ubx2+IL3fkev22mEN38+dFmYF3zE+hpE7jVxrJpC3EP9PLoFgFPKCuctMnjXmeHoiGs756N5r1Mm1ffZu4H19MSuALJlxQR7VXE/LzxRXDuaB2u9days/6muP6gbGX1ASxbJd/ou8+viHmSC/ioHzNjItVCPaJjDyc6bv+gs1NPCt0qZ69G+JmgHW/PsMMeL4n5bh74g0fJSHqiI9ewEmOG/8bedSREv2XXtKV39STxPweceIOh0k23s3N6+wvuSUAJE7u1LkDo14cobtZ/MCw/QhimYPd1u5HnEJvRhPxz0nVPz0QqL/YQeOkAYk7uzgeb2yPzJ6DBtnTnGDkglekhVzQBFRJdk740LEj6swkJ",
   "sig":"c94e74533b482aa8eeeb54ae72a5303e0b21f62909ca43c8ef06b0357412d6f8a92f96e1a205102753777fd25321a58fba3fb384eee114bd53ce6c06a1c22bab"
}