From 944e9d27a586a4635d6161567ee8c9769f9d22b1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 27 Jun 2026 11:20:07 -0300 Subject: [PATCH] epoch-based deterministic wrapper keys for NIP-17. --- 17.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/17.md b/17.md index 23fde73a4c..dc31a306b6 100644 --- a/17.md +++ b/17.md @@ -18,6 +18,25 @@ The set of `pubkey` + `p` tags defines a chat room. If a new `p` tag is added or An optional `subject` tag defines the current name/topic of the conversation. Any member can change the topic by simply submitting a new `subject` to an existing `pubkey` + `p` tags room. There is no need to send `subject` in every message. The newest `subject` in the chat room is the subject of the conversation. +## Conversation Wrapper Profile + +To allow receivers to query chat messages from relays without decrypting each event, the gift wrap is signed by a deterministic keypair called a "conversation wrapper profile". Both parties can derive this independently using the same HKDF function they use for [NIP-44](44.md), but with a different salt: + +``` +shared_x = secp256k1_ecdh(private_key_a, public_key_b) +wrapper_private_key = hkdf_extract( + IKM = shared_x, + salt = utf8_encode('nip17-w-' + epoch + '-' + counter) +) +``` + +- `epoch = math.floor(now() / 1_000_000).string()` (~12 days per epoch). The `shared_x` is the same ECDH shared secret used in [NIP-44](44.md) conversation key derivation. +- `counter` is just the string `"1"` for the vast majority of situations (such that it can almost be safely hardcoded to `"1"`), but in the very rare case in which it doesn't yield a valid secret key, both parties will increment it to `"2"` and so on until it does. + +The wrapper public key is the `pubkey` of the gift wrap event, which means a specific conversation can be queried directly with `{authors: [wrapper_pubkey]}`. + +Since the key changes each epoch, it provides some level of privacy against an observer in case these gift-wraps leak, as the observer won't be able to link conversations across epochs. + ## Encrypting Following [NIP-59](59.md), the **unsigned** chat messages must be sealed (`kind:13`) and then gift-wrapped (`kind:1059`) to each receiver and the sender individually. @@ -46,13 +65,13 @@ Following [NIP-59](59.md), the **unsigned** chat messages must be sealed (`kind: }, nip44.compute_conversation_key(wrapperPrivateKey, receiverPublicKey) ), - "sig": "" + "sig": "" } ``` `unsignedMessageRumor` is a rumor (an unsigned event, as per [NIP-59](59.md)), usually a `kind:14`, but could also be a different kind, see [Message Rumor Definitions](#message-rumor-definitions) below. -`wrapperPrivateKey` and `wrappedPublicKey` are a new keypair, generated randomly anew for each message sent. +`wrapperPrivateKey` and `wrapperPublicKey` form the conversation wrapper profile keypair, derived deterministically from the shared secret as described in the [Conversation Wrapper Profile](#conversation-wrapper-profile) section. Clients MUST verify if pubkey of the `kind:13` is the same pubkey as that of the `unsignedMessageRumor`, otherwise any sender can impersonate any other by simply changing the pubkey on the rumor.