Encryption on Nostr — NIP-04 to NIP-17
- Encryption on Nostr
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
- Never use NIP-04. It’s broken.
- NIP-44 = the encryption engine. Use it directly for any encrypted Nostr communication.
- NIP-59 Gift Wrap = the privacy layer. Always wrap DMs.
- NIP-17 = the complete DM spec. Use this for private messaging.
- All NWC communication is NIP-44 encrypted under the hood.
Write a comment