{"id":"6326e58f78925daaf8d41b98aac21942809c60cd7b22ee86a3b7f84821e0d4f2","pubkey":"e8cc4bdfb6a8bf7913acd3e14634b7edc2b052577f9f23cde433356cf1948db4","created_at":1759070799,"kind":30817,"tags":[["d","Time-Lock Encrypted Messages (Time Capsules)"],["title","Time Capsule"],["client","nostrhub.io"]],"content":"# **NIP-XX — Time-Lock Encrypted Messages (Time Capsules)**\n\n`draft` `optional`\n\nThis NIP defines **time capsules**: Nostr events whose plaintext becomes readable **at/after a target time** using a drand time-lock (tlock). Capsules can be broadcast publicly or delivered privately with [**NIP-59**](https://github.com/nostr-protocol/nips/blob/master/59.md) gift wrapping; encryption for sealing/wrapping uses [**NIP-44 v2**](https://github.com/nostr-protocol/nips/blob/master/44.md).\n\n> Encoding note: All Base64 in this NIP is RFC 4648 padded and MUST NOT contain line breaks.\n>\n> **Hex note:** All hex strings in this NIP are **lowercase**.\n\n---\n\n## Event kinds\n\n- **1041** — Time Capsule.\n\n---\n\n## Time capsule (kind: 1041)\n\nA public capsule is a signed `kind:1041` event. Its `content` is a **Base64 of the binary (non-armored) age v1 ciphertext** with **exactly one `tlock` recipient stanza** (no other recipient types).\n\n```json\n{\n  \"id\": \"<32-byte lowercase hex sha256 of serialized event>\",\n  \"pubkey\": \"<32-byte lowercase hex pubkey of the author>\",\n  \"created_at\": \"<unix timestamp in seconds>\",\n  \"kind\": 1041,\n  \"tags\": [\n    [\"tlock\", \"<drand_chain_hex64>\", \"<drand_round_uint>\"],\n    [\"alt\", \"<description>\"]\n  ],\n  \"content\": \"<base64(binary age v1 tlock ciphertext)>\",\n  \"sig\": \"<64-byte lowercase hex signature of the event hash>\"\n}\n```\n\n**Rules (public 1041):**\n\n- Exactly **one** `tlock` tag (see below).\n- `content` **MUST** be Base64 of **binary** age v1 with a **single `tlock` recipient stanza** and **no other** recipient stanzas (e.g., **no** `X25519`, `scrypt`). ASCII-armored age is **invalid**.\n- **clients** enforce unlock by verifying drand beacons; relays **do not** enforce time.\n\n---\n\n## Tags\n\n### `tlock` (required on 1041)\n\n**Single, preferred format (normative):**\n\n```json\n[\"tlock\", \"<drand_chain_hex64>\", \"<drand_round_uint>\"]\n```\n\n**Validation:**\n\n- `drand_chain_hex64` matches `^[0-9a-f]{64}$` (lowercase).\n- `drand_round_uint` matches `^[1-9][0-9]{0,18}$` (positive, 64-bit safe).\n- The age ciphertext **MUST** contain **exactly one** recipient stanza of type `tlock` whose **chain and round equal** the tag values; any mismatch **MUST** be rejected.\n\n### `p` (routing) — **only valid on outer 1059**\n\n- The **inner** `kind:1041` **MUST NOT** contain `p` tags. Clients **MUST** reject any private capsule whose inner 1041 includes a `p` tag.\n- On the **outer** `kind:1059` (gift wrap), include at least one `[\"p\",\"<recipient-npub>\",\"<relay_url?>\"]` per recipient for routing.\n\n### `alt` (optional on 1041)\n\n- Human-readable description for UX.\n\n---\n\n## Private capsule (sealed & wrapped per NIP-59)\n\nA private capsule is delivered via the NIP-59 pipeline:\n\n1. **Create the rumor (kind:1041, unsigned).**\n\n   Same schema as public, but **do not sign**. `content` is Base64(binary age v1 `tlock` ciphertext) and the `tlock` tag is present. **Omit `p`**.\n\n   **Rumor MUST NOT include `sig`.** **`id` MAY be present**; if present, clients **MUST** recompute it after recovery and reject on mismatch.\n\n2. **Seal (kind:13).**\n\n   JSON-serialize the rumor and encrypt it to the **recipient** using **NIP-44 v2**; put the ciphertext in `.content`. **`tags` MUST be `[]`**. **Sign with the author’s real key.**\n\n3. **Gift wrap (kind:1059).**\n\n   JSON-serialize the **seal** and encrypt it to the **recipient** using **NIP-44 v2** with a **one-time ephemeral** key; put the ciphertext in `.content`. Add at least one `[\"p\",\"<recipient>\",\"<relay_url?>\"]` (one 1059 per recipient is best practice). **Sign with the ephemeral key.**\n\n   Broadcast only to the recipient’s **DM relays** as advertised by their relay list metadata (per the relevant NIP).\n\n### Minimal examples (structure only)\n\n**Rumor (kind:1041, unsigned):**\n\n```json\n{\n  \"id\": \"<32-byte lowercase hex sha256 of serialized event>\",\n  \"pubkey\": \"<author pubkey hex32>\",\n  \"created_at\": 1234567890,\n  \"kind\": 1041,\n  \"tags\": [\n    [\"tlock\", \"<drand_chain_hex64>\", \"<drand_round_uint>\"],\n    [\"alt\", \"<description>\"]\n  ],\n  \"content\": \"<base64(binary age v1 tlock ciphertext)>\"\n}\n```\n\n**Seal (kind:13, signed by author; `tags = []`):**\n\n```json\n{\n  \"id\": \"<32-byte lowercase hex sha256 of serialized event>\",\n  \"pubkey\": \"<author pubkey hex32>\",\n  \"created_at\": 1234567890,\n  \"kind\": 13,\n  \"tags\": [],\n  \"content\": \"<NIP-44 v2 ciphertext of JSON(rumor kind:1041)>\",\n  \"sig\": \"<author signature hex64>\"\n}\n```\n\n**Gift wrap (kind:1059, signed by ephemeral; includes `p`):**\n\n```json\n{\n  \"id\": \"<32-byte lowercase hex sha256 of serialized event>\",\n  \"pubkey\": \"<ephemeral pubkey hex32>\",\n  \"created_at\": 1234567890,\n  \"kind\": 1059,\n  \"tags\": [[\"p\", \"<recipient npub>\", \"<relay_url>\"]],\n  \"content\": \"<NIP-44 v2 ciphertext of JSON(seal kind:13)>\",\n  \"sig\": \"<ephemeral signature hex64>\"\n}\n```\n\n---\n\n## Decryption & validation (client-side)\n\n### Public 1041\n\n1. Verify **NIP-01** signature; check **exactly one** `tlock` tag; Base64-decode `content`.\n2. Fetch the drand beacon for `drand_round_uint` and **verify** it against the chain’s BLS public key derived from `drand_chain_hex64`.\n3. Parse the **binary** age v1 ciphertext; ensure **exactly one** recipient stanza of type `tlock` whose chain/round **match the tag**; reject ASCII armor or extra recipient types.\n4. Decrypt with the verified beacon; the result is the plaintext.\n\n### Private (1059 → 13 → 1041)\n\n1. Validate outer **1059** (ephemeral **NIP-01** signature); **NIP-44 v2** decrypt `.content` with your key.\n2. Parse inner **kind:13**; **`tags` MUST be empty**; verify **author** signature; **NIP-44 v2** decrypt `.content` using the author↔recipient conversation key.\n3. Parse recovered **unsigned kind:1041 rumor**. **Verify** `lower(seal.pubkey) == lower(rumor.pubkey)` (both 32-byte lowercase hex). If `rumor.id` is present, **recompute** and reject on mismatch. For display and ordering, **use `rumor.created_at`**; the `created_at` of the seal and wrap are transport metadata and **MUST NOT** replace the rumor’s timestamp in UX.\n4. Fetch & verify drand beacon as above; ensure `tlock` tag ↔ age stanza chain/round match; then age-decrypt to recover the plaintext.\n\n---\n\n## Relay semantics\n\n- Relays **MUST NOT** attempt to decrypt or enforce unlock times.\n- Clients **MUST** enforce unlock using **verified** drand beacons, **not** local clocks.\n\n---\n\n## Security considerations\n\n- **Beacon verification:** Always verify drand beacons against the chain’s BLS public key (derived from `drand_chain_hex64`) before age decryption. Do **not** trust local time or unsigned beacons; accept the first BLS-verified beacon from any endpoint.\n- **Ciphertext format:** Accept **only** binary age v1 `tlock` with **exactly one** recipient stanza; **reject** ASCII-armored inputs and stanza multiplicity or other stanza types.\n- **Bounds & DoS:** Before allocation, clients **SHOULD** enforce `tlock_blob ≤ 4096 bytes` and **SHOULD** reject 1041 whose **decoded** `content` exceeds **64 KiB**. Relays **MAY** drop 1041 exceeding **256 KiB** decoded.\n- **Sealing/wrapping crypto:** Use **NIP-44 v2** (ECDH → HKDF, ChaCha20, HMAC, padded Base64). Validate MAC in constant time **before** attempting decryption.\n- **Timestamps & privacy:** Randomize seal/wrap `created_at` slightly (e.g., jitter/backdate) for metadata privacy; the rumor’s `created_at` is canonical for UX.\n\n---\n\n## Implementations\n\n- **Relay** [**Shugur Relay**](<https://github.com/Shugur-Network/relay>)\n- **Client** [**Shugur Time Capsules**](<https://capsules.shugur.com>)","sig":"1e2bdbfb8fe9d01cd966bc16ecca5f16a69a0e7c855a3eac16ba3ccd9888d504e62dca03a7b55efb41230a07ad454601fc661a5aac42c484670683a5463ab422"}