Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NIP-87 private groups #706

Closed
wants to merge 8 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
350 changes: 350 additions & 0 deletions 87.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
NIP-87
=======

Encrypted Groups
----------------

`draft` `optional` `author:staab` `author:earonesty` `author:vitorpamplona` `author:water783`

Large groups are a distinct use case from small groups, which can be implemented using duplicate direct messages to all participants with cc tags. In order for a group to scale, keys must be shared. However, shared keys increases the risk of key leakage and member doxxing. Additionally, large groups require moderation, which small groups do not.

This NIP introduces several new event kinds, and a two-tier shared key system which results in the following benefits:

- No admin, moderator, or member metadata leakage outside the group
- Optionally public group metadata
- Users may request admission or removal
- Members may select whitelist, blacklist, or no moderation
- Members and moderators may request the removal of another member
- Shared keys can be rotated with weak forward secrecy
- Use of wrapping allows any nostr event to be posted to the group

This NIP relies on terms defined in [59.md](NIP 59) for wrapping.

# Wrapping Notation

To make descriptions of wrapped events clearer below when referring to a gift wrapped event the rumor kind, the author of the seal, the author of the gift wrap, and the recipients are specified using this notation:

`kind:author wrapper->[recipient1,recipient2]`

- If using an ephemeral key, the symbol `?` should be used.
- If using a derived key, the notation `{a,b}` should be used.
- If using an unknown kind, use `any`.
- If no recipients are specified, use `[]`.

For example, if a kind 1 is sent by `a` to a key derived from `a`'s private key and `b`'s public key and wrapped using an ephemeral key:

`1:a ?->[{a,b}]`

# Protocol Description

## Admin key

Groups are bootstrapped using a dedicated pubkey called the "admin key", and identified by the address of the admin's replaceable 10024 event (regardless of whether it has been published).

This key is used for performing privileged actions, such as publishing group metadata and key rotations. Non-administrative events MAY be published to the group by the admin.

## Shared key

Distinct from the admin key, a shared key is generated and shared with group members. Posession of a shared key constitutes membership in the group. The shared key allows group members to post to the group and decrypt group messages.

When posting to the group, events MUST follow this scheme: `any:author shared_key->[shared_key]`.

New members are invited using this scheme: `24:admin_key ?->[recipient]`. Multiple events are sent, each addressed to a single invitee directly. The rumor's `tags` MUST contain a single `shared_key` tag containing the shared key and SHOULD contain one or more `r` tags to help clients find group messages. It MAY also include any number of `p` tags to help clients build member lists.

```json
{
"kind": 24,
"content": "Just a regular key rotation, nothing to worry about, we definitely didn't leak a key",
"tags": [
["shared_key", "<new shared key>"],
["r", "wss://some-relay.com"],
["p", "<pubkey>"],
["p", "<pubkey>"]
],
}
```

## Key Rotation

To rotate the shared key, the admin must publish a new `24:admin_key ?->[recipient]` to each group member as described above. A `grace_period` tag MAY be included on the inner event indicating how many seconds after the new event's `created_at` timestamp previous keys are valid. This invalidates previous keys and replaces them with a new shared key.

An `expiration` tag (defined by NIP-40) MAY be included on the wrapper to support a weak form of forward secrecy (weak, since it relies on relays to delete the event). This can reduce the impact of a member's private key being leaked, which could otherwise expose old shared keys and messages addressed to those keys.

If new members are added when a key rotation occurs, the group admin SHOULD re-publish any private `10024` or `10025` events using the new shared key so that new members have access to metadata for the group.

## Access Requests

Anyone may request access to a group using a `25:author ?->[admin_key]`. The rumor's `content` MAY include a message.

```json
{
"kind": 25,
"content": "Pleeease let me in",
"tags": [],
}
```

If the admin chooses to admit the new member, clients may do one of the following:

- Inititate a new key rotation in order to prevent the new member from seeing any group history.
- Re-send the most recent `kind 24` event to the new member, granting access to all messages since the most recent key rotation.

## Key Rotation Request

Any group member may request a key rotation: `26:author shared_key->[shared_key]`. The rumor's `content` MAY include a reason for the request, and `tags` MAY include any number of `p` tags to exclude from the key rotation. These may or may not be honored by the admin, depending on whether the author's own `p` tag is included (leaving a group), or if the author is a moderator.

```json
{
"kind": 26,
"content": "I'm outta here",
"tags": [["p", "<pubkey>"]],
}
```

This event is sent to the group rather than the admin so that group members (including the sender) can optimistically update their member lists. This SHOULD be limited only to removing the author's pubkey from the group.

## Members Added Notifications

When a key rotation is performed, the admin MAY publish a `27:admin_key shared_key->[shared_key]` with a `p` tag for each member in the group. The event MUST be addressed to the **new** shared key. This helps people build member lists and know what groups they are a part of. If the member list is very large, multiple events MAY be published with the same key, each with a subset of `p` tags.

```json
{
"kind": 27,
"content": "Welcome, Gary!",
"tags": [["p", "<pubkey>"], ["p", "<pubkey>"]],
}
```

## Members Removed Notifications

When a key rotation is performed, the admin MAY publish a `28:admin_key shared_key->[shared_key]` with a `p` tag for each member being removed from the group. The rumor MUST be addressed to the **now-invalid** shared key. This helps people build member lists and know what groups they are a part of. If the list of members removed is very large, multiple events MAY be published with the same key, each with a subset of `p` tags.

```json
{
"kind": 28,
"content": "Shame, shame",
"tags": [["p", "<pubkey>"]],
}
```

## Messages

Any group member may post a wrapped event to the group sealed by their own key, signed by the shared key, and addressed to the shared key: `any:author shared_key->[shared_key]`. Anyone with the shared key may decrypt these messages. The set of these messages constitutes an unmoderated version of the group.

## Opinions

Anyone may post a `1985:author shared_key->[shared_key]` event using the `nip87` namespace and either an `accept` or `reject` label. This indicates approval or disapproval of a message as a valid post to the group. An `e` tag referencing the **innermost** event (`rumor`) is required. `content` SHOULD include an explanation. `tags` MAY include other more specific labels as well.

```json
{
"kind": 1985,
"content": "Off topic, this group is about marlon brando",
"tags": [["e", "<rumor id>"], ["L", "nip87"], ["l", "reject", "nip87"]]
}
```

This scheme allows group members to choose how posts are moderated for them:

1. They can view all posts
2. They can view all posts except ones with a negative opinion
3. They can view only posts with a positive opinion

Members can also choose whose opinions to subscribe to, for example people they follow or group moderators.

## Group Metadata

Group metadata may be published using a `kind 10024` event. `content` MUST be a json object defining the attributes of the group. `name`, `about`, and `picture` are recommended. `tags` SHOULD include one or more `r` tags with recommended relays. This event may be published as a regular nostr event signed by the admin key, or it may be published privately to the group as `10024:admin_key shared_key->[shared_key]`, depending on if the admin wants the group information to be public.

```json
{
"kind": 10024,
"content": "{\"name\": \"Low-cal Calzone Zone\"}",
"tags": [["r", "wss://my-trustworthy-relay.com"]],
}
```

Whether a `kind 10024` exists or not, the corresponding event address is the canonical way to refer to the group for sharing purposes, rather than pubkey alone. This is to avoid confusion with user pubkeys.

## Moderator lists

An admin MAY publish a `kind 10025` "moderators" event. `content` MAY include a message, and `tags` MUST contain one `p` tag for each moderator. This MAY be wrapped using `10025:admin_key shared_key->[shared_key]` to create a private moderator list.

```json
{
"kind": 10025,
"content": "Please welcome jim to the moderation team!",
"tags": [["p", "<pubkey1>"], ["p", "<pubkey2>"]]
}
```

Members MAY use moderator lists to inform what `kind 1985` opinions to follow. Admins may use moderator lists to help decide what pubkeys in a `kind 26` should be removed from the group.

# Other Notes

## Anonymous membership

To post to a group, you must reveal your public key, doxxing yourself to all other members of a group, all of whom are able in turn to reveal your identity outside the group. To avoid this, users may join groups anonymously by generating a new keypair separate from their primary key. They may then publish an alternative kind0 to the group to maintain a different identity. Clients should help users to manage their alternative key using an appropriate strategy.

## Always-on admin key

There is no need for an admin to be always online, since key rotation requests do not expire. However, for larger groups timely key rotation might be desired. Because an admin key is distinct from that of the group owner(s), it would be easy to create an always-on service that can automate key rotations.

## Admin key vulnerabilities

Since ultimate control of the group lies with the holder of the admin key, the security of this key is paramount. Leakage of this key would result in complete compromise of the group, as well as the doxxing of all members who have posted to the group. Management of this key is up to the group admin, but should be taken seriously.

An ideal solution would be to use an air-gapped signing mechanism to publish events, viable since the group admin need not publish often except to rotate shared keys. Also viable would be to manage key access using an nsec bunker.

## Shared key vulnerabilities

Any member of the group can implicitly invite new members to the group, since they have the private key (although this would have to happen out-of-band, since the private key is stored in a `rumor`). Any member of a group can dox other members by publishing their wrapped messages. Any member of the group can spam the group, or otherwise DOS the group.

If any single member leaks the shared secret, all messages can then be decrypted by others until the next key rotation. The use of optional frequent forward secrecy rotation events can mitigate these attacks, provided the server is compliant with the expiration times.

## REQ filter support

Because all group events are posted using wrapped events, regular REQs filters can't be used to filter events. In most cases, filtering by `created_at` should be enough to create a workable dataset that can be filtered client-side, but this can be a problem for very large groups.

In situations where REQ support is desired, special purpose relays may be recommended by the group metadata event whose pubkeys are included as members of the group. This would allow these relays to unwrap events posted to the group, index them, and serve the original wrapped version in response to REQs.

## Replaceable events

Kind `10024` and `10025` are replaceable events, but are wrapped and so can't be de-duplicated by relays. Clients SHOULD perform this deduplication manually, keeping on the most recent version.

# Example

Alice decides to create a group called the "Low-cal Calzone Zone" where she and her friends play Cones of Dunshire.

Keys in play:

- Admin key `e9cdfbfbb053312a968546d0c7cfc97864e709e2c913cec9b998cfe96213f5f3`
- Alice `6f510cde1efc79320a47477f9ce95434744c540f47376c48d872eb8ea20904d0`
- Bob `9556b15db87540a67e40aad3c2b187b366b965d5a6900720c9e7c9007af4cd6b`
- Charlie `0b82fc3012a3d6a950396eebf111ff8d0a60b7945afa3c8568df50d8c1cfb403`
- Shared key #1 `7405d6717625f7ad54d9cc7a77191e6d217719881c16897a5c1fadf52f638b73`
- Shared key #2 `61a8edddd8a6ef60d73b64c4da8e7eb931b38399ed3ea79818fa77233ab6f3d7`

## Invitations

To start with, Alice only wants to invite herself and Bob to the group. She creates a `kind 24` invitation with shared key #1 as the `privkey`.

Rumor:

```json
{
"kind": 24,
"created_at": 1686840217,
"pubkey": "4995ddb14eae1b11ee2aea8384646be4f98c6b72787b4c1841dd35375d2de5de",
"content": "Welcome, Bob!",
"tags": [["privkey", "7405d6717625f7ad54d9cc7a77191e6d217719881c16897a5c1fadf52f638b73"]]
}
```

Alice sends this event as the admin to herself:

```json

```

And to Bob:

```json

```

## Group metadata

Now, Alice can (optionally) advertise the group with a `kind 10024` event:

```json
{
"kind": 10024,
"created_at": 1686840217,
"pubkey": "4995ddb14eae1b11ee2aea8384646be4f98c6b72787b4c1841dd35375d2de5de",
"content": "{\"name\": \"Low-cal Calzone Zone\"}",
"tags": [["r", "wss://my-trustworthy-relay.com"]],
}
```

This can either be wrapped using the group's admin key and sent to all group members:

```json
```

Or signed using the group's admin key and broadcast normally.

## Posting to the group

Alice wants to send a message to the group about a game this weekend:

```json
{
"kind": 1,
"created_at": 1686840217,
"pubkey": "82100c3bec3f0674b59dd5f4f2cdab6f8b4bd936f138a7f0ee6bbde2e19e2ca4",
"content": "Want to play this weekend?",
"tags": [],
}
```

She wraps this using the shared key:

```json
```

## A new member

Charlie wants to join the group, so he sends an access request addressed to the admin key:

```json
{
"kind": 25,
"created_at": 1686840217,
"pubkey": "3c8acf67852bc44fcb193bd353b6062ab84dc95be2c11831d74a8d9c0299101d",
"content": "",
"tags": [],
}
```

Wrapped:

```json
```

Alice can then grant access by sending the same `kind 24` event as before to Charlie's pubkey.

## Voting on a post

Bob wants to make sure Charlie sees Alice's post, regardless of his moderation settings, so he posts an opinion to the group:

```json
{
"kind": 1985,
"content": "Just want to make sure everyone saw this",
"tags": [["e", "<kind 1 id>"], ["L", "nip87"], ["l", "accept", "nip87"]]
}
```

Wrapped:

```json
```

## Removing a member

Bob has gotten tired of Cones of Dunshire and wants out. He sends a `kind 26` to the group using the current shared key with his pubkey included as a `p` tag:

```json
{
"kind": 26,
"content": "I'm out guys, have fun",
"tags": [["p", "2fb048557ca34a671e40bf9fae8f82d5919c96bea5ecba4d1b5bedf5b28604ca"]]
}
```

Wrapped:

```json
```

Using the admin key, Alice issues a `kind 27` in response, as well as a new `kind 24` sent only to herself and Charlie.