Skip to content

Build with PsstPsst

Private messages

Implement PsstPsst NIP-17 private messaging extensions: split-key NIP-44 envelopes, identity-key routing, sender verification, and millisecond timestamps.

Protocol source ↗
On this page

draft optional

PsstPsst extends NIP-17 with separate encryption keys and millisecond timestamps. Key announcement and device synchronization are specified in NIP-4E. Group-specific behavior is documented separately in Stable groups.

Split-key envelope

The identity key signs the kind 13 seal. The seal’s first n tag identifies the sender’s encryption public key announced through NIP-4E:

["n", "<sender-encryption-pubkey>"]

The seal content is NIP-44-encrypted using the sender’s encryption private key and the recipient’s encryption public key. The seal created_at equals the rumor created_at.

The kind 1059 gift wrap is NIP-44-encrypted to the recipient’s encryption public key but routed with the recipient’s identity public key:

["p", "<recipient-identity-pubkey>"]

The complete envelope is:

{
  "id": "<gift-wrap-id>",
  "pubkey": "<one-time-ephemeral-pubkey>",
  "created_at": "<random-time-within-the-previous-two-days>",
  "kind": 1059,
  "tags": [["p", "<recipient-identity-pubkey>"]],
  "content": nip44_encrypt(
    JSON.stringify({
      "id": "<seal-id>",
      "pubkey": "<sender-identity-pubkey>",
      "created_at": "<rumor-created-at>",
      "kind": 13,
      "tags": [["n", "<sender-encryption-pubkey>"]],
      "content": nip44_encrypt(
        JSON.stringify(unsigned_message_rumor),
        conversation_key(
          "<sender-encryption-private-key>",
          "<recipient-encryption-pubkey>"
        )
      ),
      "sig": "<sender-identity-signature>"
    }),
    conversation_key(
      "<one-time-ephemeral-private-key>",
      "<recipient-encryption-pubkey>"
    )
  ),
  "sig": "<one-time-ephemeral-signature>"
}

One gift wrap is created for each recipient and for the sender. Relay routing continues to use the identity pubkey and kind 10050; only NIP-44 key agreement uses the encryption key.

Receiving

A receiver:

  1. Verifies the kind 1059 gift wrap and decrypts it with the recipient’s encryption private key and the wrapper’s ephemeral public key.
  2. Verifies the enclosed kind 13 seal with the sender’s identity public key.
  3. Reads the sender’s encryption public key from the seal’s first n tag and decrypts the rumor with that key and the recipient’s encryption private key.
  4. Verifies the rumor ID and requires the rumor pubkey to equal the seal pubkey.

An authenticated seal provides encryption-key evidence for its sender identity. Compare seal evidence using the authenticated rumor order (created_at, optional ms, then event ID), and kind 10044 evidence using its signed created_at and event ID. Select the newest evidence across both sources; delayed history MUST NOT replace a newer encryption key with an older one.

A relay-delivered seal without a valid n tag is not a PsstPsst split-key envelope.

ms tag

Private message rumors may carry the millisecond component of their creation time:

["ms", "<0..999>"]

Only the first ms tag is used. A missing or malformed value contributes zero milliseconds. The full timestamp is:

created_at * 1000 + ms

The tag is encrypted with the rumor and is not used for relay queries or envelope timestamps.