Revise NIP for encrypted binary attachments in DMs

This update revises the NIP to focus on encrypted binary attachments for private messaging, clarifying the scope, specifications, and security considerations. Changes include rewording sections, updating examples, and emphasizing the use of encryption in attachments.
This commit is contained in:
jab-r 2025-08-26 22:13:55 -04:00 committed by GitHub
parent befaa2882d
commit 494dd3e146
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 89 additions and 186 deletions

View File

@ -1,6 +1,6 @@
--- ---
nip: XXX nip: XXX
title: Binary Attachments for Notes and DMs title: Encrypted Binary Attachments for DMs and MLS
author: Jonathan Borden (jonathan@loxation.com) author: Jonathan Borden (jonathan@loxation.com)
status: Draft status: Draft
type: Standards Track type: Standards Track
@ -10,89 +10,51 @@ license: CC0-1.0
## Abstract ## Abstract
This NIP standardizes how Nostr events reference binary attachments (images, audio, video, documents), supporting both unencrypted and encrypted forms. It defines: This NIP profile standardizes encrypted binary attachments (images, audio, video, documents) for private Nostr messaging only: NIP17 direct messages and MLS groups. It defines:
- Tag-based referencing for public events - A normalized JSON structure for NIP17 DMs (keys live only inside the DM ciphertext)
- A JSON structure for private DMs that keeps encryption secrets private - An MLS profile that derives perattachment AEAD key/nonce via the MLS exporter
- A per-attachment symmetric encryption scheme (AES-256-GCM) with normative sizes - A perattachment AEAD scheme (AES256GCM) with normative sizes
- Integrity fields and basic metadata for rendering - Integrity requirements (SHA256 over ciphertext)
- Optional alignment with existing NIPs (NIP-17, NIP-94, NIP-96, NIP-98) - HTTP storage guidance (presigned upload/finalize), optionally compatible with NIP96 and NIP98
Public, unencrypted tags (attach/eattach) are explicitly out of scope for this profile.
## Motivation ## Motivation
Today, clients handle attachments inconsistently. Public events often embed arbitrary tags; private DMs sometimes inline URLs or bespoke JSON. This NIP provides a consistent, privacy-preserving way to: Clients and servers need a consistent, interoperable, privacypreserving mechanism for sharing files in private contexts. This document scopes the solution to encryptedonly delivery via NIP17 (1:1) and MLS (1:group), which matches our deployment and avoids ambiguity and leakage associated with public note tags.
- Attach unencrypted files to public notes
- Attach encrypted files whose decryption keys are delivered privately (via DMs) or embedded inside DMs
- Ensure interoperability by keeping metadata normalized (MIME, size, filename, integrity)
## Rationale ## Rationale
- Nostr events should remain lightweight; binary bytes should live off-relay in HTTP-accessible storage. - Keep symmetric keys in encrypted channels only (DM content or MLS exporter). Never in public tags or relayvisible metadata.
- Unencrypted public attachments are simple URLs plus integrity metadata. - Use perattachment AEAD to avoid key reuse and enable granular sharing and revocation.
- Encrypted attachments use a per-attachment symmetric key to avoid key reuse and allow granular sharing. - Compute integrity over ciphertext so clients can verify prior to decryption and rendering.
- For public notes, encryption keys should not be exposed in cleartext (or, if made public intentionally, considered “obfuscated” not secure). - Keep events lightweight; store bytes offrelay at canonical HTTPS URLs.
- For DMs, keys live inside encrypted content; tags remain metadata-only.
## Definitions ## Definitions
- **Attachment**: A binary resource referenced by an event. - **Attachment**: A binary resource referenced by a message.
- **Ciphertext attachment**: The uploaded bytes are encrypted; clients must decrypt locally to render. - **Ciphertext attachment**: Uploaded bytes are encrypted; clients decrypt locally to render.
- **Per-attachment key**: A random 32-byte AES-256 key generated uniquely for each attachment. - **Perattachment key**: A random 32byte AES256 key generated uniquely per attachment (DMs). For MLS, the key is derived via the exporter.
## Specification ## Specification
### 1. Unencrypted attachments (public notes) ### 1. Cipher and integrity
Clients MAY attach resources using `attach` tags: - AEAD: AES256GCM
- key: 32 bytes (base64) [DMs only; MLS derives]
- iv/nonce: 12 bytes (base64)
- tag: 16 bytes (base64)
- Integrity: sha256 MUST be computed over ciphertext and verified before decrypt/render.
``` ### 2. NIP17 direct messages (DMs)
["attach", "<url>", "sha256=<hex>", "m=<mime>", "size=<bytes>", "fn=<filename>", "alt=<text>", "dim=<WxH>", "blurhash=<...>"]
```
**Required:** url, sha256, m, size Attachment parameters MUST live inside the DMs encrypted content (not tags). The DM plaintext embeds a normalized JSON array of attachment objects:
**Recommended:** fn, alt, dim, blurhash
Clients MUST verify sha256 before rendering.
### 2. Encrypted attachments
**Cipher:** AES-256-GCM
- key: 32 bytes, base64
- iv: 12 bytes, base64
- tag: 16 bytes, base64
- Ciphertext: raw GCM ciphertext at URL
Integrity: sha256 MUST be computed over ciphertext.
#### 2.1 Public notes (`eattach` tags)
```
["eattach", "<url>", "sha256=<hex>", "m=<mime>", "size=<bytes>", "fn=<filename>", "algo=A256GCM", "ekref=<event_id_or_url>", "alt=<text>"]
```
- `algo` MUST be `"A256GCM"`.
- `ekref` points to where the decryption key is conveyed (DM, URL, or replaceable event).
- Including `k/iv/t` directly in tags is discouraged (no confidentiality).
#### 2.2 Private DMs (NIP-17)
Keys MUST live inside the DMs encrypted content, not tags.
To avoid exposing symmetric keys in plaintext, this NIP specifies an envelope-wrapped key for DMs:
- Payload encryption: AES-256-GCM (per-attachment K)
- Key wrapping: X25519-HKDF-AESGCM
- Sender generates an ephemeral X25519 keypair (epk, esk)
- Shared secret s = X25519(esk, recipient_static_x25519_pub)
- wrapKey = HKDF-SHA256(s, info="attachment-key-wrap", salt=random16)
- ek = AES-GCM-Encrypt(wrapKey, nonce=random12, K)
Plaintext (before NIP17 DM encryption) embedding the metadata:
```json ```json
{ {
"type": "message", "type": "message",
"text": "optional message", "text": "optional user text",
"attachments": [ "attachments": [
{ {
"url": "https://storage.example/enc/blob", "url": "https://storage.example/enc/blob",
@ -103,32 +65,39 @@ Plaintext (before NIP17 DM encryption) embedding the metadata:
"enc": { "enc": {
"mode": "dm", "mode": "dm",
"algo": "A256GCM", "algo": "A256GCM",
"k": "<b64-32-bytes>",
"iv": "<b64-12-bytes>", "iv": "<b64-12-bytes>",
"t": "<b64-16-bytes>", "t": "<b64-16-bytes>"
"ek": "<b64-wrapped-key-ciphertext>",
"epk": "<b64-x25519-ephemeral-pub>",
"wrap": {
"alg": "X25519-HKDF-AESGCM",
"nonce": "<b64-12-bytes>",
"salt": "<b64-16-bytes>"
}
}, },
"alt": "a cat" "alt": "a cat",
"blurhash": "..."
} }
] ]
} }
``` ```
On receive, clients decrypt the DM, unwrap K using their static X25519 private key and epk, then decrypt the blob with K/iv/t. No plaintext symmetric keys appear in metadata. Receiver processing:
1) Decrypt the DM per NIP17.
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`).
#### 2.3 MLS group attachments Notes:
- The `size` field SHOULD reflect ciphertext length.
- Clients SHOULD cache both ciphertext and decrypted plaintext for efficient rerendering.
For MLS groups, the attachment AEAD key and nonce are derived from the MLS group secret and epoch via the MLS exporter (no ek/epk in metadata): ### 3. MLS group attachments
- key = MLS.exporter(label="attachment", context=concat(epoch, "|", blobId or messageId), length=32) For MLS application messages, the attachment AEAD key and nonce are derived via the MLS exporter; no symmetric key material is placed in relayvisible metadata.
- nonce = MLS.exporter(label="attachment-nonce", context=concat(epoch, "|", blobId or messageId), length=12)
Metadata embedded in the MLS application message (or carried adjacent as client policy) SHOULD include: 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 messagescoped `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:
```json ```json
{ {
@ -140,100 +109,45 @@ Metadata embedded in the MLS application message (or carried adjacent as client
"enc": { "enc": {
"mode": "mls", "mode": "mls",
"algo": "A256GCM", "algo": "A256GCM",
"iv": "<b64-12-bytes>",
"t": "<b64-16-bytes>", "t": "<b64-16-bytes>",
"mls": { "group_id": "<groupId>", "epoch": 42 } "mls": { "group_id": "<groupId>", "epoch": 42, "ctx": "<blobId|attachmentId>" }
} }
} }
``` ```
Receivers derive the same key/nonce via exporter using (groupId, epoch, context) and decrypt the blob. No key material is placed in metadata. 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`.
### 3. Relationship to Existing NIPs ### 4. Out of scope
#### 3.1 NIP-17 (DMs) - Public note tags for unencrypted or encrypted media (attach/eattach).
- Placement: For private messages, all attachment details that include encryption material MUST live inside the DMs encrypted content (see 2.2). Do not place keys in public tags. - NIP92 “imeta” and NIP94 filemetadata records for public media.
- Rendering: Receivers decrypt the DM per NIP17, fetch bytes, verify sha256 over ciphertext, then decrypt with enc.k/iv/t. This profile targets encrypted attachments delivered via NIP17 and MLS only.
#### 3.2 NIP-92 (Media Attachments) — Interoperability ### 5. Storage and transport
- Purpose: NIP92 defines “imeta” tags that annotate media URLs present in the event content. Many mediafocused clients render using imeta.
- Coexistence with NIPXXX:
- attach/eattach MAY be used alongside NIP92 imeta in the same event.
- When the event content contains a media URL, publishers SHOULD include a corresponding imeta tag so NIP92aware clients render consistently.
- When both attach/eattach and imeta are present, fields SHOULD be kept consistent.
- Field mapping (informational):
- url (positional in attach/eattach) ↔ imeta: "url <https://...>"
- m ↔ imeta: "m <mime>"
- size ↔ imeta: "size <bytes>" (if used)
- sha256 ("sha256=<hex>" in attach/eattach) ↔ imeta: "x <hex>" (per NIP94)
- alt, dim, blurhash ↔ imeta: "alt …", "dim WxH", "blurhash …"
- NIP92 "fallback" URLs MAY be included in imeta; there is no attach/eattach equivalent.
- Encrypted media:
- The imeta "url" MUST point to the ciphertext URL (the same as in eattach).
- The imeta "x" hash MUST match the sha256 over ciphertext (same value as eattach).
- Do NOT include keys in imeta or content.
- Recommended client behavior:
- If a public note includes media URLs in content, add imeta derived from attach/eattach to maximize compatibility with NIP92 renderers.
- If a note does not inline the URL in content (tagsonly publishing), imeta is not applicable; attach/eattach alone is sufficient.
#### 3.3 NIP-94 (File Metadata) - Storage: Offrelay HTTP object storage with presigned upload + finalize flows that return canonical download URLs and servercomputed metadata (size/checksum). These flows are compatible with NIP96 where applicable.
- Publication guidance (normative): - Auth: Publishers and storage providers MAY require NIP98 (HTTP Auth) for upload/finalize/download.
- Publishers MAY emit a NIP94 filemetadata event for each public attachment (attach or eattach) using the same url/m/size/sha256. This provides durable, relayindexable metadata. - Alternate device transports (e.g., BLE/Noise) MAY carry the same JSON payloads; this does not change the onwire format for Nostr DMs or MLS.
- Clients that publish a NIP94 event SHOULD reference it from the note (e.g., "e" or "a" tag per client policy).
- For encrypted attachments, NIP94 events MUST describe the ciphertext (url/m/size/sha256) and MUST NOT include decryption keys; keys live only in private channels (e.g., NIP17 DM).
- When both inline attach/eattach and a NIP94 record are present, clients SHOULD prefer inline fields for immediate render and also index the NIP94 record.
- Hash alignment:
- In attach/eattach, sha256=<hex> corresponds to the NIP94 "x" field value used by NIP92 imeta. Implementations SHOULD ensure these values match across representations.
#### 3.4 NIP-96 (HTTP File Storage) ### 6. Client behavior
- Compatibility: Presigned upload/finalize flows align with NIP96. A NIP96compliant server can provide initiation, direct upload, and finalize endpoints returning canonical download URLs and servercomputed metadata.
#### 3.5 NIP-98 (HTTP Auth) - Verify `sha256` over ciphertext before decrypt/render.
- Usage: Publishers and storage providers MAY require NIP98 for upload/finalize/download operations. Include an Authorization header per NIP98; servers verify signatures and apply rate limits/quotas. - Verify GCM auth tag during decryption.
- Show filename/thumbnail; respect accessibility fields like `alt`.
- Cache intelligently; apply quotas and safecontent policies when fetching.
### 4. Client Behavior ### 7. Security considerations
- Verify sha256 before render - Do not place keys, IVs, or tags in public tags or content.
- Show filename/thumbnail - Do not include encryption material in NIP92 `imeta` or any relayvisible metadata.
- Cache ciphertext and decrypted plaintext - Treat URLbased key delivery or external key references as nonconfidential; this profile forbids such patterns.
- Respect accessibility (`alt`) - Ensure unique IVs per key; perattachment keys simplify this, but libraries MUST still generate fresh IVs.
### 5. Security Considerations
- Do not leak keys in public tags (no confidentiality).
- Do not include encryption material (k/iv/t) in NIP92 imeta or any public content.
- Clients MUST verify sha256 and GCM auth tag before render/decrypt.
- Clients MUST NOT autoresolve ekref via untrusted schemes; keys SHOULD be conveyed via private DMs (NIP17) or other trusted mechanisms.
- Apply quotas/content scanning on downloads.
## Examples ## Examples
### Public note with two attachments ### NIP17 DM with encrypted attachment
```json
{
"kind": 1,
"content": "trip photos",
"tags": [
["attach","https://cdn.example/a.jpg","sha256=...","m=image/jpeg","size=24567","fn=beach.jpg","alt=beach","dim=1920x1080"],
["attach","https://cdn.example/itinerary.pdf","sha256=...","m=application/pdf","size=90123","fn=itinerary.pdf"]
]
}
```
### Public note with encrypted video
```json
{
"kind": 1,
"content": "members-only video (see your DM for the key)",
"tags": [
["eattach","https://cdn.example/enc/v1/xyz","sha256=...","m=video/mp4","size=8329001","fn=talk.mp4","algo=A256GCM","ekref=event:9b3e..."]
]
}
```
### DM carrying encrypted attachment
```json ```json
{ {
@ -243,46 +157,35 @@ Receivers derive the same key/nonce via exporter using (groupId, epoch, context)
"url": "https://cdn.example/enc/xyz", "url": "https://cdn.example/enc/xyz",
"ct": "image/jpeg", "ct": "image/jpeg",
"size": 23011, "size": 23011,
"sha256": "...", "sha256": "55aa...",
"fn": "photo.jpg", "fn": "photo.jpg",
"enc": { "enc": { "mode": "dm", "algo": "A256GCM", "k": "...", "iv": "...", "t": "..." }
"algo": "A256GCM",
"k": "...",
"iv": "...",
"t": "..."
}
} }
] ]
} }
``` ```
### Public note with attach + imeta (unencrypted image) ### MLS attachment metadata (ciphertext at URL; key/nonce via exporter)
```json ```json
{ {
"kind": 1, "url": "https://cdn.example/enc/xyz",
"content": "beach pic https://cdn.example/a.jpg", "ct": "video/mp4",
"tags": [ "size": 8329001,
["attach","https://cdn.example/a.jpg","sha256=2f3a...","m=image/jpeg","size=24567","fn=beach.jpg","alt=beach at dusk","dim=1920x1080","blurhash=..."], "sha256": "2f3a...",
["imeta","url https://cdn.example/a.jpg","m image/jpeg","x 2f3a...","dim 1920x1080","alt beach at dusk","blurhash ..."] "fn": "talk.mp4",
] "enc": {
} "mode": "mls",
``` "algo": "A256GCM",
"t": "....",
### Public note with eattach + imeta (encrypted video; key via DM) "mls": { "group_id": "deadbeef", "epoch": 42, "ctx": "blob:e3b0c442..." }
}
```json
{
"kind": 1,
"content": "members-only video (see your DM for the key) https://cdn.example/enc/v1/xyz",
"tags": [
["eattach","https://cdn.example/enc/v1/xyz","sha256=55aa...","m=video/mp4","size=8329001","fn=talk.mp4","algo=A256GCM","ekref=event:9b3e..."],
["imeta","url https://cdn.example/enc/v1/xyz","m video/mp4","x 55aa...","alt members-only talk"]
]
} }
``` ```
## Implementation Status ## Implementation Status
- Reference client implementation: **loxation-sw** - Reference client: **loxationsw**
- Supports per-attachment AES-256-GCM, presigned upload/finalize, and UI rendering. - Supports perattachment AES256GCM, presigned upload/finalize, NIP17 DM JSON payloads, and UI rendering.
- MLS exporterbased attachments are supported in the MLS messaging flows used by Loxation.