NIP-04

Encrypted Direct Messages

final social features

NIP-04 enables private messaging on Nostr using kind 4 events with AES-256-CBC encryption. Messages are end-to-end encrypted between sender and recipient.

Author
fiatjaf
Last Updated
15 January 2024
Official Spec
View on GitHub →

NIP-04: Encrypted Direct Messages

Status: Final (but see Security Considerations) Author: fiatjaf Category: Social Features


Overview

NIP-04 defines how Nostr users send private, encrypted direct messages (DMs). Messages use kind 4 events with AES-256-CBC encryption based on a shared secret derived via ECDH (Elliptic Curve Diffie-Hellman).

Key Features:

  • End-to-end encryption - Only sender and recipient can read messages
  • Decentralized - No central server holds your messages
  • Portable - DMs sync across all your clients
  • Nostr-native - Uses the same event/relay infrastructure

Important: NIP-04 has known security limitations. Read Security Considerations below.


Why Private Messaging on Nostr?

Compared to Traditional Apps

FeatureNIP-04 DMsTraditional Apps
EncryptionEnd-to-end (E2EE)Varies (often E2EE)
PortabilityAccess from any clientLocked to one app
Server dependencyAny relay, user’s choiceCentralized servers
CensorshipResistant (multiple relays)Vulnerable (single company)
IdentityNostr keypairPhone number or account

Advantages

Censorship-resistant: No single company can block your messages ✅ Portable: Switch clients without losing message history ✅ Pseudonymous: No phone number or email required ✅ Open protocol: Any client can implement

Disadvantages

⚠️ Metadata visible: Who you message and when is public ⚠️ No perfect forward secrecy: Compromised key exposes all messages ⚠️ Replay attacks: Relays can serve old messages as new ⚠️ Limited adoption: Not all clients implement DMs yet


How It Works

Event Structure

Encrypted DMs use kind 4 events:

{
  "id": "...",
  "pubkey": "sender_public_key",
  "created_at": 1673347337,
  "kind": 4,
  "tags": [
    ["p", "recipient_public_key"]
  ],
  "content": "base64_encrypted_message?iv=initialization_vector",
  "sig": "..."
}

Key Fields:

  • kind: Always 4 for encrypted DMs
  • tags: Single p tag with recipient’s pubkey
  • content: Base64-encoded encrypted message + IV

Encryption Process

1. Compute Shared Secret

Use ECDH to derive a shared secret from your private key and recipient’s public key:

import { getSharedSecret } from 'nostr-tools';

// Derive shared secret (same for both sender and recipient)
const sharedSecret = getSharedSecret(myPrivateKey, recipientPubkey);

Both parties compute the same shared secret:

  • Sender: secret = ECDH(myPrivateKey, recipientPubkey)
  • Recipient: secret = ECDH(myPrivateKey, senderPubkey)

2. Encrypt the Message

Use AES-256-CBC with the shared secret as the key:

import CryptoJS from 'crypto-js';

// Generate random IV (initialization vector)
const iv = CryptoJS.lib.WordArray.random(128 / 8);

// Encrypt message
const encrypted = CryptoJS.AES.encrypt(
  plaintext,
  CryptoJS.enc.Hex.parse(sharedSecret),
  {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  }
);

// Encode as base64
const encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
const ivBase64 = iv.toString(CryptoJS.enc.Base64);

// Create content field
const content = `${encryptedBase64}?iv=${ivBase64}`;

3. Create and Publish Event

const event = {
  kind: 4,
  created_at: Math.floor(Date.now() / 1000),
  tags: [["p", recipientPubkey]],
  content: content, // "encrypted_base64?iv=iv_base64"
  pubkey: myPubkey
};

// Sign and publish
const signedEvent = signEvent(event, myPrivateKey);
relay.publish(signedEvent);

Decryption Process

1. Fetch DM Events

Query for kind 4 events where you’re the recipient:

// Fetch DMs sent to you
relay.sub([{
  kinds: [4],
  "#p": [myPubkey]
}]);

// Fetch DMs you sent
relay.sub([{
  kinds: [4],
  authors: [myPubkey]
}]);

2. Compute Shared Secret

Use the sender’s pubkey (from the event):

const sharedSecret = getSharedSecret(myPrivateKey, event.pubkey);

3. Decrypt the Message

Parse the content field and decrypt:

// Parse content: "encrypted_base64?iv=iv_base64"
const [encryptedBase64, ivBase64] = event.content.split('?iv=');

// Decode from base64
const encrypted = CryptoJS.enc.Base64.parse(encryptedBase64);
const iv = CryptoJS.enc.Base64.parse(ivBase64);

// Decrypt
const decrypted = CryptoJS.AES.decrypt(
  { ciphertext: encrypted },
  CryptoJS.enc.Hex.parse(sharedSecret),
  {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  }
);

// Convert to plaintext
const plaintext = decrypted.toString(CryptoJS.enc.Utf8);

Complete Example

Sending a DM

import { getPublicKey, getSharedSecret, signEvent } from 'nostr-tools';
import CryptoJS from 'crypto-js';

function encryptDM(privateKey, recipientPubkey, message) {
  // 1. Compute shared secret
  const sharedSecret = getSharedSecret(privateKey, recipientPubkey);

  // 2. Generate random IV
  const iv = CryptoJS.lib.WordArray.random(128 / 8);

  // 3. Encrypt message
  const encrypted = CryptoJS.AES.encrypt(
    message,
    CryptoJS.enc.Hex.parse(sharedSecret),
    { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
  );

  // 4. Create content field
  const encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
  const ivBase64 = iv.toString(CryptoJS.enc.Base64);
  return `${encryptedBase64}?iv=${ivBase64}`;
}

// Usage
const myPrivateKey = "your_private_key_hex";
const myPubkey = getPublicKey(myPrivateKey);
const recipientPubkey = "recipient_public_key_hex";

const encryptedContent = encryptDM(
  myPrivateKey,
  recipientPubkey,
  "Hello! This is a secret message."
);

// Create and publish event
const dmEvent = {
  kind: 4,
  created_at: Math.floor(Date.now() / 1000),
  tags: [["p", recipientPubkey]],
  content: encryptedContent,
  pubkey: myPubkey
};

const signedDM = signEvent(dmEvent, myPrivateKey);
relay.publish(signedDM);

Receiving a DM

function decryptDM(privateKey, senderPubkey, encryptedContent) {
  // 1. Compute shared secret
  const sharedSecret = getSharedSecret(privateKey, senderPubkey);

  // 2. Parse content
  const [encryptedBase64, ivBase64] = encryptedContent.split('?iv=');
  const encrypted = CryptoJS.enc.Base64.parse(encryptedBase64);
  const iv = CryptoJS.enc.Base64.parse(ivBase64);

  // 3. Decrypt
  const decrypted = CryptoJS.AES.decrypt(
    { ciphertext: encrypted },
    CryptoJS.enc.Hex.parse(sharedSecret),
    { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
  );

  // 4. Convert to plaintext
  return decrypted.toString(CryptoJS.enc.Utf8);
}

// Usage (when receiving an event)
relay.on('event', (event) => {
  if (event.kind === 4 && event.tags[0][1] === myPubkey) {
    const plaintext = decryptDM(
      myPrivateKey,
      event.pubkey, // sender
      event.content
    );
    console.log("Received DM:", plaintext);
  }
});

Security Considerations

⚠️ NIP-04 has known security limitations. It was designed for simplicity, not maximum security.

Known Vulnerabilities

1. No Perfect Forward Secrecy (PFS)

If your private key is compromised:

  • All past DMs can be decrypted (even old messages)
  • All future DMs can be decrypted (until you change keys)

Why: Uses static keypairs, not ephemeral keys like Signal’s Double Ratchet.

Mitigation:

  • Keep your private key secure
  • Consider rotating keys periodically
  • Use NIP-17 for better security (see below)

2. Metadata Leakage

The following is publicly visible:

  • Who you’re messaging (recipient pubkey in p tag)
  • When you sent the message (created_at timestamp)
  • Message frequency and timing patterns

Why: Kind 4 events are stored on public relays.

Mitigation:

  • Relays can’t read content, but they can see metadata
  • Consider using privacy-focused relays
  • Be aware of traffic analysis risks

3. Replay Attacks

Malicious relays can:

  • Serve old messages as new
  • Duplicate messages
  • Reorder message delivery

Why: No sequence numbers or timestamp verification.

Mitigation:

  • Clients should deduplicate by event ID
  • Display timestamps to users
  • Treat DMs as eventually-consistent

4. Padding Oracle Attacks

Improper PKCS7 padding validation could leak information.

Mitigation: Use constant-time padding validation (library-dependent).


NIP-17: Improved Private Messages

⚠️ NIP-04 is deprecated in favor of NIP-17 for new implementations.

NIP-17 provides:

  • ✅ Sealed sender (hides sender identity from relays)
  • ✅ Better metadata protection
  • ✅ Improved key derivation
  • ✅ Gift wrapping for privacy

However: NIP-04 remains widely supported for backward compatibility.

Recommendation:

  • New clients should implement NIP-17
  • Continue supporting NIP-04 for legacy messages
  • Encourage users to migrate

Client Support

Full NIP-04 Support

  • Damus - Encrypted DMs with clean UI
  • Primal - Fast DM sync across platforms
  • Amethyst - Android-native encrypted messaging
  • Snort - Web-based DMs
  • Iris - Simple DM interface

Partial Support

  • Some clients may not implement DMs at all
  • Some use NIP-17 instead

Check our Client Directory for specific features.


Privacy Best Practices

For Users

  1. Understand the limitations: NIP-04 is not Signal or WhatsApp
  2. Don’t share highly sensitive info: Use stronger tools for that
  3. Verify recipient’s key: Ensure you’re messaging the right person
  4. Use trusted relays: They can’t read messages but see metadata
  5. Secure your private key: Compromise = all DMs exposed

For Developers

  1. Implement NIP-17 for new clients (better security)
  2. Support NIP-04 for backward compatibility
  3. Warn users about metadata visibility
  4. Use secure libraries: Don’t roll your own crypto
  5. Constant-time operations: Prevent timing attacks

Common Questions

Can relays read my DMs?

No. Relays store encrypted content. Without your private key and the other person’s public key, they can’t decrypt messages.

But: Relays can see who you’re messaging and when.

Can I delete DMs?

You can send a deletion request (NIP-09), but:

  • Relays may ignore it
  • Recipients already have the message
  • True deletion is impossible

Are group DMs supported?

Not in NIP-04. It only supports 1-to-1 messaging. For groups:

  • Use NIP-28 (public chat)
  • Use NIP-17 (private groups - experimental)

What if I lose my private key?

You lose access to:

  • All DM history
  • Ability to decrypt future messages to that key
  • Ability to send DMs from that identity

No recovery is possible. Back up your key securely.


  • NIP-01 - Basic protocol (event structure)
  • NIP-17 - Private direct messages (improved security)
  • NIP-44 - Encrypted payloads (versioned encryption)

For Developers

Implementation Checklist

Sending DMs:

  • Compute shared secret using ECDH
  • Generate random IV for each message
  • Encrypt with AES-256-CBC
  • Format: base64_encrypted?iv=base64_iv
  • Create kind 4 event with p tag
  • Sign and publish

Receiving DMs:

  • Subscribe to kind 4 events (#p = your pubkey)
  • Compute shared secret with sender’s pubkey
  • Parse content (split on ?iv=)
  • Decrypt with AES-256-CBC
  • Handle decryption errors gracefully

Security:

  • Use secure crypto libraries
  • Never log private keys or shared secrets
  • Warn users about metadata visibility
  • Deduplicate messages by event ID
  • Consider implementing NIP-17 instead

JavaScript:

import { getSharedSecret } from 'nostr-tools';
import CryptoJS from 'crypto-js';

// Or use built-in nip04 helpers
import { nip04 } from 'nostr-tools';

const encrypted = await nip04.encrypt(privateKey, recipientPubkey, message);
const decrypted = await nip04.decrypt(privateKey, senderPubkey, encrypted);

Python:

from nostr.nip04 import encrypt_message, decrypt_message

encrypted = encrypt_message(private_key, recipient_pubkey, "Hello!")
decrypted = decrypt_message(private_key, sender_pubkey, encrypted)

Technical Specification

For the complete technical specification, see NIP-04 on GitHub.


Summary

NIP-04 enables encrypted direct messaging on Nostr:

Kind 4 events for private messages ✅ AES-256-CBC encryption with ECDH shared secrets ✅ End-to-end encrypted (relays can’t read content) ✅ Portable across all clients

⚠️ Security limitations:

  • No perfect forward secrecy
  • Metadata is public
  • Vulnerable to replay attacks

For better security, consider NIP-17 (improved private messages).


Next Steps:


Last updated: January 2024 Official specification: GitHub

Client Support

This NIP is supported by the following clients:

damus primal amethyst snort iris coracle nostrudel nos nostur plebstr current
View all clients →

Related NIPs

NIP-01 NIP-17 NIP-44
← Browse All NIPs