nips/68.md
2024-05-09 15:58:49 -04:00

4.4 KiB

NIP-68

Shared Replaceables

draft optional

This NIP creates replaceable events that can be changed by any public key in the list of editors. Editors can also add and remove new editors.

Every shared replaceable event MUST be signed with it's own private key. The event owns itself.

The event's private key MUST be shared with all editors through p tags. The key is NIP-44-encrypted to each editor and placed as the 4th element in a regular p tag.

val edittingKeyPair = nostr.generateKeyPair()

{
  "pubkey": edittingKeyPair.publicKey
  "kind": 3xxxx or 1xxxx,
  "tags": [
    ["d", "<unique identifier>"]
    ["p", "<pubkey 1>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 1>") ]
    ["p", "<pubkey 2>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 2>") ]
  ],
  "content": "",
  "sig": signWith(edittingKeyPair.privateKey)
  // ...
}

Any replaceable event kind can be shared among editors.

To update the event, receivers MUST:

  1. find the ciphertext in the p-tag for their key
  2. decrypt the ciphertext with nip44Decrypt(tag[3], user.privatekey, event.pubkey) to get the event's private key in hex.
  3. use the event's private key to sign.

Encrypted Shared Replaceables

Some use cases require separate editting and viewing permissions: the .content can be encrypted so that only users with viewing permissions can see the information.

To achieve this dynamic, the replaceable event MUST own two shared private keys: one for editting and one for viewing.

Both keys are shared as encrypted p tags between the editting key and each user's public key.

The .content is then encrypted from the editing private key to the viewing public key.

val edittingKeyPair = nostr.generateKeyPair()
val viewingKeyPair = nostr.generateKeyPair()

{
  "pubkey": edittingKeyPair.publicKey
  "kind": 3xxxx or 1xxxx,
  "tags": [
    ["d", "<unique identifier>"]
    ["p", "<pubkey 1>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 1>") ]
    ["p", "<pubkey 2>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 2>") ]
    ["p", "<pubkey 3>", "<relay url>", nip44Encrypt(viewingKeyPair.privateKeyHex,  edittingKeyPair.privateKey, "<pubkey 3>") ] // view only
  ],
  "content": nip44Encrypt("some text", edittingKeyPair.privateKey, viewingKeyPair.publicKey),
  "sig": signWith(edittingKeyPair.privateKey)
  // ...
}

To decrypt the event, all receivers MUST:

  1. find the ciphertext in the p-tag for their key
  2. decrypt the ciphertext with nip44Decrypt(tag[3], user.privatekey, event.pubkey) to get the event's private key in hex.
  3. calculate the public key of the shared key.
  4. if the public key is the same as .pubkey, this is an editing key, if not this is the viewing key
  5. if it is the editing key, decrypt all the other p-tag keys and find the viewing key
  6. once both keys are known, decrypt the .content with nip44Decrypt(event.content, viewingKeyPair.privatekey, event.pubkey)

Special Case: No Viewing Keys

If the group if users that only have viewing permissions is empty there won't be a p-tag to host the viewing key. In those cases, the .content MUST then be encrypted to the editing public key.

val edittingKeyPair = nostr.generateKeyPair()

{
  "pubkey": edittingKeyPair.publicKey
  "kind": 3xxxx or 1xxxx,
  "tags": [
    ["d", "<unique identifier>"]
    ["p", "<pubkey 1>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 1>") ]
    ["p", "<pubkey 2>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 2>") ]
  ],
  "content": nip44Encrypt("some text", edittingKeyPair.privateKey, edittingKeyPair.publicKey),
  "sig": signWith(edittingKeyPair.privateKey)
  // ...
}

Similarly, when decrypting the .content, if the receiver client can't find a viewing key, it SHOULD use the editing key to decrypt: nip44Decrypt(event.content, edittingKeyPair.privateKey, edittingKeyPair.publcKey)

Final Considerations

If any of the event's private keys are lost due to an encrypting bug or if there is a failure to add the ciphertext in the p-tags before signing, and if relays don't have previous versions of this event, the event might become permanentely unmodifiable and undecryptable, which can also be a feature in some use cases.