Encrypted Binary Attachments for DMs and MLS

Jonathan Borden (jonathan@loxation.com)

Abstract

This NIP profile standardizes encrypted binary attachments (images, audio, video, documents) for private Nostr messaging only: NIP‑17 direct messages and MLS groups. It defines:

Public, unencrypted tags (attach/eattach) are explicitly out of scope for this profile.

Motivation

Clients and servers need a consistent, interoperable, privacy‑preserving mechanism for sharing files in private contexts. This document scopes the solution to encrypted‑only delivery via NIP‑17 (1:1) and MLS (1:group), which matches our deployment and avoids ambiguity and leakage associated with public note tags.

Rationale

Definitions

Specification

1. Cipher and integrity

2. NIP‑17 direct messages (DMs)

Attachment parameters MUST live inside the DM’s encrypted content (not tags). The DM plaintext embeds a normalized JSON array of attachment objects:

{
  "type": "message",
  "text": "optional user text",
  "attachments": [
    {
      "url": "https://storage.example/enc/blob",
      "ct": "image/jpeg",
      "size": 23011,
      "sha256": "<hex_of_ciphertext>",
      "fn": "photo.jpg",
      "enc": {
        "mode": "dm",
        "algo": "A256GCM",
        "k": "<b64-32-bytes>",
        "iv": "<b64-12-bytes>",
        "t": "<b64-16-bytes>"
      },
      "alt": "a cat",
      "blurhash": "..."
    }
  ]
}

Receiver processing: 1) Decrypt the DM per NIP‑17.
2) Fetch the ciphertext bytes from url.
3) Verify sha256 over ciphertext.
4) Decrypt with enc.k/iv/t.
5) Render using ct, fn, alt, and optional hints (e.g., blurhash).

Notes: - The size field SHOULD reflect ciphertext length.
- Clients SHOULD cache both ciphertext and decrypted plaintext for efficient re‑rendering.

3. MLS group attachments

For MLS application messages, the attachment AEAD key and nonce are derived via the MLS exporter; no symmetric key material is placed in relay‑visible metadata.

Key/nonce derivation (normative): - key = MLS.exporter(label=“attachment”, context=concat(epoch, “|”, ctx), length=32)
- nonce = MLS.exporter(label=“attachment-nonce”, context=concat(epoch, “|”, ctx), length=12)

Where ctx is a stable, mutually known identifier for this attachment (e.g., server blobId or a message‑scoped attachmentId). Publishers MUST include enough metadata for receivers to compute the same ctx.

Attachment metadata embedded in or adjacent to the MLS application message SHOULD include:

{
  "url": "https://storage.example/enc/blob",
  "ct": "image/jpeg",
  "size": 23011,
  "sha256": "<hex_of_ciphertext>",
  "fn": "photo.jpg",
  "enc": {
    "mode": "mls",
    "algo": "A256GCM",
    "t": "<b64-16-bytes>",
    "mls": { "group_id": "<groupId>", "epoch": 42, "ctx": "<blobId|attachmentId>" }
  }
}

Receiver processing: 1) Use MLS state for group_id/epoch to derive key and nonce with the exporter and ctx.
2) Fetch ciphertext, verify sha256, then decrypt with derived key/nonce and verify auth tag t.

4. Out of scope

5. Storage and transport

6. Client behavior

7. Security considerations

Examples

NIP‑17 DM with encrypted attachment

{
  "type": "message",
  "attachments": [
    {
      "url": "https://cdn.example/enc/xyz",
      "ct": "image/jpeg",
      "size": 23011,
      "sha256": "55aa...",
      "fn": "photo.jpg",
      "enc": { "mode": "dm", "algo": "A256GCM", "k": "...", "iv": "...", "t": "..." }
    }
  ]
}

MLS attachment metadata (ciphertext at URL; key/nonce via exporter)

{
  "url": "https://cdn.example/enc/xyz",
  "ct": "video/mp4",
  "size": 8329001,
  "sha256": "2f3a...",
  "fn": "talk.mp4",
  "enc": {
    "mode": "mls",
    "algo": "A256GCM",
    "t": "....",
    "mls": { "group_id": "deadbeef", "epoch": 42, "ctx": "blob:e3b0c442..." }
  }
}

Implementation Status