epoch-based deterministic wrapper keys for NIP-17.

This commit is contained in:
fiatjaf
2026-06-27 11:20:07 -03:00
parent d675e3b6cb
commit 944e9d27a5

23
17.md
View File

@@ -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": "<signed by randomPrivateKey>"
"sig": "<signed by wrapperPrivateKey>"
}
```
`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.