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:
parent
befaa2882d
commit
494dd3e146
|
@ -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: NIP‑17 direct messages and MLS groups. It defines:
|
||||||
|
|
||||||
- Tag-based referencing for public events
|
- A normalized JSON structure for NIP‑17 DMs (keys live only inside the DM ciphertext)
|
||||||
- A JSON structure for private DMs that keeps encryption secrets private
|
- An MLS profile that derives per‑attachment AEAD key/nonce via the MLS exporter
|
||||||
- A per-attachment symmetric encryption scheme (AES-256-GCM) with normative sizes
|
- A per‑attachment AEAD scheme (AES‑256‑GCM) with normative sizes
|
||||||
- Integrity fields and basic metadata for rendering
|
- Integrity requirements (SHA‑256 over ciphertext)
|
||||||
- Optional alignment with existing NIPs (NIP-17, NIP-94, NIP-96, NIP-98)
|
- HTTP storage guidance (presigned upload/finalize), optionally compatible with NIP‑96 and NIP‑98
|
||||||
|
|
||||||
|
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, 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.
|
||||||
|
|
||||||
- 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 relay‑visible metadata.
|
||||||
- Unencrypted public attachments are simple URLs plus integrity metadata.
|
- Use per‑attachment 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 off‑relay 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.
|
- **Per‑attachment key**: A random 32‑byte AES‑256 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: AES‑256‑GCM
|
||||||
|
- 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. NIP‑17 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 DM’s 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 DM’s 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 NIP‑17 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 NIP‑17 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 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`).
|
||||||
|
|
||||||
#### 2.3 MLS group attachments
|
Notes:
|
||||||
|
- The `size` field SHOULD reflect ciphertext length.
|
||||||
|
- Clients SHOULD cache both ciphertext and decrypted plaintext for efficient re‑rendering.
|
||||||
|
|
||||||
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 relay‑visible 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 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:
|
||||||
|
|
||||||
```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 DM’s encrypted content (see 2.2). Do not place keys in public tags.
|
- NIP‑92 “imeta” and NIP‑94 file‑metadata records for public media.
|
||||||
- Rendering: Receivers decrypt the DM per NIP‑17, fetch bytes, verify sha256 over ciphertext, then decrypt with enc.k/iv/t.
|
This profile targets encrypted attachments delivered via NIP‑17 and MLS only.
|
||||||
|
|
||||||
#### 3.2 NIP-92 (Media Attachments) — Interoperability
|
### 5. Storage and transport
|
||||||
- Purpose: NIP‑92 defines “imeta” tags that annotate media URLs present in the event content. Many media‑focused clients render using imeta.
|
|
||||||
- Coexistence with NIP‑XXX:
|
|
||||||
- attach/eattach MAY be used alongside NIP‑92 imeta in the same event.
|
|
||||||
- When the event content contains a media URL, publishers SHOULD include a corresponding imeta tag so NIP‑92‑aware 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 NIP‑94)
|
|
||||||
- alt, dim, blurhash ↔ imeta: "alt …", "dim WxH", "blurhash …"
|
|
||||||
- NIP‑92 "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 NIP‑92 renderers.
|
|
||||||
- If a note does not inline the URL in content (tags‑only publishing), imeta is not applicable; attach/eattach alone is sufficient.
|
|
||||||
|
|
||||||
#### 3.3 NIP-94 (File Metadata)
|
- Storage: Off‑relay HTTP object storage with presigned upload + finalize flows that return canonical download URLs and server‑computed metadata (size/checksum). These flows are compatible with NIP‑96 where applicable.
|
||||||
- Publication guidance (normative):
|
- Auth: Publishers and storage providers MAY require NIP‑98 (HTTP Auth) for upload/finalize/download.
|
||||||
- Publishers MAY emit a NIP‑94 file‑metadata event for each public attachment (attach or eattach) using the same url/m/size/sha256. This provides durable, relay‑indexable metadata.
|
- Alternate device transports (e.g., BLE/Noise) MAY carry the same JSON payloads; this does not change the on‑wire format for Nostr DMs or MLS.
|
||||||
- Clients that publish a NIP‑94 event SHOULD reference it from the note (e.g., "e" or "a" tag per client policy).
|
|
||||||
- For encrypted attachments, NIP‑94 events MUST describe the ciphertext (url/m/size/sha256) and MUST NOT include decryption keys; keys live only in private channels (e.g., NIP‑17 DM).
|
|
||||||
- When both inline attach/eattach and a NIP‑94 record are present, clients SHOULD prefer inline fields for immediate render and also index the NIP‑94 record.
|
|
||||||
- Hash alignment:
|
|
||||||
- In attach/eattach, sha256=<hex> corresponds to the NIP‑94 "x" field value used by NIP‑92 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 NIP‑96. A NIP‑96‑compliant server can provide initiation, direct upload, and finalize endpoints returning canonical download URLs and server‑computed metadata.
|
|
||||||
|
|
||||||
#### 3.5 NIP-98 (HTTP Auth)
|
- Verify `sha256` over ciphertext before decrypt/render.
|
||||||
- Usage: Publishers and storage providers MAY require NIP‑98 for upload/finalize/download operations. Include an Authorization header per NIP‑98; 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 safe‑content 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 NIP‑92 `imeta` or any relay‑visible metadata.
|
||||||
- Cache ciphertext and decrypted plaintext
|
- Treat URL‑based key delivery or external key references as non‑confidential; this profile forbids such patterns.
|
||||||
- Respect accessibility (`alt`)
|
- Ensure unique IVs per key; per‑attachment 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 NIP‑92 imeta or any public content.
|
|
||||||
- Clients MUST verify sha256 and GCM auth tag before render/decrypt.
|
|
||||||
- Clients MUST NOT auto‑resolve ekref via untrusted schemes; keys SHOULD be conveyed via private DMs (NIP‑17) or other trusted mechanisms.
|
|
||||||
- Apply quotas/content scanning on downloads.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Public note with two attachments
|
### NIP‑17 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: **loxation‑sw**
|
||||||
- Supports per-attachment AES-256-GCM, presigned upload/finalize, and UI rendering.
|
- Supports per‑attachment AES‑256‑GCM, presigned upload/finalize, NIP‑17 DM JSON payloads, and UI rendering.
|
||||||
|
- MLS exporter‑based attachments are supported in the MLS messaging flows used by Loxation.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue