Skip to content

Commit

Permalink
feat: introduce channel-level encryption
Browse files Browse the repository at this point in the history
- distribute ratchet keys at both community and channel levels
- use explicit `HashRatchetGroupID` in ecryption layer, instead of
  inheriting `groupID` from `CommunityID`
- populate `HashRatchetGroupID` with `CommunityID+ChannelID` for
  channels, and `CommunityID` for whole community
- hydrate channels with members; channel members are now subset of
  community members
- include channel permissions in periodic permissions check

closes: status-im/status-desktop#10998
  • Loading branch information
osmaczko committed Jul 27, 2023
1 parent 3f6963d commit 65bc869
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 108 deletions.
4 changes: 2 additions & 2 deletions protocol/common/message_sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func (s *MessageSender) sendCommunity(
// Check if it's a key exchange message. In this case we send it
// to all the recipients
if rawMessage.CommunityKeyExMsgType != KeyExMsgNone {
keyExMessageSpecs, err := s.protocol.GetKeyExMessageSpecs(rawMessage.CommunityID, s.identity, rawMessage.Recipients, rawMessage.CommunityKeyExMsgType == KeyExMsgRekey)
keyExMessageSpecs, err := s.protocol.GetKeyExMessageSpecs(rawMessage.HashRatchetGroupID, s.identity, rawMessage.Recipients, rawMessage.CommunityKeyExMsgType == KeyExMsgRekey)
if err != nil {
return nil, err
}
Expand All @@ -307,7 +307,7 @@ func (s *MessageSender) sendCommunity(

// If it's a chat message, we send it on the community chat topic
if ShouldCommunityMessageBeEncrypted(rawMessage.MessageType) {
messageSpec, err := s.protocol.BuildHashRatchetMessage(rawMessage.CommunityID, wrappedMessage)
messageSpec, err := s.protocol.BuildHashRatchetMessage(rawMessage.HashRatchetGroupID, wrappedMessage)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions protocol/common/raw_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ type RawMessage struct {
CommunityKeyExMsgType CommKeyExMsgType
Ephemeral bool
BeforeDispatch func(*RawMessage) error
HashRatchetGroupID []byte
}
87 changes: 65 additions & 22 deletions protocol/communities/community.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,20 @@ func (o *Community) GetMember(pk *ecdsa.PublicKey) *protobuf.CommunityMember {
return o.getMember(pk)
}

func (o *Community) getChatMember(pk *ecdsa.PublicKey, chatID string) *protobuf.CommunityMember {
if !o.hasMember(pk) {
return nil
}

chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok {
return nil
}

key := common.PubkeyToHex(pk)
return chat.Members[key]
}

func (o *Community) hasMember(pk *ecdsa.PublicKey) bool {

member := o.getMember(pk)
Expand Down Expand Up @@ -736,18 +750,7 @@ func (o *Community) IsMemberInChat(pk *ecdsa.PublicKey, chatID string) bool {
o.mutex.Lock()
defer o.mutex.Unlock()

if !o.hasMember(pk) {
return false
}

chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok {
return false
}

key := common.PubkeyToHex(pk)
_, ok = chat.Members[key]
return ok
return o.getChatMember(pk, chatID) != nil
}

func (o *Community) RemoveUserFromChat(pk *ecdsa.PublicKey, chatID string) (*protobuf.CommunityDescription, error) {
Expand Down Expand Up @@ -905,17 +908,27 @@ func (o *Community) AddRoleToMember(pk *ecdsa.PublicKey, role protobuf.Community
}

updated := false
member := o.getMember(pk)
if member != nil {
addRole := func(member *protobuf.CommunityMember) {
roles := make(map[protobuf.CommunityMember_Roles]bool)
roles[role] = true
if !o.hasMemberPermission(member, roles) {
member.Roles = append(member.Roles, role)
o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] = member
updated = true
}
}

member := o.getMember(pk)
if member != nil {
addRole(member)
}

for channelID := range o.chats() {
chatMember := o.getChatMember(pk, channelID)
if chatMember != nil {
addRole(member)
}
}

if updated {
o.increaseClock()
}
Expand All @@ -931,8 +944,7 @@ func (o *Community) RemoveRoleFromMember(pk *ecdsa.PublicKey, role protobuf.Comm
}

updated := false
member := o.getMember(pk)
if member != nil {
removeRole := func(member *protobuf.CommunityMember) {
roles := make(map[protobuf.CommunityMember_Roles]bool)
roles[role] = true
if o.hasMemberPermission(member, roles) {
Expand All @@ -943,11 +955,22 @@ func (o *Community) RemoveRoleFromMember(pk *ecdsa.PublicKey, role protobuf.Comm
}
}
member.Roles = newRoles
o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] = member
updated = true
}
}

member := o.getMember(pk)
if member != nil {
removeRole(member)
}

for channelID := range o.chats() {
chatMember := o.getChatMember(pk, channelID)
if chatMember != nil {
removeRole(member)
}
}

if updated {
o.increaseClock()
}
Expand Down Expand Up @@ -1328,16 +1351,20 @@ func (o *Community) ToBytes() ([]byte, error) {
}

func (o *Community) Chats() map[string]*protobuf.CommunityChat {
response := make(map[string]*protobuf.CommunityChat)

// Why are we checking here for nil, it should be the responsibility of the caller
if o == nil {
return response
return make(map[string]*protobuf.CommunityChat)
}

o.mutex.Lock()
defer o.mutex.Unlock()

return o.chats()
}

func (o *Community) chats() map[string]*protobuf.CommunityChat {
response := make(map[string]*protobuf.CommunityChat)

if o.config != nil && o.config.CommunityDescription != nil {
for k, v := range o.config.CommunityDescription.Chats {
response[k] = v
Expand Down Expand Up @@ -1391,7 +1418,21 @@ func (o *Community) TokenPermissions() map[string]*protobuf.CommunityTokenPermis
}

func (o *Community) HasTokenPermissions() bool {
return len(o.config.CommunityDescription.TokenPermissions) > 0
return o.config.CommunityDescription.TokenPermissions != nil && len(o.config.CommunityDescription.TokenPermissions) > 0
}

func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
if !o.HasTokenPermissions() {
return false
}

for _, tokenPermission := range o.TokenPermissions() {
if includes(tokenPermission.ChatIds, chatID) {
return true
}
}

return false
}

func TokenPermissionsByType(permissions map[string]*protobuf.CommunityTokenPermission, permissionType protobuf.CommunityTokenPermission_Type) []*protobuf.CommunityTokenPermission {
Expand Down Expand Up @@ -2064,6 +2105,8 @@ func (o *Community) createChat(chatID string, chat *protobuf.CommunityChat) erro
}
}

chat.Members = o.config.CommunityDescription.Members

o.config.CommunityDescription.Chats[chatID] = chat

return nil
Expand Down
8 changes: 5 additions & 3 deletions protocol/communities/community_encryption_key_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ func evaluateCommunityLevelEncryptionKeyAction(origin, modified *Community, chan
func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, changes *CommunityChanges) *map[string]EncryptionKeyAction {
result := make(map[string]EncryptionKeyAction)

for chatID := range modified.config.CommunityDescription.Chats {
for channelID := range modified.config.CommunityDescription.Chats {
chatID := modified.IDString() + channelID

originChannelViewOnlyPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
originChannelViewAndPostPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
originChannelPermissions := append(originChannelViewOnlyPermissions, originChannelViewAndPostPermissions...)
Expand All @@ -75,13 +77,13 @@ func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, chang
membersAdded := make(map[string]*protobuf.CommunityMember)
membersRemoved := make(map[string]*protobuf.CommunityMember)

chatChanges, ok := changes.ChatsModified[chatID]
chatChanges, ok := changes.ChatsModified[channelID]
if ok {
membersAdded = chatChanges.MembersAdded
membersRemoved = chatChanges.MembersRemoved
}

result[chatID] = *evaluateEncryptionKeyAction(originChannelPermissions, modifiedChannelPermissions, modified.config.CommunityDescription.Members, membersAdded, membersRemoved)
result[channelID] = *evaluateEncryptionKeyAction(originChannelPermissions, modifiedChannelPermissions, modified.config.CommunityDescription.Chats[channelID].Members, membersAdded, membersRemoved)
}

return &result
Expand Down
22 changes: 13 additions & 9 deletions protocol/communities/community_encryption_key_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/suite"

"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
Expand Down Expand Up @@ -622,7 +623,8 @@ func (s *CommunityEncryptionKeyActionSuite) TestCommunityLevelKeyActions_Permiss
}

func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
chatID := "0x1234"
channelID := "1234"
chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID
testCases := []struct {
name string
originPermissions []*protobuf.CommunityTokenPermission
Expand Down Expand Up @@ -736,7 +738,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
for _, tc := range testCases {
s.Run(tc.name, func() {
origin := createTestCommunity(s.identity)
_, err := origin.CreateChat(chatID, &protobuf.CommunityChat{
_, err := origin.CreateChat(channelID, &protobuf.CommunityChat{
Members: map[string]*protobuf.CommunityMember{},
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP},
Identity: &protobuf.ChatIdentity{},
Expand All @@ -752,7 +754,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
for _, member := range tc.originMembers {
_, err := origin.AddMember(member, []protobuf.CommunityMember_Roles{})
s.Require().NoError(err)
_, err = origin.AddMemberToChat(chatID, member, []protobuf.CommunityMember_Roles{})
_, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{})
s.Require().NoError(err)
}

Expand All @@ -763,12 +765,12 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
for _, member := range tc.modifiedMembers {
_, err := modified.AddMember(member, []protobuf.CommunityMember_Roles{})
s.Require().NoError(err)
_, err = modified.AddMemberToChat(chatID, member, []protobuf.CommunityMember_Roles{})
_, err = modified.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{})
s.Require().NoError(err)
}

actions := EvaluateCommunityEncryptionKeyActions(origin, modified)
channelAction, ok := actions.ChannelKeysActions[chatID]
channelAction, ok := actions.ChannelKeysActions[channelID]
s.Require().True(ok)
s.Require().Equal(tc.expectedAction.ActionType, channelAction.ActionType)
s.Require().Len(tc.expectedAction.Members, len(channelAction.Members))
Expand All @@ -783,8 +785,10 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() {
newCommunity := createTestCommunity(s.identity)

chatID := "0x1234"
_, err := newCommunity.CreateChat(chatID, &protobuf.CommunityChat{
channelID := "0x1234"
chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID

_, err := newCommunity.CreateChat(channelID, &protobuf.CommunityChat{
Members: map[string]*protobuf.CommunityMember{},
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP},
Identity: &protobuf.ChatIdentity{},
Expand Down Expand Up @@ -813,6 +817,6 @@ func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() {
actions := EvaluateCommunityEncryptionKeyActions(nil, newCommunity)
s.Require().Equal(actions.CommunityKeyAction.ActionType, EncryptionKeyAdd)
s.Require().Len(actions.ChannelKeysActions, 1)
s.Require().NotNil(actions.ChannelKeysActions[chatID])
s.Require().Equal(actions.ChannelKeysActions[chatID].ActionType, EncryptionKeyAdd)
s.Require().NotNil(actions.ChannelKeysActions[channelID])
s.Require().Equal(actions.ChannelKeysActions[channelID].ActionType, EncryptionKeyAdd)
}
Loading

0 comments on commit 65bc869

Please sign in to comment.