How NIP-17 Gift-Wrapping Enhances Privacy for Nostr Messages in BitChat
NIP-17 gift-wrapping in BitChat encrypts private messages using ephemeral X25519 keys and NIP-44 v2 encryption, wrapping them in an outer envelope that relays can verify without reading the content, ensuring only the intended recipient can decrypt the payload.
BitChat implements the NIP-17 gift-wrapping protocol to provide end-to-end encryption for direct messages on the Nostr network. This approach wraps sensitive payload data inside an outer cryptographic envelope, preventing relay operators and third-party observers from accessing message content while maintaining the decentralized nature of the protocol.
How NIP-17 Gift-Wrapping Works in BitChat
The implementation follows a strict separation between the outer envelope (visible to relays) and the inner encrypted payload (readable only by the recipient).
Envelope Construction with Ephemeral Keys
In bitchat/Nostr/NostrProtocol.swift, the createPrivateMessage method constructs the gift-wrapped structure. The process begins by generating an ephemeral X25519 key pair unique to this specific message. This ephemeral key ensures that each gift-wrap uses a distinct encryption key, preventing correlation attacks across multiple messages.
The inner event containing the actual message content is encrypted using NIP-44 v2 symmetric encryption via XChaCha20Poly1305Compat.encrypt, utilizing the recipient's public key. This guarantees that only the holder of the corresponding private key can decrypt the payload.
The Outer Gift-Wrap Layer
The encrypted payload is placed inside a new outer event with kind: 1059 (the standardized kind number for NIP-17 gift wraps) and kind: .giftWrap in the Swift implementation. The outer event is signed by the sender's identity, allowing relays to verify authenticity without accessing the inner content. This separation ensures that infrastructure operators see only routing metadata while the content remains confidential.
Inbound Processing and Security Controls
The receiving pipeline implements strict verification and replay protection before exposing decrypted content to the UI.
Verification and Decryption in NostrInboundPipeline
In bitchat/ViewModels/NostrInboundPipeline.swift, the handleGiftWrap method processes incoming kind 1059 events. The pipeline first verifies the outer envelope's cryptographic signature to ensure authenticity. It then checks the event ID against a processed events cache to prevent duplicate handling.
After verification, the system offloads decryption to a background actor. The method extracts the ephemeral public key from the envelope and uses the recipient's private key to derive the shared secret, decrypting the inner NIP-44 v2 payload. The decrypted content then routes to the appropriate private chat view.
Replay Attack Prevention
BitChat implements strict replay protection through event ID tracking. The context.hasProcessedNostrEvent(giftWrap.id) guard clause in NostrInboundPipeline.swift (lines 89-110 and 293-311) silently ignores duplicate event IDs. This prevents attackers from replaying captured gift-wraps across multiple relays to induce duplicate processing or extract metadata through timing analysis. Unit tests in ChatNostrCoordinatorContextTests.swift (lines 357-376) verify that duplicate gift-wraps are ignored on replay.
Code Implementation Examples
The following Swift code demonstrates the gift-wrapping workflow based on the BitChat test suite:
// Create a gift-wrapped private message using ephemeral keys
let recipient = try NostrIdentity(pubkeyHex: "<recipient-pubkey>")
let giftWrap = try NostrProtocol.createPrivateMessage(
id: UUID().uuidString,
kind: .textNote,
content: "Hello from gift wrap",
tags: [["p", recipient.pubkey]],
createdAt: Date()
)
// Publish to Nostr relays
try context.sessionFactory.latestConnection(for: relayURL)?
.emitEventMessage(subscriptionID: "gift-wraps", event: giftWrap)
On the receiving side, the pipeline automatically handles decryption:
func handleIncoming(_ event: NostrEvent) {
// Verifies signature, checks for duplicates, and decrypts in background
inboundPipeline.handleGiftWrap(event, id: recipient)
}
Unit tests in bitchatTests/NostrProtocolTests.swift (lines 26-34 and 351-363) verify the encryption format, specifically checking for the "v2:" prefix that denotes NIP-44 v2 encryption. Additional tests in bitchatTests/Services/NostrTransportTests.swift validate geohash-based private message routing using the gift-wrap mechanism handled by NostrInboundPipeline.subscribeGiftWrap.
Summary
- NIP-17 gift-wrapping encrypts message content using ephemeral X25519 keys and NIP-44 v2 via
XChaCha20Poly1305Compat.encrypt, wrapping it in a kind 1059 envelope. - Ephemeral keys ensure each message uses unique encryption parameters, preventing correlation attacks.
- Relay verification works on the outer envelope signature alone, keeping content hidden from infrastructure operators.
- Replay protection via
hasProcessedNostrEventprevents duplicate processing and replay attacks. - Background decryption in
NostrInboundPipeline.swift(lines 89-110 and 293-311) ensures UI responsiveness while maintaining security.
Frequently Asked Questions
What is NIP-17 gift-wrapping in Nostr?
NIP-17 gift-wrapping is a privacy protocol that encrypts private messages inside an outer Nostr event envelope. The outer event uses kind 1059 and contains encrypted payload data that only the intended recipient can decrypt, while relays can still verify the sender's signature on the outer wrapper to ensure authenticity.
How does ephemeral key encryption improve privacy in BitChat?
Each gift-wrapped message generates a fresh X25519 ephemeral key pair during creation in NostrProtocol.createPrivateMessage. This means compromised keys from one message cannot decrypt other messages, and observers cannot correlate multiple messages as originating from the same sender based on encryption parameters or key fingerprints.
Can relays read the content of NIP-17 gift-wrapped messages?
No. Relays only see the outer envelope with metadata necessary for routing (recipient public key, timestamp, signature). The actual content remains encrypted with NIP-44 v2 using XChaCha20Poly1305Compat.encrypt until decrypted by the recipient's client, keeping message content confidential from relay operators and network observers.
How does BitChat prevent replay attacks on gift-wrapped messages?
BitChat records every processed event ID using context.hasProcessedNostrEvent(giftWrap.id) in NostrInboundPipeline.swift. If the same gift-wrap appears on multiple relays or is resent maliciously, the duplicate ID detection silently drops the event, preventing replay attacks while maintaining efficient processing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →