From 3292c1c88355d06e29ede0c4497d2ba512e1f404 Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Wed, 18 Oct 2023 19:01:02 +0200 Subject: [PATCH] feat: rekey community on control node change closes: status-im/status-desktop#11963 --- protocol/communities/community.go | 5 +- protocol/communities/community_changes.go | 12 +- .../community_encryption_key_action.go | 27 ++- .../community_encryption_key_action_test.go | 159 ++++++++++++++++++ 4 files changed, 198 insertions(+), 5 deletions(-) diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 1fc9984bb..b62593063 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -2207,7 +2207,10 @@ func (o *Community) createChat(chatID string, chat *protobuf.CommunityChat) erro } } - chat.Members = o.config.CommunityDescription.Members + chat.Members = make(map[string]*protobuf.CommunityMember) + for pubKey, member := range o.config.CommunityDescription.Members { + chat.Members[pubKey] = member + } o.config.CommunityDescription.Chats[chatID] = chat diff --git a/protocol/communities/community_changes.go b/protocol/communities/community_changes.go index cebaef6ed..6409d001f 100644 --- a/protocol/communities/community_changes.go +++ b/protocol/communities/community_changes.go @@ -1,6 +1,10 @@ package communities -import "github.com/status-im/status-go/protocol/protobuf" +import ( + "crypto/ecdsa" + + "github.com/status-im/status-go/protocol/protobuf" +) type CommunityChatChanges struct { ChatModified *protobuf.CommunityChat @@ -14,6 +18,8 @@ type CommunityChatChanges struct { type CommunityChanges struct { Community *Community `json:"community"` + ControlNodeChanged *ecdsa.PublicKey `json:"controlNodeChanged"` + MembersAdded map[string]*protobuf.CommunityMember `json:"membersAdded"` MembersRemoved map[string]*protobuf.CommunityMember `json:"membersRemoved"` @@ -82,6 +88,10 @@ func (c *CommunityChanges) HasMemberLeft(identity string) bool { func EvaluateCommunityChanges(origin, modified *Community) *CommunityChanges { changes := evaluateCommunityChangesByDescription(origin.Description(), modified.Description()) + if origin.ControlNode() != nil && !modified.ControlNode().Equal(origin.ControlNode()) { + changes.ControlNodeChanged = modified.ControlNode() + } + originTokenPermissions := origin.tokenPermissions() modifiedTokenPermissions := modified.tokenPermissions() diff --git a/protocol/communities/community_encryption_key_action.go b/protocol/communities/community_encryption_key_action.go index 5dfc43cc0..cac75556c 100644 --- a/protocol/communities/community_encryption_key_action.go +++ b/protocol/communities/community_encryption_key_action.go @@ -57,7 +57,14 @@ func evaluateCommunityLevelEncryptionKeyAction(origin, modified *Community, chan originBecomeMemberPermissions := origin.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER) modifiedBecomeMemberPermissions := modified.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER) - return evaluateEncryptionKeyAction(originBecomeMemberPermissions, modifiedBecomeMemberPermissions, modified.config.CommunityDescription.Members, changes.MembersAdded, changes.MembersRemoved) + return evaluateEncryptionKeyAction( + originBecomeMemberPermissions, + modifiedBecomeMemberPermissions, + modified.config.CommunityDescription.Members, + changes.MembersAdded, + changes.MembersRemoved, + changes.ControlNodeChanged != nil, + ) } func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, changes *CommunityChanges) *map[string]EncryptionKeyAction { @@ -83,13 +90,20 @@ func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, chang membersRemoved = chatChanges.MembersRemoved } - result[channelID] = *evaluateEncryptionKeyAction(originChannelPermissions, modifiedChannelPermissions, modified.config.CommunityDescription.Chats[channelID].Members, membersAdded, membersRemoved) + result[channelID] = *evaluateEncryptionKeyAction( + originChannelPermissions, + modifiedChannelPermissions, + modified.config.CommunityDescription.Chats[channelID].Members, + membersAdded, membersRemoved, + changes.ControlNodeChanged != nil, + ) } return &result } -func evaluateEncryptionKeyAction(originPermissions, modifiedPermissions []*CommunityTokenPermission, allMembers, membersAdded, membersRemoved map[string]*protobuf.CommunityMember) *EncryptionKeyAction { +func evaluateEncryptionKeyAction(originPermissions, modifiedPermissions []*CommunityTokenPermission, + allMembers, membersAdded, membersRemoved map[string]*protobuf.CommunityMember, controlNodeChanged bool) *EncryptionKeyAction { result := &EncryptionKeyAction{ ActionType: EncryptionKeyNone, Members: map[string]*protobuf.CommunityMember{}, @@ -103,6 +117,13 @@ func evaluateEncryptionKeyAction(originPermissions, modifiedPermissions []*Commu return to } + // control node changed on closed community/channel + if controlNodeChanged && len(modifiedPermissions) > 0 { + result.ActionType = EncryptionKeyRekey + result.Members = copyMap(allMembers) + return result + } + // permission was just added if len(modifiedPermissions) > 0 && len(originPermissions) == 0 { result.ActionType = EncryptionKeyAdd diff --git a/protocol/communities/community_encryption_key_action_test.go b/protocol/communities/community_encryption_key_action_test.go index df11b8c5f..443166756 100644 --- a/protocol/communities/community_encryption_key_action_test.go +++ b/protocol/communities/community_encryption_key_action_test.go @@ -2,6 +2,7 @@ package communities import ( "crypto/ecdsa" + "reflect" "testing" "github.com/stretchr/testify/suite" @@ -26,6 +27,7 @@ func createTestCommunity(identity *ecdsa.PrivateKey) (*Community, error) { CommunityTokensMetadata: []*protobuf.CommunityTokenMetadata{}, }, ID: &identity.PublicKey, + ControlNode: &identity.PublicKey, Joined: true, MemberIdentity: &identity.PublicKey, } @@ -825,3 +827,160 @@ func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() { s.Require().NotNil(actions.ChannelKeysActions[channelID]) s.Require().Equal(actions.ChannelKeysActions[channelID].ActionType, EncryptionKeyAdd) } + +func (s *CommunityEncryptionKeyActionSuite) TestControlNodeChange() { + channelID := "1234" + chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID + + testCases := []struct { + name string + permissions []*protobuf.CommunityTokenPermission + members []*ecdsa.PublicKey + channelMembers []*ecdsa.PublicKey + expectedActions EncryptionKeyActions + }{ + { + name: "change control node in open community", + permissions: []*protobuf.CommunityTokenPermission{}, + members: []*ecdsa.PublicKey{&s.member1.PublicKey, &s.member2.PublicKey}, + channelMembers: []*ecdsa.PublicKey{&s.member1.PublicKey}, + expectedActions: EncryptionKeyActions{ + CommunityKeyAction: EncryptionKeyAction{ + ActionType: EncryptionKeyNone, + Members: map[string]*protobuf.CommunityMember{}, + }, + ChannelKeysActions: map[string]EncryptionKeyAction{ + channelID: EncryptionKeyAction{ + ActionType: EncryptionKeyNone, + Members: map[string]*protobuf.CommunityMember{}, + }, + }, + }, + }, + { + name: "change control node in token-gated community", + permissions: []*protobuf.CommunityTokenPermission{ + &protobuf.CommunityTokenPermission{ + Id: "some-id", + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: make([]*protobuf.TokenCriteria, 0), + ChatIds: []string{}, + }, + }, + members: []*ecdsa.PublicKey{&s.member1.PublicKey, &s.member2.PublicKey}, + channelMembers: []*ecdsa.PublicKey{&s.member1.PublicKey}, + expectedActions: EncryptionKeyActions{ + CommunityKeyAction: EncryptionKeyAction{ + ActionType: EncryptionKeyRekey, + Members: map[string]*protobuf.CommunityMember{ + s.member1Key: &protobuf.CommunityMember{}, + s.member2Key: &protobuf.CommunityMember{}, + }, + }, + ChannelKeysActions: map[string]EncryptionKeyAction{ + channelID: EncryptionKeyAction{ + ActionType: EncryptionKeyNone, + Members: map[string]*protobuf.CommunityMember{}, + }, + }, + }, + }, + { + name: "change control node in open community with token-gated channel", + permissions: []*protobuf.CommunityTokenPermission{ + &protobuf.CommunityTokenPermission{ + Id: "some-id", + Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, + TokenCriteria: make([]*protobuf.TokenCriteria, 0), + ChatIds: []string{chatID}, + }, + }, + members: []*ecdsa.PublicKey{&s.member1.PublicKey, &s.member2.PublicKey}, + channelMembers: []*ecdsa.PublicKey{&s.member1.PublicKey}, + expectedActions: EncryptionKeyActions{ + CommunityKeyAction: EncryptionKeyAction{ + ActionType: EncryptionKeyNone, + Members: map[string]*protobuf.CommunityMember{}, + }, + ChannelKeysActions: map[string]EncryptionKeyAction{ + channelID: EncryptionKeyAction{ + ActionType: EncryptionKeyRekey, + Members: map[string]*protobuf.CommunityMember{ + s.member1Key: &protobuf.CommunityMember{}, + }, + }, + }, + }, + }, + { + name: "change control node in token-gated community with token-gated channel", + permissions: []*protobuf.CommunityTokenPermission{ + &protobuf.CommunityTokenPermission{ + Id: "some-id-1", + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: make([]*protobuf.TokenCriteria, 0), + ChatIds: []string{}, + }, + &protobuf.CommunityTokenPermission{ + Id: "some-id-2", + Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, + TokenCriteria: make([]*protobuf.TokenCriteria, 0), + ChatIds: []string{chatID}, + }, + }, + members: []*ecdsa.PublicKey{&s.member1.PublicKey, &s.member2.PublicKey}, + channelMembers: []*ecdsa.PublicKey{&s.member1.PublicKey}, + expectedActions: EncryptionKeyActions{ + CommunityKeyAction: EncryptionKeyAction{ + ActionType: EncryptionKeyRekey, + Members: map[string]*protobuf.CommunityMember{ + s.member1Key: &protobuf.CommunityMember{}, + s.member2Key: &protobuf.CommunityMember{}, + }, + }, + ChannelKeysActions: map[string]EncryptionKeyAction{ + channelID: EncryptionKeyAction{ + ActionType: EncryptionKeyRekey, + Members: map[string]*protobuf.CommunityMember{ + s.member1Key: &protobuf.CommunityMember{}, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + origin, err := createTestCommunity(s.identity) + s.Require().NoError(err) + + _, err = origin.CreateChat(channelID, &protobuf.CommunityChat{ + Members: map[string]*protobuf.CommunityMember{}, + Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP}, + Identity: &protobuf.ChatIdentity{}, + }) + s.Require().NoError(err) + + for _, permission := range tc.permissions { + _, err := origin.UpsertTokenPermission(permission) + s.Require().NoError(err) + } + for _, member := range tc.members { + _, err := origin.AddMember(member, []protobuf.CommunityMember_Roles{}) + s.Require().NoError(err) + } + for _, member := range tc.channelMembers { + _, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{}) + s.Require().NoError(err) + } + + // change control node to arbitrary member + modified := origin.CreateDeepCopy() + modified.setControlNode(&s.member1.PublicKey) + + actions := EvaluateCommunityEncryptionKeyActions(origin, modified) + s.Require().True(reflect.DeepEqual(tc.expectedActions, *actions)) + }) + } +}