Chapter 04 - Encryption
04
Nostr Implementation Possibility - Hex № 04

Encrypted Direct Message

The original kind-4 encrypted note between two users. Kept in this manual for history and for reading old events, not for new work.

Deprecated

1 Overview

NIP-04 defines kind 4, the first encrypted direct message format in Nostr. A sender encrypts the content field so that only the receiver can read it.

The encryption uses a shared secret. The sender computes this secret from their own private key and the receiver's public key. The receiver computes the same secret from their own private key and the sender's public key. Both sides land on the same secret without ever sending it over the wire. This technique is called ECDH, Elliptic Curve Diffie-Hellman.

Relays still see the sender, the receiver, and the timing of every kind-4 event, even though they cannot read the message text. That gap between "encrypted content" and "private conversation" is the reason this NIP is deprecated.

Deprecated

NIP-04 is unrecommended. Use NIP-17, Private Direct Messages, for all new work. NIP-17 hides the sender, receiver, and event kind from relays, which NIP-04 cannot do. Read this chapter to understand old events and to migrate away from kind 4, not to build new features.

2 The Kind-4 Event

A kind-4 event carries one encrypted message. It must follow three rules, on top of the base event format from NIP-01.

Kind-4 rules
FieldRule
kindMust be 4.
contentMust be the base64-encoded, aes-256-cbc encrypted message, with the base64 initialization vector appended as a querystring-style parameter named iv. See below.
tagsMust contain a p tag naming the receiver. May contain an e tag naming a prior message in the conversation. See §3.

Deriving the shared secret

The sender combines their private key with the receiver's public key on the curve secp256k1. This produces a shared point. Nostr's shared secret is the X coordinate of that point only, kept as raw bytes.

Warning!

The default libsecp256k1 ECDH function hashes the shared point with sha256 and uses both X and Y coordinates. Nostr does not do this. Pass a custom hashfp function to secp256k1_ecdh that copies out the raw X coordinate and skips the hash. Using the library default produces a secret that does not match other Nostr clients.

Sender privkey Receiver pubkey ECDH Shared point take X only, no hash aes-256-cbc plaintext + IV content field ciphertext?iv=...
Fig. 4-1 — Encryption flow. ECDH produces a shared point. Only its X coordinate, unhashed, becomes the aes-256-cbc key.

Building the content field

Follow these steps to encrypt a message:

  1. Compute the shared secret as described above.
  2. Generate a random 16-byte initialization vector, the IV.
  3. Encrypt the plaintext with aes-256-cbc, using the shared secret as the key and the IV.
  4. Base64-encode the ciphertext.
  5. Base64-encode the IV.
  6. Join them as <ciphertext_base64>?iv=<iv_base64> and set this string as content.
Wire format - content field
"content": "<encrypted_text>?iv=<initialization_vector>"

3 Tags

Kind-4 tags
TagRequiredFormPurpose
pMUST["p", "<receiver pubkey, hex>"]Names the receiver. Lets a relay forward the event to them.
eMAY["e", "<event id>"]Names the previous message in the conversation, or the message this one replies to.
Tech Note

The p tag is what lets a relay index and forward the message, but it also tells everyone who can read the relay's data exactly who is talking to whom. Encrypting content does not hide this. See §4.

4 Security Warning

This format is far behind the current state of the art for encrypted peer-to-peer communication. It leaks metadata through the event fields.

  • Do not use kind 4 for anything that truly needs to stay secret.
  • If you must use it, only publish to relays that require NIP-42 AUTH and restrict who may fetch your kind-4 events.
  • Sender, receiver, and timing are all visible to any relay and to anyone who can read the relay's stored events. Only the message text is hidden.

5 Client Implementation Warning

Clients should not search the decrypted content for public key or note references and replace them, the way NIP-27 mention handling works for regular text notes.

That process turns a reference such as @npub1… into #[0] and adds a matching ["p", "…"] tag. Doing this to a direct message adds a plaintext p tag naming the mentioned user. The tag leaks the mention, and the mentioned user's client will show the encrypted message in their own inbox, even though they cannot read it.

6 Worked Example

Alice sends Bob a kind-4 message, then Bob replies. Orange lines go to the relay. Blue lines come from the relay.

Session transcript - wss://relay.example.com
-- Alice computes the shared secret from her privkey and Bob's pubkey --
-- sharedX = ECDH(alice_priv, bob_pub).x   (unhashed)
-- iv = random 16 bytes
-- ciphertext = aes-256-cbc-encrypt("gm bob", key=sharedX, iv=iv)

→ ["EVENT", {"id":"7ac2a…","pubkey":"a1b2c3…","created_at":1749762600,
     "kind":4,"tags":[["p","bobpub00…"]],
     "content":"u3f9skA2==?iv=q8x2Lp0zK1==","sig":"9f21ab…"}]
← ["OK", "7ac2a…", true, ""]

-- Bob's client fetches messages addressed to him --
→ ["REQ", "dm1", {"kinds":[4], "#p":["bobpub00…"], "limit":20}]
← ["EVENT", "dm1", {"id":"7ac2a…","kind":4,"tags":[["p","bobpub00…"]],
     "content":"u3f9skA2==?iv=q8x2Lp0zK1==", …}]
← ["EOSE", "dm1"]

-- Bob decrypts with ECDH(bob_priv, alice_pub).x and the iv from content --
-- plaintext: "gm bob"

-- Bob replies, tagging Alice and referencing the prior event --
→ ["EVENT", {"id":"c04e1…","pubkey":"bobpub00…","created_at":1749762660,
     "kind":4,"tags":[["p","a1b2c3…"],["e","7ac2a…"]],
     "content":"gm2==?iv=z9y1Nn3xR7==","sig":"3d8e2f…"}]
← ["OK", "c04e1…", true, ""]

7 Code Sample

The steps in §2 translate directly to code. This JavaScript sample uses @noble/secp256k1 for the ECDH step and Node's built-in crypto for aes-256-cbc.

JavaScript - build a kind-4 event
import crypto from 'crypto'
import * as secp from '@noble/secp256k1'

let sharedPoint = secp.getSharedSecret(ourPrivateKey, '02' + theirPublicKey)
let sharedX = sharedPoint.slice(1, 33)

let iv = crypto.randomFillSync(new Uint8Array(16))
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(sharedX), iv)
let encryptedMessage = cipher.update(text, 'utf8', 'base64')
encryptedMessage += cipher.final('base64')
let ivBase64 = Buffer.from(iv.buffer).toString('base64')

let event = {
  pubkey: ourPubKey,
  created_at: Math.floor(Date.now() / 1000),
  kind: 4,
  tags: [['p', theirPublicKey]],
  content: encryptedMessage + '?iv=' + ivBase64
}

The event still needs id and sig, computed with the serialization rule from NIP-01.

8 Version History

NIP-04 is finalized and no longer expected to change. Notable changes, newest first:

  • 20252025-12Added the relay capability tag to the metadata line. No spec change.
  • 20242024-04Deprecation target switched from NIP-44 to the newly published NIP-17, which wraps NIP-44 in a full private-messaging design.
  • 20232023-12NIP-44 published as a modern replacement encryption standard; NIP-04 marked unrecommended for the first time.
  • 20232023-03 to 2023-04Added the security warning and the client mention-handling warning; fixed a bug and an import in the code sample; minor grammar fixes.
  • 20232023-02Clarified that the ECDH secret is the raw, unhashed X coordinate of the shared point, not the libsecp256k1 default.
  • 20222022-11Marked final: no further spec changes expected.
  • Origin2022-05Migrated into the nostr-protocol/nips repository, authored by arcbtc.

Original Proposal - Credits

Authorarcbtc
First commit2022-05-01 — "migrate nips from main nostr repo" (attributed author:arcbtc in the original text)
Statusfinal - unrecommended - optional — deprecated in favor of NIP-17
Source04.md on GitHub - full change history

9 See Also

  • NIP-17 — Private Direct Messages. The recommended replacement; hides sender, receiver, and kind from relays.
  • NIP-44 — Encrypted Payloads (Versioned). The modern encryption primitive that NIP-17 builds on.
  • NIP-59 — Gift Wrap. The event-wrapping technique NIP-17 uses to hide metadata that NIP-04 exposes.
  • NIP-01 — base event format, serialization, and signing that every kind-4 event still follows.
  • NIP-42 — relay AUTH. Recommended when kind-4 events must still be used.