nips icon indicating copy to clipboard operation
nips copied to clipboard

NIP-38 - Encrypted Group Chat using a single shared secret

Open vishalxl opened this issue 3 years ago • 15 comments

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.

vishalxl avatar Nov 06 '22 16:11 vishalxl

A related note. We may need to have in total three types of "channels":

a) totally public kind 4x channels b) encrypted channels with membership (like this proposed NIP 38 with kind 14x) c) public channels with membership ( where content is public but only allowed people are considered in), could be kind 24x

Edit: On second thought, this needs revisiting. Cause membership is possible even in channel 40 with an extension of adding p tags as members.

vishalxl avatar Nov 24 '22 19:11 vishalxl

c) public channels with membership ( where content is public but only allowed people are considered in), could be kind 24x

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

Semisol avatar Nov 25 '22 18:11 Semisol

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: https://en.wikipedia.org//wiki/Double_Ratchet_Algorithm

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

cmdruid avatar Nov 25 '22 19:11 cmdruid

Thanks for writing this NIP. I think it can be used as an alternative to https://github.com/nostr-protocol/nips/pull/49 for joinstr.

I have not tested it yet in nostr console. Will do it today.

ghost avatar Nov 27 '22 20:11 ghost

I think this may be a superior alternative, since it would be fully stateless like NIP-04: https://github.com/nostr-protocol/nips/issues/72#issuecomment-1353592661

fiatjaf avatar Dec 15 '22 19:12 fiatjaf

@fiatjaf

I think this may be a superior alternative, since it would be fully stateless like NIP-04: https://github.com/nostr-protocol/nips/issues/72#issuecomment-1353592661

Can you explain the stateless part in detail or what are the advantages of this method over the one shared in this PR?

@vishalxl

The meta-data for any message is practically much public in nostr. So such group chat should not be considered private. Anybody can see who is sending message(s) to what channel, and when.

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.

Once members are 'removed' by using the kind 141 event for a channel, they can continue to read the new messages posted in the group, because currently there is no provision to change the shared secret of a single group. The remaining users, may howerver choose to create a new group where the recently-removed member is not a member, and which would have a new shared-secret.

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.

ghost avatar Dec 27 '22 22:12 ghost

Can you explain the stateless part in detail or what are the advantages of this method over the one shared in this PR?

Each event contains its own decryption keys, just like NIP-04, that is what I called "stateless".

fiatjaf avatar Dec 27 '22 22:12 fiatjaf

aB, aC and aD

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?

vishalxl avatar Dec 27 '22 23:12 vishalxl

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.

cmdruid avatar Dec 28 '22 01:12 cmdruid

What about a replaceable event that simply provides each member with the latest shared secret for a channel, encrypted to their pubkey?

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.

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.

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.

You could also throw other channel metadata in there as well, just use the whole content field as a metadata store for the channel.

I am thinking of having a json for kind 104, with details about group also thrown in, if that's what you mean too.

vishalxl avatar Dec 28 '22 04:12 vishalxl

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.

Can you elaborate on this more? Are you encrypting the private key using the public key and ECDH?

cmdruid avatar Dec 28 '22 04:12 cmdruid

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 avatar Dec 28 '22 06:12 cmdruid

@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

// 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)

Note: this shared secret is also used in kind 4 messages.

Channel Owner

When 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")

// 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)

// cmd: Use tag field as a hash map for distributing member keys. 
// my comment: the hashmap is just application logic; this can be the creator going in a loop. The application will have generated different secret for each intended member/recipient, so it will send them to them using their specific secret. This is the kind 104 , which is modeled on kind 4.
const memberTag : HashMap = [
  sharedPubKey,    // Hex-encoded compressed public key.
  encryptedString  // AES-encrypted using member key and ECDH.
]


b) second is a kind 140 event with following format, which also has member tags so people can fetch it with #p REQ:

// Example of a channel event.
const channelEvent = {
  id      : 'eventId',
  kind    : 10000,
  tags    : [ memberTag ],
  content : encryptedMetaData,
  pub     : ownerPublicKey,
  sig     : ownerSignature
}

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

// 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)
}

NB I am assuming kind 4 works like this ECDH logic you mentioned, without verifying. Because kind 104 follows just how kind 4 does.

vishalxl avatar Dec 28 '22 07:12 vishalxl

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.

cmdruid avatar Dec 28 '22 09:12 cmdruid

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?

DM's ( of kind 104) need to be sent only when the secret is shared. Then no DM's need to be sent.

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.

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).

vishalxl avatar Dec 28 '22 09:12 vishalxl

Any news about it?

guilhermegps avatar Apr 24 '23 00:04 guilhermegps

What's the state of this NIP?? The last update is a couple of months ago.

So is this still relevant?

MTG2000 avatar May 09 '23 11:05 MTG2000

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.

vishalxl avatar May 10 '23 03:05 vishalxl

Closing in favor of #574, #686 and possibly #875

staab avatar Dec 14 '23 19:12 staab