Skip to content

Latest commit

 

History

History
588 lines (442 loc) · 32.1 KB

File metadata and controls

588 lines (442 loc) · 32.1 KB

MIP-05

Push Notifications

draft optional

This document defines a privacy-preserving approach to delivering push notifications for Marmot group messages through Apple and Google push notification services while minimizing metadata leakage and protecting user privacy.

Overview

Users expect timely push notifications for new messages in their chat applications. While most users accept that notifications go through Apple (APNs) and Google (FCM) push notification services, a subset of privacy-conscious users (activists, journalists, etc.) require the ability to opt-out of these centralized services.

This specification provides a system where:

  1. Encrypted tokens are shared among group members instead of direct device tokens
  2. Notification server acts as a privacy-preserving intermediary that cannot determine message content or sender
  3. User control allows opting out of push notifications entirely
  4. Minimal metadata is exposed to Apple/Google (just that a notification occurred, not content or sender)

Architecture

Components

  1. Client Application: Marmot-compatible messaging app that generates encrypted tokens
  2. Notification Server: State-minimized server that decrypts tokens and triggers APNs/FCM
  3. Apple/Google Services: Standard push notification infrastructure (APNs/FCM)

Privacy Properties

The system is designed to minimize information leakage:

  • Apple/Google learn: A notification occurred for a device, and the device owner's platform identity (Apple ID / Google account) via the push token—this is unavoidable with push.
  • Apple/Google CANNOT learn: Message content, sender's Nostr identity, recipient's Nostr identity, group membership, or any relationship between the device and specific Nostr accounts
  • Notification server learns: Timing of notification events arriving at relays
  • Notification server CANNOT learn: Message content, sender's Nostr identity, recipient's Nostr identity, group membership, which Nostr user owns which device token, sender IP address (only relays observe sender IPs)
  • Relays learn: Sender IP address, size and timing of notification trigger events
  • Relays CANNOT learn: Message content, sender identity (events are gift-wrapped), recipient identity, group membership, which device tokens are being triggered, relationship between notification events and specific users
  • Critical privacy property: Ephemeral keypairs are used at every layer—token encryption, kind 446 rumor pubkey, and gift wrap—preventing the server from linking any notification event to user identities

Encrypted Token Generation

Device Token Registration

When a user enables push notifications, the client:

  1. Obtains device token from the native push service using standard OS APIs:
    • iOS: APNs token via UNUserNotificationCenter
    • Android: FCM token via Firebase SDK
  2. Uses notification server's Nostr public key (hardcoded in client or discovered via NIP-89 in future versions)
  3. Discovers inbox relays via kind 10050 event published by the notification server public key.
  4. Encrypts device token using the server's public key (with random nonce for probabilistic encryption)
  5. Shares encrypted token with all group members

Encryption Format

Encrypted tokens use probabilistic encryption to ensure that the same device token produces different ciphertexts for each group, preventing cross-group correlation:

// Server's Nostr public key
server_pubkey = "8f3e2b1a4c5d6e7f..."  // 32-byte secp256k1 pubkey

// CRITICAL: Generate NEW ephemeral keypair for EACH token encryption.
// Reusing keypairs allows the server to correlate tokens across groups.
ephemeral_privkey = random_bytes(32)  // Random secp256k1 private key
ephemeral_pubkey = secp256k1.get_pubkey(ephemeral_privkey)  // Derive public key

// CRITICAL: Key derivation MUST use ECDH + HKDF with exact parameters.
// Incorrect derivation breaks interoperability and may weaken privacy.
shared_x = secp256k1_ecdh(ephemeral_privkey, server_pubkey)
prk = HKDF-Extract(salt="mip05-v1", IKM=shared_x)
encryption_key = HKDF-Expand(prk, "mip05-token-encryption", 32)

// CRITICAL: Generate NEW random nonce for EACH encryption.
// Nonce reuse with the same key completely breaks ChaCha20-Poly1305 security.
nonce = random_bytes(12)

// Construct padded payload (see Payload Padding section below)
padded_payload = construct_padded_payload(platform_identifier, device_token)

// CRITICAL: Use ChaCha20-Poly1305 with empty AAD exactly as specified.
// The Poly1305 tag provides authentication; tampering detection fails without it.
ciphertext = ChaCha20-Poly1305.encrypt(
    key: encryption_key,
    nonce: nonce,
    plaintext: padded_payload,
    aad: ""  // Empty AAD - MUST be empty for interoperability
)

// Token package includes ephemeral pubkey, nonce, and ciphertext
encrypted_token = ephemeral_pubkey || nonce || ciphertext  // 32 + 12 + 236 = 280 bytes

Probabilistic Property: The combination of ephemeral keypairs and random nonces ensures that encrypting the same device token multiple times (for different groups) produces different ciphertexts with no linkage to the user's identity. This prevents the notification server from:

  1. Correlating a user's presence across multiple groups
  2. Linking encrypted tokens back to the user who created them
  3. Connecting notification trigger events to specific device tokens

Payload Padding

To prevent group members from inferring other members' platforms (APNs vs FCM) from encrypted token size, all payloads are padded to a uniform size before encryption.

Padded Payload Format (220 bytes total):

padded_payload = platform_byte || token_length || device_token || random_padding

Where:
- platform_byte:  1 byte  (0x01 = APNs, 0x02 = FCM)
- token_length:   2 bytes (big-endian uint16, length of device_token)
- device_token:   variable (APNs: 32 bytes, FCM: up to 200 bytes)
- random_padding: variable (fills remainder to reach 220 bytes total)

Encryption produces uniform 280-byte tokens:

  • Padded payload: 220 bytes
  • ChaCha20-Poly1305 auth tag: 16 bytes
  • Ciphertext total: 236 bytes
  • Encrypted token: 32 (ephemeral pubkey) + 12 (nonce) + 236 (ciphertext) = 280 bytes

Example - APNs token (32 bytes):

platform_byte  = 0x01
token_length   = 0x00 0x20  (32 in big-endian)
device_token   = <32 bytes>
random_padding = <185 random bytes>
Total: 1 + 2 + 32 + 185 = 220 bytes

Example - FCM token (200 bytes):

platform_byte  = 0x02
token_length   = 0x00 0xC8  (200 in big-endian)
device_token   = <200 bytes>
random_padding = <17 random bytes>
Total: 1 + 2 + 200 + 17 = 220 bytes

Server Decryption: The server decrypts by:

  1. Extracting the first 32 bytes as the ephemeral public key
  2. Extracting the next 12 bytes as the nonce
  3. Using the ephemeral public key and its private key to derive the same encryption key via ECDH + HKDF
  4. Decrypting the remaining 236 bytes as the ciphertext (produces 220-byte padded payload)
  5. Parsing the padded payload:
    • Read platform_byte (1 byte): 0x01 = APNs, 0x02 = FCM
    • Read token_length (2 bytes, big-endian)
    • Read device_token (token_length bytes)
    • Discard remaining padding

Server Discovery

The notification server's Nostr public key is typically hardcoded in the client at build time. This is because iOS enforces a link between the bundle ID of the app and the credentials of the notification server registered with APNS. Clients SHOULD query kind: 10050 events for servers to find their inbox relays.

Token Distribution

Gossip-Based Token Synchronization

Encrypted tokens are shared using a gossip-style protocol where each group member maintains a complete list of all active encrypted device tokens in the group. This approach provides redundancy, fast convergence, and works well even when members are offline.

All token distribution uses standard Marmot Group Messages (MIP-03) as application messages inside kind: 445 Group Events. The inner application messages use the following Nostr event kinds (all unsigned as per MIP-03):

Token Request (kind 447)

When a device joins a group or needs to refresh its view of active tokens, it sends an unsigned kind: 447 event as an application message:

{
    "kind": 447,
    "created_at": 1693876700,
    "pubkey": "<sender_nostr_pubkey>",
    "content": "",
    "tags": [
        ["token", "<base64-encoded-encrypted-token>", "<hex-encoded-server-pubkey>", "<relay_hint>"]
    ]
}

Tags:

  • token: One or more token tags, each containing:
    • First value: Complete encrypted token (ephemeral_pubkey || nonce || ciphertext) as base64
    • Second value: The notification server's public key used for encryption (hex). This is required for interoperability between different clients.
    • Third value: A relay hint URL where the notification server's kind: 10050 event can be found. This helps senders discover the server's inbox relays even if they don't share relays with the server.

Notes:

  • The requesting device's own token is included in token tags to bootstrap it into the group immediately
  • The MLS leaf index is implicit from the MLS sender identity (verified by MLS layer)
  • This event MUST remain unsigned (no sig field) per MIP-03 security requirements
  • Clients MUST use kind: 447 events to refresh their own token. Clients seeing kind: 447 events for members already in the group MUST replace the stored token for the sender's leaf node.

Token List Response (kind 448)

Any group member may respond with their complete view of all tokens as an unsigned kind: 448 event:

{
    "kind": 448,
    "created_at": 1693876750,
    "pubkey": "<sender_nostr_pubkey>",
    "content": "",
    "tags": [
        ["token", "<base64-encoded-encrypted-token>", "<hex-encoded-server-pubkey>", "<relay_hint>", "<leaf_index>"],
        ["token", "<base64-encoded-encrypted-token>", "<hex-encoded-server-pubkey>", "<relay_hint>", "<leaf_index>"],
        ["e", "<event_id of 447 you're responding to>"]
    ]
}

Tags:

  • token: One or more token tags, each containing:
    • First value: Complete encrypted token as base64
    • Second value: The notification server's public key (hex)
    • Third value: A relay hint URL where the notification server's kind: 10050 event can be found
    • Fourth value: MLS leaf index (decimal string) of the device that owns this token
  • e: The id of the kind: 447 event this is in response to

Response behavior:

  • Members SHOULD add a random delay (0-2 seconds) before responding to avoid simultaneous responses
  • After the delay, members SHOULD skip responding if they observe another member already responded to the same kind: 447 event
  • Multiple responses are acceptable and help ensure new members receive complete information
  • This event MUST remain unsigned (no sig field) per MIP-03 security requirements

Token Removal (kind 449)

When a device leaves a group or wants to disable notifications, it sends an unsigned kind: 449 event as an application message:

{
    "kind": 449,
    "created_at": 1693876700,
    "pubkey": "<sender_nostr_pubkey>",
    "content": "",
    "tags": []
}

Notes:

  • The MLS leaf index is implicit from the MLS sender identity (verified by MLS layer)
  • This event MUST remain unsigned (no sig field) per MIP-03 security requirements
  • Receiving clients MUST remove the token for the leaf node identified by the MLS sender identity

Token Storage

Each client maintains a local token store (e.g., SQLite database) with the following structure:

struct StoredToken {
    encrypted_token: Vec<u8>,    // Encrypted token bytes
    server_pubkey: PublicKey,    // Notification server public key
    relay_hint: String,          // Relay URL hint for server's kind 10050
    leaf_index: LeafIndex,       // Which device owns this token (one token per leaf)
    added_at: i64,               // Timestamp for pruning stale entries
}

Note: Each MLS leaf index has at most one token. If a client receives a new token for a leaf index that already has a token stored, the new token replaces the old one.

Storage properties:

  • Tokens are indexed by leaf_index for efficient removal
  • Tokens older than 35 days SHOULD be pruned (spec recommends 30-day refresh)
  • When MLS Remove operations occur, tokens for removed leaves MUST be deleted

Gossip Protocol Properties

  • Passive learning: Members observe all token messages and update their local store, creating natural convergence
  • Redundancy: Multiple members can respond to requests, ensuring delivery even when some are offline
  • Fast convergence: New members share their tokens in the request, incorporating them immediately
  • Self-healing: Members can send token requests at any time to refresh their view
  • Multi-device support: Each device (MLS leaf) manages its own tokens independently

Token Update Triggers

Clients SHOULD send token updates when:

  • Device first enables notifications in the app (send kind: 447 in all groups)
  • Device joins a new group (send kind: 447)
  • Device token changes due to OS-initiated refresh (send kind: 447)
  • Periodic refresh (recommended: every 25-35 days with random jitter to prevent timing patterns, send kind: 447)

Clients SHOULD send token removal messages when:

  • Device disables notifications (send kind: 449)
  • Device is removed from the group (send kind: 449 before leaving)
  • App is uninstalled (best-effort via OS hooks, send kind: 449)

Automatic Cleanup

When MLS Remove operations occur (user leaves, admin removes user, device removed):

  1. All members process the MLS Remove operation
  2. All members automatically delete tokens for the removed leaf_index(es)
  3. No explicit kind: 449 (Token Removal) message is required (though it's polite to send one before leaving)

This ensures token lists stay synchronized with actual group membership.

Stale token cleanup: In edge cases where a user reinstalls their app without sending a removal message, the old token may persist. However, these stale tokens are eventually cleaned up when clients evict inactive leaves from the group—a recommended practice for group hygiene (see MIP-03 Group Hygiene). The MLS Remove operation for the inactive leaf triggers automatic token deletion.

Multi-Device Scenarios

Each device (MLS leaf) manages tokens independently:

  • Multiple devices: User with iPhone (leaf 3) and Desktop (leaf 7) has tokens at both leaf indices
  • Token updates: Updating tokens for one device doesn't affect other devices
  • Device removal: MLS Remove operation automatically triggers token cleanup for that leaf
  • User removal: All of user's device tokens are removed via cleanup of all their leaf indices

Implementation Considerations

Initial join flow:

  1. Device joins group via MLS Add or External Commit
  2. Immediately creates and sends a kind: 447 (Token Request) event including its own tokens
  3. Wraps the event in an MLS Application Message and publishes as kind: 445
  4. Receives kind: 448 (Token List) responses and merges tokens into local store

Token management:

  • Devices can send kind: 447 events at any time to refresh their view
  • Use MLS message ordering (epoch + message index) to resolve simultaneous updates
  • When processing kind: 447 events, verify sender's MLS identity
  • Prune tokens older than 35 days

Event handling:

  • All notification events (447, 448, 449) are MLS Application Messages
  • These events follow the same encryption and authentication flow as chat messages (kind 9)
  • Inner events MUST be unsigned to prevent leakage if accidentally exposed
  • Sender identity is verified through MLS authentication, not event signatures

Handling decoy notifications:

  • Clients will sometimes receive silent push notifications that result in no new messages (caused by decoy tokens from other users' groups)
  • This is expected behavior and essential for privacy—decoys prevent the notification server from inferring social graphs
  • Clients MUST handle this gracefully: wake up, check relays for new messages, find nothing, and return to sleep without alerting the user
  • Do not display "no new messages" errors or warnings to users when this occurs

Triggering Notifications

Gift-Wrapped Notification Events

When a user sends a message to a group, their client triggers notifications by publishing a gift-wrapped Nostr event to the notification server's inbox relays. This approach:

  • Maintains Nostr-native protocol: Uses standard Nostr event types
  • Provides sender privacy: Gift wrap hides the sender's identity from relays
  • Enables relay-based delivery: Leverages existing Nostr infrastructure
  • Supports asynchronous delivery: Server processes events from its inbox

Event Structure

The notification trigger uses NIP-59 Gift Wrap with the following layered structure:

  1. Rumor (kind 446 - Marmot Notification Request): An unsigned event containing the notification data
  2. Seal (kind 13): A signed event that authenticates the rumor using the same ephemeral key
  3. Gift Wrap (kind 1059): An encrypted wrapper that hides the seal from relays

Rumor Event (kind 446):

{
    "kind": 446,
    "content": "<base64_of_concatenated_tokens>",
    "tags": [],
    "pubkey": "<random_ephemeral_pubkey>",
    "created_at": <timestamp>,
    "id": "<event_id>"
}

Seal Event (kind 13):

{
    "kind": 13,
    "content": "<encrypted_rumor>",
    "tags": [],
    "pubkey": "<same_ephemeral_pubkey_as_rumor>",
    "created_at": <timestamp>,
    "id": "<event_id>",
    "sig": "<signature_by_ephemeral_key>"
}

Gift Wrap Event (kind 1059):

{
    "kind": 1059,
    "content": "<encrypted_seal>",
    "tags": [["p", "<notification_server_pubkey>"]],
    "pubkey": "<outer_ephemeral_pubkey>",
    "created_at": <timestamp>,
    "id": "<event_id>",
    "sig": "<signature_by_outer_ephemeral_key>"
}

Notes:

  • The content field of the rumor is a base64-encoded blob of concatenated encrypted tokens (each token is exactly 280 bytes)
  • The rumor's pubkey MUST be a freshly generated random ephemeral key (not the sender's identity) to prevent the server from linking notification events to users
  • The seal MUST be signed by the SAME ephemeral key used in the rumor's pubkey field, providing authentication without revealing the real sender's identity
  • The gift wrap uses a separate ephemeral key and is addressed to the notification server's pubkey
  • All events follow NIP-59 structure and created_at SHOULD be randomized to prevent timing analysis

Size Constraints: The kind 446 event content is limited by the NIP-59 gift wrap process and relay event size limits (commonly 64-128KB depending on relay implementation):

  • Uniform encrypted token size: 280 bytes — see Payload Padding section
  • Content encoding: All tokens concatenated, then base64-encoded as a single blob
  • Practical capacity (assuming 64KB limit, ~60KB usable after event overhead): ~160 tokens per event
  • Recommendation: Batch at 100 tokens per event to stay well under limits across all relay implementations. For groups with more than 100 members, clients MUST send multiple kind: 446 events.

Processing Flow

When a device sends a message to a group:

  1. Collect tokens: Get all stored encrypted tokens from all active leaves in the group (all tokens are already uniform 280 bytes due to payload padding during encryption)
  2. Filter active leaves: Only include tokens from leaves that still exist in the MLS tree (removes tokens from departed members)
  3. Handling multiple notification servers: It's possible that the tokens will be from multiple different notification servers (from different clients). If so, do the following steps for each server. Note: this reveals which notification servers (and thus which clients) group members use—a known metadata tradeoff for interoperability.
  4. Add decoys: Include real encrypted tokens as decoys to obscure group size (random bytes don't work—real tokens have valid curve points and pass AEAD authentication, so servers trivially distinguish random bytes):
    • Self-decoy: With ~50% probability, include your own encrypted token. The server cannot identify which token belongs to the sender (due to ephemeral keys), so this adds ±1 ambiguity to the recipient count.
    • Other-user decoys: Randomly include tokens from your other Marmot groups (10-20% of real token count, minimum 3 decoys total). These decrypt successfully; those users receive a silent wake, check relays, find nothing new, and return to sleep. This adds genuine noise and prevents the server from inferring social graphs by observing which tokens are bundled together. If the user has no tokens from other groups, use only the self-decoy and the tokens from the current group.
    • Shuffle all tokens (real + decoys) before concatenation.
  5. Create rumor: Concatenate all tokens (real + decoys, shuffled), base64-encode the result, create unsigned kind: 446 event with ephemeral pubkey
  6. Create seal: Encrypt the rumor to the notification server's pubkey per NIP-59, sign the kind: 13 seal with the SAME ephemeral key used in the rumor
  7. Gift wrap: Encrypt the seal to the notification server's pubkey per NIP-59 using a separate outer ephemeral key
  8. Publish: Send gift-wrapped event to notification server's inbox relays
  9. Server processes:
    • Unwraps the gift wrap using its private key per NIP-59
    • Decrypts and verifies the inner seal
    • Extracts and validates the kind 446 rumor (verifies the rumor's pubkey matches the seal's pubkey)
    • Base64-decodes the rumor's content, splits into 280-byte chunks
    • For each chunk: extracts ephemeral pubkey, derives encryption key via ECDH + HKDF, decrypts ciphertext
    • Parses padded payload to extract platform and device token
    • Sends push notifications to APNs/FCM for valid tokens (silently ignores decoys/invalid tokens)

Notification Payload

The push notification sent to APNs/FCM contains minimal information:

  • Silent notification: No content, title, or sender information
  • Content-available flag: Triggers background fetch in the app
  • Badge increment: Indicates new activity without specifics

The app fetches and decrypts actual messages from relays when awakened by the notification.

Notification Server Inbox Relays

The notification server advertises its inbox relays via kind 10050 event. Clients SHOULD publish to all advertised relays to ensure delivery.

Platform Integration

Native Push Services Only

This specification requires native platform tokens:

  • iOS devices: APNs tokens from Apple's UNUserNotificationCenter API
  • Android devices: FCM tokens from Firebase Cloud Messaging SDK
  • Desktop clients: Should skip token registration entirely and rely on other devices for notifications

See Client Requirements for the critical prohibition against using FCM as a proxy for iOS devices.

Privacy Enhancements

Tor Support

For maximum privacy, clients MAY support publishing gift-wrapped events through Tor:

  1. User setting: "Use Tor for notifications" (disabled by default)
  2. Tor integration: Use OS Tor daemon or embedded Tor library
  3. Circuit management: Use fresh circuits periodically to prevent correlation
  4. Relay connections: Connect to notification server's inbox relays via Tor

This prevents relays and the notification server from learning the sender's IP address.

Rate Limiting and DoS Protection

Server-side:

  • Verify gift wrap structure before processing
  • Leverage relay-level spam protection (AUTH, rate limiting by IP)
  • Use multiple inbox relays for redundancy

Client-side:

  • Batch multiple messages into single notification event (1-2 second delay)
  • Don't send notifications for every keystroke or unnecessary event

Note: Since kind 446 events use ephemeral pubkeys for privacy, identity-based rate limiting is not possible. DoS protection relies primarily on relay-level mechanisms. Future versions may explore proof-of-work or other privacy-preserving rate limiting approaches.

Event Size Padding

To obscure patterns: ensure kind 446 content reaches minimum size (e.g., 10 tokens) using decoy tokens from other groups and self-decoys, use randomized created_at timestamps per NIP-59, and leverage gift wrap encryption for consistent appearance.

Privacy-Only Users

Users who completely opt out of Apple/Google push notifications:

  1. Don't generate tokens: Skip device token registration entirely
  2. Can still trigger notifications: Publish kind 1059 gift-wrapped events for groups they're in if desired
  3. Alternative mechanisms: Rely on background fetch or manual app opening

Implementation Requirements

Client Requirements

Clients MUST:

  • Never use FCM as a proxy for iOS: iOS clients MUST use APNs tokens directly via UNUserNotificationCenter, not FCM. Using FCM for both platforms would allow Google to correlate users across iOS/Android and across groups, defeating the probabilistic encryption privacy guarantees that prevent cross-group tracking.
  • Use native platform tokens: APNs tokens on iOS, FCM tokens on Android via Firebase SDK
  • Use the hardcoded notification server public key for token encryption
  • Fetch the server's inbox relays from the server's kind: 10050 event
  • Implement probabilistic token encryption per the Encryption Format and Payload Padding sections: generate ephemeral secp256k1 keypair per token, derive encryption key via ECDH + HKDF (Extract then Expand with info "mip05-token-encryption"), construct 220-byte padded payload, encrypt with ChaCha20-Poly1305 using random 12-byte nonce
  • Use padded payload format: platform_byte (0x01=APNs, 0x02=FCM) || token_length (2 bytes BE) || device_token || random_padding (to 220 bytes total)
  • Implement handlers for token distribution events (447, 448, 449) as the payload of MLS Application Messages (kind: 445)
  • Ensure all token distribution events (447, 448, 449) remain unsigned (no sig field) per MIP-03 requirements
  • Verify sender's MLS identity when processing token events
  • Maintain local token store indexed by leaf_index with server_pubkey and added_at timestamp
  • Automatically delete tokens for removed leaves when processing MLS Remove operations
  • Create gift-wrapped notification events per NIP-59 with the proper layered structure: rumor (kind: 446 with ephemeral pubkey), seal (kind: 13 signed by the same ephemeral key), and gift wrap (kind: 1059)
  • Only include tokens from active MLS leaves when triggering notifications
  • Handle multiple notification servers: if tokens are from different servers, create separate notification events for each

Clients SHOULD:

  • Send kind: 447 (Token Request) with own token when joining a group, when device token changes, and periodically (every 25-35 days with random jitter)
  • Send kind: 449 (Token Removal) when disabling notifications or before leaving a group
  • Add random delay (0-2 seconds) before responding to kind: 447 requests
  • Skip responding to kind: 447 if another member has already responded to the same request
  • Passively learn from all token messages to maintain local store
  • Prune tokens older than 35 days from local store
  • Add decoy tokens to kind: 446 events: include self-decoy with ~50% probability, add tokens from other groups (10-20% of real token count, minimum 3 decoys total), shuffle all tokens together
  • Add random delay (0-30 seconds) before fetching messages after receiving a silent notification to prevent timing correlation
  • Batch notifications: wait 1-2 seconds to combine multiple messages into a single notification event
  • Implement user opt-out setting to disable push notifications entirely

Clients MAY:

  • Support publishing gift-wrapped events through Tor for enhanced sender privacy

Server Requirements

Servers MUST:

  • Implement state-minimized design with no persistent token storage
  • Publish kind: 10050 inbox relay list for clients to discover
  • Monitor inbox relays for kind: 1059 gift-wrapped events addressed to the server's pubkey
  • Unwrap gift-wrapped events per NIP-59: decrypt the outer layer to extract the kind: 13 seal, then decrypt the seal to extract the kind: 446 rumor
  • Verify seal authenticity: ensure the seal is properly signed and the rumor's pubkey matches the seal's pubkey
  • Decrypt tokens by: extracting ephemeral pubkey (first 32 bytes), nonce (next 12 bytes), and ciphertext (236 bytes); deriving encryption key via ECDH + HKDF (Extract then Expand with info "mip05-token-encryption"); decrypting with ChaCha20-Poly1305
  • Parse padded payload (220 bytes): read platform_byte (0x01=APNs, 0x02=FCM), read token_length (2 bytes BE), read device_token, discard padding
  • Verify gift wrap and inner event structure before processing
  • Maintain direct integrations with both APNs and FCM (no intermediary services)
  • Route tokens to appropriate push service based on platform identifier
  • Send silent notifications with content-available flag (no message content, title, or sender info)
  • Silently ignore invalid tokens (malformed data, expired tokens, ECDH failures, authentication failures)

Servers SHOULD:

  • Leverage relay-level spam protection and use multiple inbox relays

Security Considerations

Threat Model

This specification protects against:

  1. Notification server compromise: Server cannot read message content, cannot correlate tokens across groups (probabilistic encryption), cannot determine which tokens belong to same user, cannot link tokens to user identities
  2. Relay surveillance: Gift wrap hides sender identity; only notification server's pubkey is visible
  3. Network surveillance: Tor support prevents IP correlation; gift wrap encryption hides event content
  4. Apple/Google surveillance: Learn that a notification occurred for a device (linked to Apple ID/Google account), but cannot learn Nostr identities, message content, sender, or group membership
  5. Token correlation: Different ciphertext per group prevents cross-group tracking
  6. Recipient enumeration: Decoys obscure exact recipient count
  7. Malicious group member: Cannot update/remove other members' tokens (authorization via MLS sender authentication), can see which leaves have tokens but cannot decrypt them for notification server use, cannot forge token events for other members due to MLS authentication

Trust Assumptions

Users must trust:

  1. Notification server operator: To run advertised code and not log excessively (mitigated by transparency and open source)
  2. Apple/Google: To deliver notifications (required for push functionality)
  3. Their device OS: To properly isolate APNs/FCM tokens

The design minimizes trust requirements by:

  • Minimizing server state (no persistent storage, stateless request processing)
  • Using strong encryption with ephemeral keys at every layer
  • Allowing Tor routing
  • Supporting complete opt-out

Attack Scenarios

Compromised notification server: Cannot decrypt messages, correlate tokens across groups, determine sender/recipient identities, or link events to users. Could refuse to send notifications (user-visible failure).

Compromised relay: Sees encrypted events but cannot decrypt content or determine sender/recipients. Could drop events (mitigated by multiple relays).

Apple/Google monitoring: Learn a device (linked to Apple ID/Google account) received a notification, but cannot determine Nostr identities, sender, content, group, or message details.

Network observer: Sees encrypted events but cannot determine sender, exact recipient count (decoys), or content due to the layered encryption structure.

Malicious group member: Can send spam requests or fake tokens for their own leaf, but cannot impersonate other leaves, prevent legitimate updates, or break the gossip protocol's redundancy.

References