From 3faee08940f965fcb1db57e8aeff21eff423490f Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Fri, 23 Jun 2023 12:49:26 +0200 Subject: [PATCH] feat: introduce channel-level encryption closes: status-im/status-desktop#10998 --- protocol/common/message_sender.go | 4 +- protocol/common/raw_message.go | 1 + protocol/communities/community.go | 116 ++++++++---- .../community_encryption_key_action.go | 14 +- .../community_encryption_key_action_test.go | 12 +- protocol/communities/community_test.go | 12 +- protocol/communities/manager.go | 176 ++++++++++++------ protocol/communities_key_distributor.go | 7 +- ...nities_messenger_token_permissions_test.go | 151 ++++++++++++++- protocol/messenger.go | 29 ++- protocol/messenger_communities.go | 12 +- 11 files changed, 403 insertions(+), 131 deletions(-) diff --git a/protocol/common/message_sender.go b/protocol/common/message_sender.go index 96d18af4edb..7030c68183c 100644 --- a/protocol/common/message_sender.go +++ b/protocol/common/message_sender.go @@ -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 } @@ -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 } diff --git a/protocol/common/raw_message.go b/protocol/common/raw_message.go index fe5fd7fadc8..4fcf05d22d0 100644 --- a/protocol/common/raw_message.go +++ b/protocol/common/raw_message.go @@ -35,4 +35,5 @@ type RawMessage struct { CommunityKeyExMsgType CommKeyExMsgType Ephemeral bool BeforeDispatch func(*RawMessage) error + HashRatchetGroupID []byte } diff --git a/protocol/communities/community.go b/protocol/communities/community.go index f8db96f7094..0d5dcefdf61 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -670,7 +670,6 @@ func (o *Community) InviteUserToChat(pk *ecdsa.PublicKey, chatID string) (*proto } func (o *Community) getMember(pk *ecdsa.PublicKey) *protobuf.CommunityMember { - key := common.PubkeyToHex(pk) member := o.config.CommunityDescription.Members[key] return member @@ -680,6 +679,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) @@ -703,16 +716,16 @@ func (o *Community) isBanned(pk *ecdsa.PublicKey) bool { return false } -func (o *Community) hasMemberPermission(member *protobuf.CommunityMember, permissions map[protobuf.CommunityMember_Roles]bool) bool { +func (o *Community) memberHasRoles(member *protobuf.CommunityMember, roles map[protobuf.CommunityMember_Roles]bool) bool { for _, r := range member.Roles { - if permissions[r] { + if roles[r] { return true } } return false } -func (o *Community) hasPermission(pk *ecdsa.PublicKey, roles map[protobuf.CommunityMember_Roles]bool) bool { +func (o *Community) hasRoles(pk *ecdsa.PublicKey, roles map[protobuf.CommunityMember_Roles]bool) bool { if pk == nil || o.config == nil || o.config.ID == nil { return false } @@ -722,7 +735,7 @@ func (o *Community) hasPermission(pk *ecdsa.PublicKey, roles map[protobuf.Commun return false } - return o.hasMemberPermission(member, roles) + return o.memberHasRoles(member, roles) } func (o *Community) HasMember(pk *ecdsa.PublicKey) bool { @@ -735,18 +748,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) { @@ -904,17 +906,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) { + if !o.memberHasRoles(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() } @@ -930,11 +942,10 @@ 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) { + if o.memberHasRoles(member, roles) { var newRoles []protobuf.CommunityMember_Roles for _, r := range member.Roles { if r != role { @@ -942,11 +953,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() } @@ -1131,36 +1153,36 @@ func (o *Community) IsOwnerOrAdmin() bool { } func (o *Community) IsMemberOwner(publicKey *ecdsa.PublicKey) bool { - return o.hasPermission(publicKey, ownerRolePermission()) + return o.hasRoles(publicKey, ownerRoles()) } func (o *Community) IsMemberAdmin(publicKey *ecdsa.PublicKey) bool { - return o.hasPermission(publicKey, adminRolePermissions()) + return o.hasRoles(publicKey, adminRoles()) } func (o *Community) IsMemberOwnerOrAdmin(publicKey *ecdsa.PublicKey) bool { - return o.hasPermission(publicKey, ownerOrAdminRolePermissions()) + return o.hasRoles(publicKey, ownerOrAdminRoles()) } -func canManageUsersRolePermissions() map[protobuf.CommunityMember_Roles]bool { - roles := ownerOrAdminRolePermissions() +func canManageUsersRoles() map[protobuf.CommunityMember_Roles]bool { + roles := ownerOrAdminRoles() roles[protobuf.CommunityMember_ROLE_MANAGE_USERS] = true return roles } -func ownerRolePermission() map[protobuf.CommunityMember_Roles]bool { +func ownerRoles() map[protobuf.CommunityMember_Roles]bool { roles := make(map[protobuf.CommunityMember_Roles]bool) roles[protobuf.CommunityMember_ROLE_OWNER] = true return roles } -func adminRolePermissions() map[protobuf.CommunityMember_Roles]bool { +func adminRoles() map[protobuf.CommunityMember_Roles]bool { roles := make(map[protobuf.CommunityMember_Roles]bool) roles[protobuf.CommunityMember_ROLE_ADMIN] = true return roles } -func ownerOrAdminRolePermissions() map[protobuf.CommunityMember_Roles]bool { +func ownerOrAdminRoles() map[protobuf.CommunityMember_Roles]bool { roles := make(map[protobuf.CommunityMember_Roles]bool) roles[protobuf.CommunityMember_ROLE_OWNER] = true roles[protobuf.CommunityMember_ROLE_ADMIN] = true @@ -1182,7 +1204,7 @@ func (o *Community) MemberRole(pubKey *ecdsa.PublicKey) protobuf.CommunityMember } func canDeleteMessageForEveryonePermissions() map[protobuf.CommunityMember_Roles]bool { - roles := ownerOrAdminRolePermissions() + roles := ownerOrAdminRoles() roles[protobuf.CommunityMember_ROLE_MODERATE_CONTENT] = true return roles } @@ -1392,7 +1414,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 { @@ -1731,8 +1767,8 @@ func (o *Community) CanManageUsers(pk *ecdsa.PublicKey) bool { return false } - roles := canManageUsersRolePermissions() - return o.hasPermission(pk, roles) + roles := canManageUsersRoles() + return o.hasRoles(pk, roles) } @@ -1749,7 +1785,7 @@ func (o *Community) CanDeleteMessageForEveryone(pk *ecdsa.PublicKey) bool { } roles := canDeleteMessageForEveryonePermissions() - return o.hasPermission(pk, roles) + return o.hasRoles(pk, roles) } func (o *Community) isMember() bool { @@ -1787,9 +1823,9 @@ func (o *Community) nextClock() uint64 { func (o *Community) CanManageUsersPublicKeys() ([]*ecdsa.PublicKey, error) { var response []*ecdsa.PublicKey - roles := canManageUsersRolePermissions() + roles := canManageUsersRoles() for pkString, member := range o.config.CommunityDescription.Members { - if o.hasMemberPermission(member, roles) { + if o.memberHasRoles(member, roles) { pk, err := common.HexToPubkey(pkString) if err != nil { return nil, err diff --git a/protocol/communities/community_encryption_key_action.go b/protocol/communities/community_encryption_key_action.go index 2472a4ca72e..958ce2a07e5 100644 --- a/protocol/communities/community_encryption_key_action.go +++ b/protocol/communities/community_encryption_key_action.go @@ -63,25 +63,25 @@ 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 { - originChannelViewOnlyPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) - originChannelViewAndPostPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) + for channelID := range modified.config.CommunityDescription.Chats { + originChannelViewOnlyPermissions := origin.ChannelTokenPermissionsByType(modified.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) + originChannelViewAndPostPermissions := origin.ChannelTokenPermissionsByType(modified.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) originChannelPermissions := append(originChannelViewOnlyPermissions, originChannelViewAndPostPermissions...) - modifiedChannelViewOnlyPermissions := modified.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) - modifiedChannelViewAndPostPermissions := modified.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) + modifiedChannelViewOnlyPermissions := modified.ChannelTokenPermissionsByType(modified.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) + modifiedChannelViewAndPostPermissions := modified.ChannelTokenPermissionsByType(modified.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) modifiedChannelPermissions := append(modifiedChannelViewOnlyPermissions, modifiedChannelViewAndPostPermissions...) 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 diff --git a/protocol/communities/community_encryption_key_action_test.go b/protocol/communities/community_encryption_key_action_test.go index bfde8426124..f7c11a954e7 100644 --- a/protocol/communities/community_encryption_key_action_test.go +++ b/protocol/communities/community_encryption_key_action_test.go @@ -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" ) @@ -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 @@ -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{}, @@ -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) } @@ -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)) diff --git a/protocol/communities/community_test.go b/protocol/communities/community_test.go index 81ac82b6dbb..d2fee8d434c 100644 --- a/protocol/communities/community_test.go +++ b/protocol/communities/community_test.go @@ -73,10 +73,10 @@ func (s *CommunitySuite) TestHasPermission() { memberKey, err := crypto.GenerateKey() s.Require().NoError(err) - s.Require().False(community.hasPermission(nil, adminRolePermissions())) + s.Require().False(community.hasRoles(nil, adminRoles())) // returns false if key is passed, but config is nil - s.Require().False(community.hasPermission(&nonMemberKey.PublicKey, adminRolePermissions())) + s.Require().False(community.hasRoles(&nonMemberKey.PublicKey, adminRoles())) // returns true if the user is the owner @@ -87,16 +87,16 @@ func (s *CommunitySuite) TestHasPermission() { community.config = &Config{ID: &ownerKey.PublicKey, CommunityDescription: communityDescription} - s.Require().True(community.hasPermission(&ownerKey.PublicKey, ownerRolePermission())) + s.Require().True(community.hasRoles(&ownerKey.PublicKey, ownerRoles())) // return false if user is not a member - s.Require().False(community.hasPermission(&nonMemberKey.PublicKey, adminRolePermissions())) + s.Require().False(community.hasRoles(&nonMemberKey.PublicKey, adminRoles())) // return true if user is a member and has permissions - s.Require().True(community.hasPermission(&memberKey.PublicKey, adminRolePermissions())) + s.Require().True(community.hasRoles(&memberKey.PublicKey, adminRoles())) // return false if user is a member and does not have permissions - s.Require().False(community.hasPermission(&memberKey.PublicKey, ownerRolePermission())) + s.Require().False(community.hasRoles(&memberKey.PublicKey, ownerRoles())) } diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index cc7d5665abf..a76f416fa1d 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -62,28 +62,28 @@ var ( ) type Manager struct { - persistence *Persistence - encryptor *encryption.Protocol - ensSubscription chan []*ens.VerificationRecord - subscriptions []chan *Subscription - ensVerifier *ens.Verifier - identity *ecdsa.PrivateKey - accountsManager account.Manager - tokenManager TokenManager - collectiblesManager CollectiblesManager - logger *zap.Logger - stdoutLogger *zap.Logger - transport *transport.Transport - quit chan struct{} - torrentConfig *params.TorrentConfig - torrentClient *torrent.Client - walletConfig *params.WalletConfig - historyArchiveTasksWaitGroup sync.WaitGroup - historyArchiveTasks sync.Map // stores `chan struct{}` - periodicMemberPermissionsTasks sync.Map // stores `chan struct{}` - torrentTasks map[string]metainfo.Hash - historyArchiveDownloadTasks map[string]*HistoryArchiveDownloadTask - stopped bool + persistence *Persistence + encryptor *encryption.Protocol + ensSubscription chan []*ens.VerificationRecord + subscriptions []chan *Subscription + ensVerifier *ens.Verifier + identity *ecdsa.PrivateKey + accountsManager account.Manager + tokenManager TokenManager + collectiblesManager CollectiblesManager + logger *zap.Logger + stdoutLogger *zap.Logger + transport *transport.Transport + quit chan struct{} + torrentConfig *params.TorrentConfig + torrentClient *torrent.Client + walletConfig *params.WalletConfig + historyArchiveTasksWaitGroup sync.WaitGroup + historyArchiveTasks sync.Map // stores `chan struct{}` + periodicMembersReevaluationTasks sync.Map // stores `chan struct{}` + torrentTasks map[string]metainfo.Hash + historyArchiveDownloadTasks map[string]*HistoryArchiveDownloadTask + stopped bool } type HistoryArchiveDownloadTask struct { @@ -614,16 +614,12 @@ func (m *Manager) EditCommunityTokenPermission(request *requests.EditCommunityTo return community, changes, nil } -func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool) error { +func (m *Manager) ReevaluateMembers(community *Community, removeAdmins bool) error { becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER) becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN) - adminPermissions := len(becomeAdminPermissions) > 0 - memberPermissions := len(becomeMemberPermissions) > 0 - - if !adminPermissions && !memberPermissions && !removeAdmins { - return nil - } + hasMemberPermissions := len(becomeMemberPermissions) > 0 + hasAdminPermissions := len(becomeAdminPermissions) > 0 for memberKey, member := range community.Members() { memberPubKey, err := common.HexToPubkey(memberKey) @@ -640,7 +636,7 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool // Check if user was not treated as an admin without wallet in open community // or user treated as a member without wallet in closed community - if (!memberHasWallet && isAdmin) || (memberPermissions && !memberHasWallet) { + if !memberHasWallet && (hasMemberPermissions || isAdmin) { _, err = community.RemoveUserFromOrg(memberPubKey) if err != nil { return err @@ -652,7 +648,7 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool // Check if user is still an admin or can become an admin and do update of member role removeAdminRole := false - if adminPermissions { + if hasAdminPermissions { permissionResponse, err := m.checkPermissionToJoin(becomeAdminPermissions, accountsAndChainIDs, true) if err != nil { return err @@ -677,21 +673,58 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool isAdmin = false } - // Skip further validation if user has admin permissions or we do not have member permissions - if isAdmin || !memberPermissions { + if isAdmin { + // Make sure admin is added to every channel + for channelID := range community.Chats() { + if !community.IsMemberInChat(memberPubKey, channelID) { + community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_ADMIN}) + } + } + // Skip further validation if user has admin permissions continue } - permissionResponse, err := m.checkPermissionToJoin(becomeMemberPermissions, accountsAndChainIDs, true) - if err != nil { - return err + if hasMemberPermissions { + permissionResponse, err := m.checkPermissionToJoin(becomeMemberPermissions, accountsAndChainIDs, true) + if err != nil { + return err + } + + if !permissionResponse.Satisfied { + _, err = community.RemoveUserFromOrg(memberPubKey) + if err != nil { + return err + } + // Skip channels validation if user has been removed + continue + } } - if !permissionResponse.Satisfied { - _, err = community.RemoveUserFromOrg(memberPubKey) + // Validate channel permissions + for channelID := range community.Chats() { + chatID := community.IDString() + channelID + + viewOnlyPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) + viewAndPostPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) + + if len(viewOnlyPermissions) == 0 && len(viewAndPostPermissions) == 0 { + continue + } + + response, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountsAndChainIDs, true) if err != nil { return err } + + isMemberAlreadyInChannel := community.IsMemberInChat(memberPubKey, channelID) + + if response.ViewOnlyPermissions.Satisfied || response.ViewAndPostPermissions.Satisfied { + if !isMemberAlreadyInChannel { + community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{}) + } + } else if isMemberAlreadyInChannel { + community.RemoveUserFromChat(memberPubKey, channelID) + } } } @@ -703,21 +736,13 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool return nil } -func (m *Manager) CheckIfStopCheckingPermissionsPeriodically(community *Community) { - if cancel, exists := m.periodicMemberPermissionsTasks.Load(community.IDString()); exists && - len(community.TokenPermissions()) == 0 { - close(cancel.(chan struct{})) // Need to cast to the chan - } -} - -func (m *Manager) CheckMemberPermissionsPeriodically(communityID types.HexBytes) { - - if _, exists := m.periodicMemberPermissionsTasks.Load(communityID.String()); exists { +func (m *Manager) ReevaluateMembersPeriodically(communityID types.HexBytes) { + if _, exists := m.periodicMembersReevaluationTasks.Load(communityID.String()); exists { return } cancel := make(chan struct{}) - m.periodicMemberPermissionsTasks.Store(communityID.String(), cancel) + m.periodicMembersReevaluationTasks.Store(communityID.String(), cancel) ticker := time.NewTicker(memberPermissionsCheckInterval) defer ticker.Stop() @@ -728,15 +753,15 @@ func (m *Manager) CheckMemberPermissionsPeriodically(communityID types.HexBytes) community, err := m.GetByID(communityID) if err != nil { m.logger.Debug("can't validate member permissions, community was not found", zap.Error(err)) - m.periodicMemberPermissionsTasks.Delete(communityID.String()) + m.periodicMembersReevaluationTasks.Delete(communityID.String()) } - err = m.CheckMemberPermissions(community, true) + err = m.ReevaluateMembers(community, true) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } case <-cancel: - m.periodicMemberPermissionsTasks.Delete(communityID.String()) + m.periodicMembersReevaluationTasks.Delete(communityID.String()) return } } @@ -1524,6 +1549,32 @@ func (m *Manager) accountsSatisfyPermissionsToJoin(community *Community, account return true, false, nil } +func (m *Manager) accountsSatisfyPermissionsToJoinChannels(community *Community, accounts []*protobuf.RevealedAccount) (map[string]*protobuf.CommunityChat, error) { + result := make(map[string]*protobuf.CommunityChat) + + accountsAndChainIDs := revealedAccountsToAccountsAndChainIDsCombination(accounts) + + for channelID, channel := range community.config.CommunityDescription.Chats { + channelViewOnlyPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) + channelViewAndPostPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) + channelPermissions := append(channelViewOnlyPermissions, channelViewAndPostPermissions...) + + if len(channelPermissions) > 0 { + permissionResponse, err := m.checkPermissions(channelPermissions, accountsAndChainIDs, true) + if err != nil { + return nil, err + } + if permissionResponse.Satisfied { + result[channelID] = channel + } + } else { + result[channelID] = channel + } + } + + return result, nil +} + func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommunity) (*Community, error) { dbRequest, err := m.persistence.GetRequestToJoin(request.ID) if err != nil { @@ -1564,6 +1615,18 @@ func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommu return nil, err } + channels, err := m.accountsSatisfyPermissionsToJoinChannels(community, revealedAccounts) + if err != nil { + return nil, err + } + + for channelID := range channels { + _, err = community.AddMemberToChat(channelID, pk, memberRoles) + if err != nil { + return nil, err + } + } + if err := m.markRequestToJoin(pk, community); err != nil { return nil, err } @@ -2797,8 +2860,17 @@ func (m *Manager) IsEncrypted(communityID string) (bool, error) { } return community.Encrypted(), nil +} +func (m *Manager) IsChannelEncrypted(communityID string, chatID string) (bool, error) { + community, err := m.GetByIDString(communityID) + if err != nil { + return false, err + } + + return community.ChannelHasTokenPermissions(chatID), nil } + func (m *Manager) ShouldHandleSyncCommunity(community *protobuf.SyncCommunity) (bool, error) { return m.persistence.ShouldHandleSyncCommunity(community) } diff --git a/protocol/communities_key_distributor.go b/protocol/communities_key_distributor.go index 19cc03094b5..60a548e5e22 100644 --- a/protocol/communities_key_distributor.go +++ b/protocol/communities_key_distributor.go @@ -29,9 +29,9 @@ func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Comm return err } - for chatID := range keyActions.ChannelKeysActions { - keyAction := keyActions.ChannelKeysActions[chatID] - err := ckd.distributeKey(community.ID(), []byte(chatID), &keyAction) + for channelID := range keyActions.ChannelKeysActions { + keyAction := keyActions.ChannelKeysActions[channelID] + err := ckd.distributeKey(community.ID(), []byte(community.IDString()+channelID), &keyAction) if err != nil { return err } @@ -83,6 +83,7 @@ func (ckd *CommunitiesKeyDistributorImpl) sendKeyExchangeMessage(communityID, ha CommunityKeyExMsgType: msgType, Recipients: pubkeys, MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE, + HashRatchetGroupID: hashRatchetGroupID, } _, err := ckd.sender.SendCommunityMessage(context.Background(), rawMessage) diff --git a/protocol/communities_messenger_token_permissions_test.go b/protocol/communities_messenger_token_permissions_test.go index 5895463d15c..0a35a8f559a 100644 --- a/protocol/communities_messenger_token_permissions_test.go +++ b/protocol/communities_messenger_token_permissions_test.go @@ -767,7 +767,156 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions( s.Require().NoError(err) // send message to channel - msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community") + msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community 2") + + // bob can read the message + response, err = WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + for _, message := range r.messages { + if message.Text == msg.Text { + return true + } + } + return false + }, + "no messages", + ) + s.Require().NoError(err) + s.Require().Len(response.Messages(), 1) + s.Require().Equal(msg.Text, response.Messages()[0].Text) +} + +func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions() { + community, chat := s.createCommunity() + + // bob joins the community + s.advertiseCommunityTo(community, s.bob) + s.joinCommunity(community, s.bob, bobPassword, []string{}) + + // send message to the channel + msg := s.sendChatMessage(s.owner, chat.ID, "hello on open community") + + // bob can read the message + response, err := WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + for _, message := range r.messages { + if message.Text == msg.Text { + return true + } + } + return false + }, + "no messages", + ) + s.Require().NoError(err) + s.Require().Len(response.Messages(), 1) + s.Require().Equal(msg.Text, response.Messages()[0].Text) + + // setup view channel permission + channelPermissionRequest := requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + ContractAddresses: map[uint64]string{testChainID1: "0x123"}, + Symbol: "TEST", + Amount: "100", + Decimals: uint64(18), + }, + }, + ChatIds: []string{chat.ID}, + } + + waitOnBobToBeKickedFromChannel := s.waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { + for channelID, channel := range sub.Community.Chats() { + if channelID == chat.CommunityChatID() && len(channel.Members) == 1 { + return true + } + } + return false + }) + waitOnChannelToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { + for channelID, action := range sub.keyActions.ChannelKeysActions { + if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeyRekey { + return true + } + } + return false + }) + + response, err = s.owner.CreateCommunityTokenPermission(&channelPermissionRequest) + s.Require().NoError(err) + s.Require().Len(response.Communities(), 1) + s.Require().True(s.owner.communitiesManager.IsChannelEncrypted(community.IDString(), chat.ID)) + + err = <-waitOnBobToBeKickedFromChannel + s.Require().NoError(err) + + err = <-waitOnChannelToBeRekeyedOnceBobIsKicked + s.Require().NoError(err) + + // send message to the channel + msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel") + + // bob can't read the message + _, err = WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + for _, message := range r.messages { + if message.Text == msg.Text { + return true + } + } + return false + }, + "no messages", + ) + s.Require().Error(err) + s.Require().ErrorContains(err, "no messages") + + // make bob satisfy channel criteria + s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, channelPermissionRequest.TokenCriteria[0]) + + waitOnChannelKeyToBeDistributedToBob := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { + for channelID, action := range sub.keyActions.ChannelKeysActions { + if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeySendToMembers { + for memberPubKey := range action.Members { + if memberPubKey == common.PubkeyToHex(&s.bob.identity.PublicKey) { + return true + } + + } + } + } + return false + }) + + // force owner to reevaluate channel members + // in production it will happen automatically, by periodic check + community, err = s.owner.communitiesManager.GetByID(community.ID()) + s.Require().NoError(err) + s.owner.communitiesManager.ReevaluateMembers(community, true) + + err = <-waitOnChannelKeyToBeDistributedToBob + s.Require().NoError(err) + + // ensure key is delivered to bob before message is sent + // FIXME: this step shouldn't be necessary as we store hash ratchet messages + // for later, to decrypt them when the key arrives. + // for some reason, without it, the test is flaky + WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + return false + }, + "", + ) + + // send message to the channel + msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel 2") // bob can read the message response, err = WaitOnMessengerResponse( diff --git a/protocol/messenger.go b/protocol/messenger.go index 0502c89cdad..de22af8fedd 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -810,7 +810,7 @@ func (m *Messenger) Start() (*MessengerResponse, error) { for _, c := range adminCommunities { if c.Joined() && c.HasTokenPermissions() { - go m.communitiesManager.CheckMemberPermissionsPeriodically(c.ID()) + go m.communitiesManager.ReevaluateMembersPeriodically(c.ID()) } } } @@ -2051,21 +2051,36 @@ func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMe } logger.Debug("sending community chat message", zap.String("chatName", chat.Name)) - isEncrypted, err := m.communitiesManager.IsEncrypted(chat.CommunityID) + isCommunityEncrypted, err := m.communitiesManager.IsEncrypted(chat.CommunityID) if err != nil { return rawMessage, err } + isChannelEncrypted, err := m.communitiesManager.IsChannelEncrypted(chat.CommunityID, chat.ID) + if err != nil { + return rawMessage, err + } + isEncrypted := isCommunityEncrypted || isChannelEncrypted if !isEncrypted { id, err = m.sender.SendPublic(ctx, chat.ID, rawMessage) + if err != nil { + return rawMessage, err + } } else { rawMessage.CommunityID, err = types.DecodeHex(chat.CommunityID) + if err != nil { + return rawMessage, err + } - if err == nil { - id, err = m.sender.SendCommunityMessage(ctx, rawMessage) + if isChannelEncrypted { + rawMessage.HashRatchetGroupID = []byte(chat.ID) + } else { + rawMessage.HashRatchetGroupID = rawMessage.CommunityID + } + + id, err = m.sender.SendCommunityMessage(ctx, rawMessage) + if err != nil { + return rawMessage, err } - } - if err != nil { - return rawMessage, err } case ChatTypePrivateGroupChat: logger.Debug("sending group message", zap.String("chatName", chat.Name)) diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index cb34078aaef..4195e4cf854 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -1698,12 +1698,12 @@ func (m *Messenger) CreateCommunityTokenPermission(request *requests.CreateCommu if community.IsOwner() { // check existing member permission once, then check periodically go func() { - err := m.communitiesManager.CheckMemberPermissions(community, true) + err := m.communitiesManager.ReevaluateMembers(community, true) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } - m.communitiesManager.CheckMemberPermissionsPeriodically(community.ID()) + m.communitiesManager.ReevaluateMembersPeriodically(community.ID()) }() } @@ -1730,7 +1730,7 @@ func (m *Messenger) EditCommunityTokenPermission(request *requests.EditCommunity // We do this in a separate routine to not block this function if community.IsOwner() { go func() { - err := m.communitiesManager.CheckMemberPermissions(community, true) + err := m.communitiesManager.ReevaluateMembers(community, true) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } @@ -1761,14 +1761,10 @@ func (m *Messenger) DeleteCommunityTokenPermission(request *requests.DeleteCommu becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN) // Make sure that we remove admins roles if we remove admin permissions - err = m.communitiesManager.CheckMemberPermissions(community, len(becomeAdminPermissions) == 0) + err = m.communitiesManager.ReevaluateMembers(community, len(becomeAdminPermissions) == 0) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } - - // Check if there's still permissions we need to track, - // if not we can stop checking token criteria on-chain - m.communitiesManager.CheckIfStopCheckingPermissionsPeriodically(community) }() }