-
Notifications
You must be signed in to change notification settings - Fork 583
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
NIP-38 - Encrypted Group Chat using a single shared secret #59
Conversation
.. with a single shared secret. Modeled on public channels NIP 28.
38.md
Outdated
|
||
Further, the creator of the channel should send a Direct Message to each of the other participants, which will have the channel's shared-secret private/public keys. The Direct message should be sent by a kind other than 4; but presently kind 4 direct messages are used. | ||
|
||
The format of that message is: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we really shouldn't do this over kind 4. this is really hacky. ideally it would be a protocol message that clients can handle without parsing, display an invite properly, etc. Users shouldn't have to see a shared secret at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with this that there is no good reason to use kind 4 here.
Invite suggestions on what kind it should be. I think it would be good to have one special kind where different apps can store or send their information from where they can pick it up. So that user can know what app is sending what message.
Edit: Kind 104 would be a good number. Its 100 away from 4, and no one has yet used such messages.
Also then the format of the message, or its 'header' or first few words etc, should help differentiate between different apps.
Currently I have used:
App Encrypted Channels: inviting you to encrypted channel <channel id> encrypted using private public keys <shared private key> <corresponding shared public key>
So it envisages a format like
"App" <app name>: <app specific information>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edited the proposed NIP 38 and mentioned kind 104 rather than kind 4 to communicate the shared-secret.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is not much parsing involved for shared-secret message. Once the app matches the first few words, which are "App Encrypted Channels: ", then the required information is at well-known points in the message. If strInvite is the given message as a string then the required values are always at the same place:
Encrypted Channel id : strInvite.substring(58, 58 + 64)
Secret private key : strInvite.substring(159, 159 + 64);
Secret public key : strInvite:substring(224, 224 + 64);
And the length of strInvite should be 288 chars.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now think the above message should ideally be in json format. will work on it further in that direction.
Unless you have some other idea there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for public key, it can be derived
i'll throw up a nip for "more secret" dm's
|
||
Note: Clients for a user may create a channel where the only participant is the creator of the channel. Then new members can be added by sending a kind 141 message, as discussed further. | ||
|
||
## Kind 141: Update metadata and Add participants |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something me and @Semisol talked about awhile back was creating a new shared secret whenever a participant is removed, and this is encrypted into tags for each participant.
The client would then query all the update messages and add all of the distinct keys to their decryption key set. Then for older messages it would try to decrypt with each key in the set until a successful decryption.
This would enable you to re-key channels once participants are removed with a pretty simple algo. Also you don't need to send invites in this case either, the key is automatically encrypted to the user in the channel creation/update tags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be a good option to create a new shared secret when a new user is added, maybe someone want to add user for the future messages but dont want to allow them to read the previous ones
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing of participant ( or cases where a new secret is needed for all ) can be catered to by this method, where we allow the creator (or anyone for that matter) to "fork" a group, so that the new fork will have a new id, new secret. We mark the new group as a 'fork' by adding some specific tag info.
Then internally in clients, user can be seamlessly shown messages from what are two channels ( if you go by channel id itself), but are logically the same cause the client knows which channels are connected logically as same.
This is the simplest way I think we can implement removal: it also has the logical consistency ( which will make coding easier imo), that each channel id is only ever connected with one shared secret. But using extra tags, we can establish a logical connection ( that 2+ channels are same channel as far as user knows).
This even allows some member to fork off a new group; people may want to do it. Lets say admin is absent, so they move to a new 'channel id' , but logically clients will/may show them just one group. Plus, if they want, they can continue using the same shared secret; there is no need for anyone to enforce that different shared secret has to be used; but good clients will obviously use different shared secret on removal of a member(s).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
im building this now, based on the "considered harmful discussions" with @paulmillr mostly as a POC. using the new "versioned encryption" system, ephemeral public invitations and well-known group public/private key, like in this document. working fine so far. you can play with a test flight version soon.
https://github.com/ArcadeLabsInc/arclib/pull/20/files
A related note. We may need to have in total three types of "channels": a) totally public kind 4x channels Edit: On second thought, this needs revisiting. Cause membership is possible even in channel 40 with an extension of adding p tags as members. |
The state for this shouldn't use replaceable events: you want to see who the historical members were to get their events. Another issue: backdating |
IME, encrypted group chat is a rabbit-hole of complexity. A lot of folks use double ratcheting as a standard. All the requirements are found in WebCrypto: I have seen this method scale (and fail) gracefully on rooms with 100+ members. So maybe that is a good option. Otherwise, I would suggest a protocol that uses forward secrecy by default, so that updating the shared secret is easy. A chat room has to broadcast frequently, so updating the secret is no big deal. Everyone has to agree on the state of channel members though. I also think that if there are plans to use group encryption and membership, you may as well add some role / admin features as well. Matrix uses a concept of "power levels" that works really well. Anyway, my 2 sats on the discussion. :-D |
Thanks for writing this NIP. I think it can be used as an alternative to #49 for joinstr. I have not tested it yet in nostr console. Will do it today. |
I think this may be a superior alternative, since it would be fully stateless like NIP-04: #72 (comment) |
Can you explain the stateless part in detail or what are the advantages of this method over the one shared in this PR?
This might not be perfect for all use cases however it should work for some. As long as shared secret and messages are private it will work for 2 of my projects: joinstr and p2p exchange Not sure if any other client and library has already implemented it apart from nostr-console.
This should not be a concern if clients implement it correctly and create a new group chat once someone leaves with the rest of the members. |
Each event contains its own decryption keys, just like NIP-04, that is what I called "stateless". |
The three secrets that Alice generates in that scheme you mentioned @fiatjaf , I can't tell what happens to them, in that where do they go after steps 4 and 5, as they are not mentioned after 5? I am having trouble understanding what is happening here. So in step 4 Alice generates three secrets, one each for 3 receivers. In step 5 she uses them to encrypt x. Then Later are those secrets never needed by Alice or any other people? Since x was encrypted with those secrets, wont' they be needed again to decrypt x? And if they will be needed, by whom are they needed? Are they given to respective recipients? |
What about a replaceable event that simply provides each member with the latest shared secret for a channel, encrypted to their pubkey? If you are a member on the list, then there will be a shared secret available for you to decrypt. Otherwise, you get nothing. Whenever the channel owner wants to update the member list, they simply update the event with a new shared secret, and update the encrypted keys for each member. Members can subscribe to the replaceable event to get the latest key. You could also throw other channel metadata in there as well, just use the whole content field as a metadata store for the channel. |
Pretty much this is what is being done in this NIP 38, where the single shared secret is being shared using kind 104, which is encrypted to the recipeients respective pubkey. So member m1 gets from creator C a kind 104 which is like a DM, which mentions the shared secret. Now whether that's replaceable or not is up for discussion.
Updation is propsed differently - the NIP 38 proposes that a single channel is always only associated with a single secret. To remove members, for example, a new channel is created with new secret. This keeps the logic simple , as in one channel, one secret, always.
I am thinking of having a json for kind 104, with details about group also thrown in, if that's what you mean too. |
Can you elaborate on this more? Are you encrypting the private key using the public key and ECDH? |
Is it something like this: Both Parties// Generate keypair for channel owner and channel member.
const { ownerPubKey, ownerPrivKey } = KeyPair.generate(32)
const { memberPubKey, memberPrivKey } = KeyPair.generate(32)
// Both parties compute shared secret between owner <-> member.
const sharedSecret =
ECDH(memberPubKey, ownerPrivKey)
|| ECDH(ownerPubKey, memberPrivKey)
// Use shared secret to derive a shared keypair.
const { sharedPubKey, sharedPrivKey } = KeyPair.from(sharedSecret) Channel Owner// The channel owner create a secret for the channel.
const channelSecret = Random.bytes(32)
// To share the channel secret with a select member,
// owner encrypts the secret using sharedPubKey
const encryptionKey = ecdh(ownerPrvKey, sharedPubKey)
const encryptedString = encrypt(encryptionKey, channelSecret)
// Use tag field as a hash map for distributing member keys.
const memberTag : HashMap = [
sharedPubKey, // Hex-encoded compressed public key.
encryptedString // AES-encrypted using member key and ECDH.
]
// Example of a channel event.
const channelEvent = {
id : 'eventId',
kind : 10000,
tags : [ memberTag ],
content : encryptedMetaData,
pub : ownerPublicKey,
sig : ownerSignature
} Channel Member// Check if shared pubKey exists in tags hash map.
if (channel.tags.has(sharedPubKey)) {
const encryptedString = tags.get(sharedPubKey)
// Use private key to derive secret between shared <-> owner.
const encryptionKey = ecdh(sharedPrvKey, ownerPubKey)
// Decrypt channel secret using shared encryption key.
const channelSecret = decrypt(encryptionKey, encryptedString)
// Decrypt the channel meta data using the channel secret.
const channelMetaData = decrypt(channelSecret, encryptedMetaData)
} |
@cmdruid that's pretty much the outline of this NIP 38 in code/pesudo-code. I have added some mention of kind 104, and otherwise edited to make it more in sync: Both Parties
Note: this shared secret is also used in kind 4 messages. Channel OwnerWhen creating the channel, the channel creator will create a random 'shared secret' and then create two messages a) First is a kind 104 message using the following logic, which is sent to EACH of the members ( it contains the shared secret, encrypted using the newly generated key mentioned in part "both parties")
b) second is a kind 140 event with following format, which also has member tags so people can fetch it with #p REQ:
Note: encryptedMetaData is in specific 'one line' format in this NIP 38 as of now, but should ideally be in json to add more information. Channel Member
NB I am assuming kind 4 works like this ECDH logic you mentioned, without verifying. Because kind 104 follows just how kind 4 does. |
Yeah you are correct, ECDH meaning Elliptic-Curve Diffe-Hellman, used to derive the shared secret. NIP-04 uses that and then encrypts using AES with random IV. Does the current proposal send DMs to all members on each channelSecret update? If that's the case, would it scale better for members to subscribe to the channel memberlist for updates, while using DMs sparingly to send out invites? Also side note: Since interim sharedPubKey obfuscates the real participating pubkeys, I don't believe the memberlist has any sensitive information? I think it would be neat if channelEvents are used to set up an encrypted group chat, but without it being publicly linked to the group chat itself, or any of its members. Then it would simply be a rendezvous for members to get the current state of the encrypted group. I like to use a hash of the channelSecret to tag the group chat, as publishing the hash does not reveal the secret, and members can subscribe to the tag as a group feed. You could also use the encryptedMetaData to store the real list of member pubkeys, so the sharedKeys aren't used outside of delivering the channelSecret. |
DM's ( of kind 104) need to be sent only when the secret is shared. Then no DM's need to be sent.
So then this will happen: creator gives shared secret key in DM to members. From that the members can derive its corresponding public key, and when they want to send messages to the group, they use that 'public key' as an e tag, lets call it channel-ID-tag. So when a member m1 sends a group chat message, all that outsiders will see is some random e tag is attached to the message. They would have no idea what that e tag is. They will also see other people using that e tag, so people can guess its one group. And since the member list is published as encrypted data, there is no publicly-seen member list. We can further improve it thus: there can be MULTIPLE channel-ID-tag, and the group would be told, as encrypted message, all the channel-ID-tag's of a group. The members can send message using any of the tags, and all the receivers can fetch all the tags. And since the set of channel-ID-tag for any channel was encrypted from start, no one outside would have any clue who is messaging as a group, as long as enough number of channel-ID-tag 's are used for each channel. Along with encrypted membership list as you mentioned too ( which can be updated with 141 kind as encrypted data too on addition of members or renaming of channel), this would give a lot of 'privacy' to users. Or some amount of anonymity, compared to the very base case scenario. NB. This logic can be used to create 1-1 communication channels between two people too, to have enhanced privacy over kind 4 DM's. They will use the first do a kind 104 DM to exchange these per-channel tags, and then communicate to each other using those tags. This increases the privacy of communication, because after the first DM, no one will know who is talking to whom ( except by some meta analysis, which can never be confirmed, unlike the open meta data of kind 4 messages). |
|
||
To add participants to the group, the admin or creator of the group emits a kind 141 message with the updated p tags, which also include the pubkeys of new members. Along with this, the admin shall also communicate to the newly added members the group shared-secret, using the same mechanism as mentioned in section for kind 104 above. | ||
|
||
### Removing Participants |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if a user wants to remove themself? Obviously the client can just ignore messages, but it seems like a good client should also notify the relay that they left the room, and the admin should remove them from the list and roll the keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's a good idea
## Kind 140: Create Encrypted channel | ||
|
||
Create a Encrypted chat channel. | ||
|
||
In the channel creation `content` field, Client SHOULD include basic channel metadata (`name`, `about` and `picture`). | ||
|
||
```json | ||
{ | ||
"content": "{\"name\": \"Demo Channel\", \"about\": \"A test channel.\", \"picture\": \"https://placekitten.com/200/200\"}", | ||
... | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Metadata leaks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, we can avoid this by using ephemeral public keys listed here:#570 (comment)
```json | ||
{ | ||
"content": "{\"name\": \"Updated Demo Channel\", \"about\": \"Updating a test channel.\", \"picture\": \"https://placekitten.com/201/201\"}", | ||
"tags": [["e", <channel_create_event_id> <relay-url>]], | ||
... | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Metadata leaks? Also you need to separate tags with ,
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tags should be in the envelope, yes - for a really private room. there is a need for both (hey whole world, this is a private room, please beg for an invite)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see also: #570 (comment) for a nice replacement that encrypts everything in a channel
} | ||
``` | ||
|
||
Further, the main use case of kind 141 event is to update the participants list. New p tags may be added, and old ones removed with the 141 event for an encrypted channel. Client will internally then manage participants. It should be noted that since the shared-secret is not changeable, the 'removed' members can continue to read all channel messages. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally a new shared secret can be generated when participants are removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
equivalent to rolling a new room and saying that's where we're talking now
|
||
Update an encrypted channel's public metadata including its particpants. | ||
|
||
Clients and relays SHOULD handle kind 141 events similar to kind 0 `metadata` events. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use replaceable events.
} | ||
``` | ||
|
||
## Kind 104: Communicate the Shared-Secret |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part could handle encryption the way it is done on #168 instead. This creates a single DM that can be read by multiple people by encrypting it with a unique secret that is included in the tags for every recipient.
This avoids the channel owner needing to send N messages every time someone is removed from the group.
Alternatively, and I don't think this is a good idea, but sharing it in case it sparks a better idea... this shared secret part could be removed entirely and all of the messages could be sent using the proposed NIP48 method. The issue with this is that the individual messages would have to include the entire list of people that can decrypt and bad clients could technically add/remove people from that list when sending messages and not obey the kind 141 participant list
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, just read that. sending N messages is avoided but you still need to do N encryptions. there's not a big difference really, except now the message is huge. so when someone wants to look for their message, they get a bunch of keys they can't use. i like the encryption in 38 better, yes you have to send more "messages", but they're smaller and this is better for routing, and you can send to different relays, etc.
|
||
The format of that message is: | ||
|
||
`App Encrypted Channels: inviting you to encrypted channel <channel id> encrypted using private public keys <shared private key> <corresponding shared public key>` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This message feels easy for humans to read and hard for machines to read. We should optimize for the opposite. Also, the public key can be derived from the public key so it does not need to be included.
My suggestion would be content="<shared private key>"
Then, include the as a tag of some sort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should not use a nip04 style encryption, instead it should use directional encryption
encrypt({
sender pub,
sender sig of this payload,
content=....
tags=...
}, ephemeral pub),
no tags outside the envelope, pubkey is ephemeral. recipient verifies the sender's provenance after decryption
all messages should work this way in this protocol - except possibly for a public broadcast advertisement (if you want everyone to know about the room)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any news about it? |
What's the state of this NIP?? So is this still relevant? |
Status as of May 2023: This is implemented, the way its mentioned right now, in nostr console. There are some improvements needed, or suggested, such as using json format to share initial shared secret etc, and there are many other improvements possible, most mentioned here in comments. I will like to implement them at least in nostr console ( but nothing certain about when that may happen). Others may also take up and implement these ideas in their/other clients. This is relevant in that this is the simplest way to implement group chat ( with single shared secret, shared with all members), but it has its inherent drawbacks. |
} | ||
``` | ||
|
||
## Kind 104: Communicate the Shared-Secret |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, just read that. sending N messages is avoided but you still need to do N encryptions. there's not a big difference really, except now the message is huge. so when someone wants to look for their message, they get a bunch of keys they can't use. i like the encryption in 38 better, yes you have to send more "messages", but they're smaller and this is better for routing, and you can send to different relays, etc.
|
||
The format of that message is: | ||
|
||
`App Encrypted Channels: inviting you to encrypted channel <channel id> encrypted using private public keys <shared private key> <corresponding shared public key>` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
Clients that support this NIP will read that and know that they're invited to join the given encrypted channel, and they can use the given secret key to send/read messages in that channel. | ||
|
||
Other clients will decrypt this message, and ignore it, but the user can read that they have been invited to such a channel. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
other clients won't decrypt it, since it's not kind 4. i think making it "compatible" makes things actually more confusing, not less. better to simply not see it. then you can dm them and be like "bruh get with a new client"
new clients that don't implement chat can chose to show invitations without supporting chat no matter what the format is
and nip04 is kinda broken now (possible iv collisions, structured ss key reuse, etc)
|
||
Note: Clients for a user may create a channel where the only participant is the creator of the channel. Then new members can be added by sending a kind 141 message, as discussed further. | ||
|
||
## Kind 141: Update metadata and Add participants |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
im building this now, based on the "considered harmful discussions" with @paulmillr mostly as a POC. using the new "versioned encryption" system, ephemeral public invitations and well-known group public/private key, like in this document. working fine so far. you can play with a test flight version soon.
https://github.com/ArcadeLabsInc/arclib/pull/20/files
NIP-38 is modeled on NIP-28, group chat. Main difference is use of a shared secret, and using p-tags to mention members in channel create and update events. Simplicity is one of the main goals.
Implementation for the events mentioned ( 140-142) is in Nostr Console 0.0.9 beta release.
Note 1: One important aspect of this NIP-38 ( kind 14x) is that they have a participant list. The 4x group chat is public by design, and it is unlikely that it will have a participant list anytime in future ( though it can add a PoW condition to participate easily). So the 14x public chat, whether it has secret shared secret, or even in case where its 'shared secret' is public, can still serve well because it allows the channel creator to mention as p-tags in 140/141 events who are the participants of the group.
Edit: pull request made more for discussion.