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.
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:
- Encrypted tokens are shared among group members instead of direct device tokens
- Notification server acts as a privacy-preserving intermediary that cannot determine message content or sender
- User control allows opting out of push notifications entirely
- Minimal metadata is exposed to Apple/Google (just that a notification occurred, not content or sender)
- Client Application: Marmot-compatible messaging app that generates encrypted tokens
- Notification Server: State-minimized server that decrypts tokens and triggers APNs/FCM
- Apple/Google Services: Standard push notification infrastructure (APNs/FCM)
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
When a user enables push notifications, the client:
- Obtains device token from the native push service using standard OS APIs:
- iOS: APNs token via
UNUserNotificationCenter - Android: FCM token via Firebase SDK
- iOS: APNs token via
- Uses notification server's Nostr public key (hardcoded in client or discovered via NIP-89 in future versions)
- Discovers inbox relays via kind 10050 event published by the notification server public key.
- Encrypts device token using the server's public key (with random nonce for probabilistic encryption)
- Shares encrypted token with all group members
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:
- Correlating a user's presence across multiple groups
- Linking encrypted tokens back to the user who created them
- Connecting notification trigger events to specific device tokens
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:
- Extracting the first 32 bytes as the ephemeral public key
- Extracting the next 12 bytes as the nonce
- Using the ephemeral public key and its private key to derive the same encryption key via ECDH + HKDF
- Decrypting the remaining 236 bytes as the ciphertext (produces 220-byte padded payload)
- 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
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.
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):
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
tokentags, 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: 10050event 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
tokentags 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
sigfield) per MIP-03 security requirements - Clients MUST use
kind: 447events to refresh their own token. Clients seeingkind: 447events for members already in the group MUST replace the stored token for the sender's leaf node.
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
tokentags, 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: 10050event can be found - Fourth value: MLS leaf index (decimal string) of the device that owns this token
- e: The id of the
kind: 447event 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: 447event - Multiple responses are acceptable and help ensure new members receive complete information
- This event MUST remain unsigned (no
sigfield) per MIP-03 security requirements
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
sigfield) per MIP-03 security requirements - Receiving clients MUST remove the token for the leaf node identified by the MLS sender identity
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
- 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
Clients SHOULD send token updates when:
- Device first enables notifications in the app (send
kind: 447in 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: 449before leaving) - App is uninstalled (best-effort via OS hooks, send
kind: 449)
When MLS Remove operations occur (user leaves, admin removes user, device removed):
- All members process the MLS Remove operation
- All members automatically delete tokens for the removed leaf_index(es)
- 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.
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
Initial join flow:
- Device joins group via MLS Add or External Commit
- Immediately creates and sends a
kind: 447(Token Request) event including its own tokens - Wraps the event in an MLS Application Message and publishes as
kind: 445 - Receives
kind: 448(Token List) responses and merges tokens into local store
Token management:
- Devices can send
kind: 447events at any time to refresh their view - Use MLS message ordering (epoch + message index) to resolve simultaneous updates
- When processing
kind: 447events, 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
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
The notification trigger uses NIP-59 Gift Wrap with the following layered structure:
- Rumor (kind 446 - Marmot Notification Request): An unsigned event containing the notification data
- Seal (kind 13): A signed event that authenticates the rumor using the same ephemeral key
- 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
contentfield of the rumor is a base64-encoded blob of concatenated encrypted tokens (each token is exactly 280 bytes) - The rumor's
pubkeyMUST 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
pubkeyfield, 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_atSHOULD 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: 446events.
When a device sends a message to a group:
- 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)
- Filter active leaves: Only include tokens from leaves that still exist in the MLS tree (removes tokens from departed members)
- 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.
- 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.
- Create rumor: Concatenate all tokens (real + decoys, shuffled), base64-encode the result, create unsigned
kind: 446event with ephemeral pubkey - Create seal: Encrypt the rumor to the notification server's pubkey per NIP-59, sign the
kind: 13seal with the SAME ephemeral key used in the rumor - Gift wrap: Encrypt the seal to the notification server's pubkey per NIP-59 using a separate outer ephemeral key
- Publish: Send gift-wrapped event to notification server's inbox relays
- 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)
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.
The notification server advertises its inbox relays via kind 10050 event. Clients SHOULD publish to all advertised relays to ensure delivery.
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.
For maximum privacy, clients MAY support publishing gift-wrapped events through Tor:
- User setting: "Use Tor for notifications" (disabled by default)
- Tor integration: Use OS Tor daemon or embedded Tor library
- Circuit management: Use fresh circuits periodically to prevent correlation
- 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.
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.
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.
Users who completely opt out of Apple/Google push notifications:
- Don't generate tokens: Skip device token registration entirely
- Can still trigger notifications: Publish kind 1059 gift-wrapped events for groups they're in if desired
- Alternative mechanisms: Rely on background fetch or manual app opening
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: 10050event - 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
sigfield) per MIP-03 requirements - Verify sender's MLS identity when processing token events
- Maintain local token store indexed by
leaf_indexwithserver_pubkeyandadded_attimestamp - 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: 446with ephemeral pubkey), seal (kind: 13signed 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: 447requests - Skip responding to
kind: 447if 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: 446events: 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
Servers MUST:
- Implement state-minimized design with no persistent token storage
- Publish
kind: 10050inbox relay list for clients to discover - Monitor inbox relays for
kind: 1059gift-wrapped events addressed to the server's pubkey - Unwrap gift-wrapped events per NIP-59: decrypt the outer layer to extract the
kind: 13seal, then decrypt the seal to extract thekind: 446rumor - 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
This specification protects against:
- 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
- Relay surveillance: Gift wrap hides sender identity; only notification server's pubkey is visible
- Network surveillance: Tor support prevents IP correlation; gift wrap encryption hides event content
- 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
- Token correlation: Different ciphertext per group prevents cross-group tracking
- Recipient enumeration: Decoys obscure exact recipient count
- 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
Users must trust:
- Notification server operator: To run advertised code and not log excessively (mitigated by transparency and open source)
- Apple/Google: To deliver notifications (required for push functionality)
- 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
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.