Encryption on Nostr — NIP-04 to NIP-17

Evolution of privacy: NIP-04 AES-CBC through NIP-44 ChaCha20 to NIP-59 Gift Wraps and NIP-17 DMs

Encryption on Nostr

How privacy works in an inherently public protocol.

Evolution

NIP-04 (deprecated)       NIP-44 (current)         NIP-17 (DMs)
    AES-CBC              ChaCha20-Poly1305        NIP-44 + Gift Wrap
 shared-secret ECDH      versioned, secure        metadata hidden
    │                         │                      │
    ▼                         ▼                      ▼
"We encrypted it"        "It's secure"           "They can't tell"

NIP-04 — Original DMs (DEPRECATED)

Do not use. Documented for historical context only.

# HOW IT WORKED (flawed):
shared_point = sender_privkey * recipient_pubkey
shared_key = sha256(shared_point.x)
encrypted = AES-256-CBC(shared_key, iv, plaintext)

Problems:

  • No MAC (no tamper detection)
  • IV reuse possible
  • Padding oracle attacks
  • Same shared key for all messages between two parties
  • No forward secrecy
  • pubkey in plaintext — everyone knows who’s talking

NIP-44 — Encrypted Payloads v2

Use this. The encryption standard for Nostr.

Algorithm

1. ECDH: conversation_key = sha256(ecdh(sender_priv, recipient_pub))
2. HKDF: keys = hkdf_extract_expand(conversation_key, salt)
3. Encrypt: ChaCha20-Poly1305(key, nonce, plaintext)
4. Final: base64(version || nonce || ciphertext || mac)

Features

  • ChaCha20-Poly1305: authenticated encryption (can’t tamper)
  • ECIES-like: Ephemeral keys for forward secrecy
  • HKDF-SHA256: Proper key derivation
  • Versioned: v2 format, extensible

Two Conversation Modes

Mode Use Case Description
nip44-v2-1 General Standard mode
nip44-v2-2 Private No pubkey in plaintext

Format

BUFFER:
  [0]  = 0x02 (version)
  [1-32] = nonce (32 bytes)
  [33-48] = auth tag (16 bytes)
  [49-...] = ciphertext

NIP-59 — Gift Wraps

A sealed envelope around an event. Hides metadata.

INNER EVENT (kind 4, 14, 15, etc.)
    │
    ▼ NIP-44 encrypt with recipient's key
ENCRYPTED PAYLOAD
    │
    ▼ Wrap in kind 1059 event
GIFT WRAP EVENT
    │
    ▼ Send to relays

RELAYS SEE:
  - kind 1059 (gift wrap)
  - randomized created_at
  - NO e or p tags
  - encrypted blob content
  
  → Can't tell who it's for, who it's from, what it says

Structure

{
  "kind": 1059,
  "created_at": <randomized>,
  "tags": [["p", "<random-pubkey-for-routing>"]],
  "content": "<NIP-44 encrypted inner event>"
}

The p tag is a random pubkey to help with relay routing, not tied to the real recipient.

NIP-17 — Private Direct Messages

The modern DM spec. Combines NIP-44 + NIP-59.

Sending a DM

1. Create kind 14 event (text) or kind 15 (file)
2. NIP-44 encrypt with recipient's key
3. Wrap in kind 1059 gift wrap
4. Publish to relays

Receiving DMs

1. Subscribe to kind 1059 events
2. Try NIP-44 decrypt with your key
3. If successful → unwrap inner event
4. If kind 14 → DM text
5. If kind 15 → file

DM Relay List (Kind 10050)

{
  "kind": 10050,
  "tags": [
    ["relay", "wss://dm-relay.example.com"]
  ]
}

Tells others which relay to send DMs to for you.

NIP-47 — NWC Encryption

Nostr Wallet Connect uses NIP-44 encrypted ephemeral events:

Sender App ──kind 23194 (encrypted)──▶ Wallet Service
Sender App ◄──kind 23195 (encrypted)──  Wallet Service

Both encrypted with NIP-44 using shared secret.
Ephemeral (kind 23xxx) → not stored by relays.

Key Exchange Flow

┌──────────────────────────────────────────────────────┐
│                   SHARED SECRET                       │
│                                                      │
│  Alice's private key   ×   Bob's public key          │
│         aG              =       bG                   │
│                                                      │
│        ECDH: a×(bG) = b×(aG) = abG                   │
│        sha256(abG.x) = conversation_key               │
│        HKDF(conversation_key) = encryption_key        │
└──────────────────────────────────────────────────────┘

Security Comparison

Feature NIP-04 NIP-44 NIP-17
Encryption AES-CBC ChaCha20-Poly1305 ChaCha20-Poly1305
Authentication ❌ No MAC ✅ Poly1305 ✅ Poly1305
Forward secrecy ✅ Ephemeral keys
Metadata hiding ✅ Gift wrap
Versioned ✅ v2
Tamper-proof
Standard Deprecated Final Final

Practice

Encrypt with nostr-cli

# NIP-44 encrypt
echo "secret message" | nostr encrypt <recipient-npub>

# NIP-17 DM
nostr dm <recipient-npub> "hello"

Python (cdk-nostr)

from cashu_devkit.nostr import NIP44

key = NIP44(private_key_hex, public_key_hex)
ciphertext = key.encrypt("secret")
plaintext = key.decrypt(ciphertext)

Key Takeaways

  1. Never use NIP-04. It’s broken.
  2. NIP-44 = the encryption engine. Use it directly for any encrypted Nostr communication.
  3. NIP-59 Gift Wrap = the privacy layer. Always wrap DMs.
  4. NIP-17 = the complete DM spec. Use this for private messaging.
  5. All NWC communication is NIP-44 encrypted under the hood.

Write a comment
No comments yet.