diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 6478cb2b5..28e54c8ed 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -1547,6 +1547,10 @@ func (o *Community) HasTokenPermissions() bool { return len(o.tokenPermissions()) > 0 } +func (o *Community) ChannelEncrypted(channelID string) bool { + return o.ChannelHasTokenPermissions(o.IDString() + channelID) +} + func (o *Community) ChannelHasTokenPermissions(chatID string) bool { o.mutex.Lock() defer o.mutex.Unlock() diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index f4a1b4a4e..eff5597c7 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -3311,7 +3311,8 @@ func (m *Manager) IsChannelEncrypted(communityID string, chatID string) (bool, e return false, err } - return community.ChannelHasTokenPermissions(chatID), nil + channelID := strings.TrimPrefix(chatID, communityID) + return community.ChannelEncrypted(channelID), nil } func (m *Manager) ShouldHandleSyncCommunity(community *protobuf.SyncInstallationCommunity) (bool, error) { diff --git a/protocol/communities_key_distributor.go b/protocol/communities_key_distributor.go index 3e9c7a7ec..47147c6cb 100644 --- a/protocol/communities_key_distributor.go +++ b/protocol/communities_key_distributor.go @@ -12,7 +12,6 @@ import ( type CommunitiesKeyDistributor interface { Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error - Rekey(community *communities.Community) error } type CommunitiesKeyDistributorImpl struct { @@ -41,32 +40,6 @@ func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Comm return nil } -func (ckd *CommunitiesKeyDistributorImpl) Rekey(community *communities.Community) error { - if !community.IsControlNode() { - return communities.ErrNotControlNode - } - - err := ckd.distributeKey(community, community.ID(), &communities.EncryptionKeyAction{ - ActionType: communities.EncryptionKeyRekey, - Members: community.Members(), - }) - if err != nil { - return err - } - - for channelID, channel := range community.Chats() { - err := ckd.distributeKey(community, []byte(community.IDString()+channelID), &communities.EncryptionKeyAction{ - ActionType: communities.EncryptionKeyRekey, - Members: channel.Members, - }) - if err != nil { - return err - } - } - - return nil -} - func (ckd *CommunitiesKeyDistributorImpl) distributeKey(community *communities.Community, hashRatchetGroupID []byte, keyAction *communities.EncryptionKeyAction) error { pubkeys := make([]*ecdsa.PublicKey, len(keyAction.Members)) i := 0 @@ -77,15 +50,7 @@ func (ckd *CommunitiesKeyDistributorImpl) distributeKey(community *communities.C switch keyAction.ActionType { case communities.EncryptionKeyAdd: - _, err := ckd.encryptor.GenerateHashRatchetKey(hashRatchetGroupID) - if err != nil { - return err - } - - err = ckd.sendKeyExchangeMessage(community, hashRatchetGroupID, pubkeys, common.KeyExMsgReuse) - if err != nil { - return err - } + fallthrough case communities.EncryptionKeyRekey: err := ckd.sendKeyExchangeMessage(community, hashRatchetGroupID, pubkeys, common.KeyExMsgRekey) diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index 30b31ede2..1a7cedba7 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -63,7 +63,6 @@ func (s *MessengerCommunitiesSuite) SetupTest() { s.bob = s.newMessenger() s.alice = s.newMessenger() - enableRekeyLoop = true s.owner.communitiesManager.RekeyInterval = 50 * time.Millisecond _, err := s.owner.Start() @@ -3352,28 +3351,12 @@ func (t *testPermissionChecker) CheckPermissions(permissions []*communities.Comm } func (s *MessengerCommunitiesSuite) TestStartCommunityRekeyLoop() { - // Create a new community - response, err := s.owner.CreateCommunity( - &requests.CreateCommunity{ - Membership: protobuf.CommunityPermissions_AUTO_ACCEPT, - Name: "status", - Color: "#57a7e5", - Description: "status community description", - }, - true, - ) - s.Require().NoError(err) - s.Require().NotNil(response) - s.Require().Len(response.Communities(), 1) + community, chat := s.createCommunity() + s.Require().False(community.Encrypted()) - // Check community is present in the DB and has default values we care about - c, err := s.owner.GetCommunityByID(response.Communities()[0].ID()) - s.Require().NoError(err) - s.Require().False(c.Encrypted()) - // TODO some check that there are no keys for the community. Alt for s.Require().Zero(c.RekeyedAt().Unix()) - - _, err = s.owner.CreateCommunityTokenPermission(&requests.CreateCommunityTokenPermission{ - CommunityID: c.ID(), + // Add community permission + _, err := s.owner.CreateCommunityTokenPermission(&requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, TokenCriteria: []*protobuf.TokenCriteria{{ ContractAddresses: map[uint64]string{3: "0x933"}, @@ -3386,37 +3369,57 @@ func (s *MessengerCommunitiesSuite) TestStartCommunityRekeyLoop() { }) s.Require().NoError(err) - c, err = s.owner.GetCommunityByID(c.ID()) + // Add channel permission + response, err := s.owner.CreateCommunityTokenPermission(&requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + ContractAddresses: map[uint64]string{3: "0x933"}, + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "STT", + Name: "Status Test Token", + Amount: "10", + Decimals: 18, + }, + }, + ChatIds: []string{chat.ID}, + }) s.Require().NoError(err) - s.Require().True(c.Encrypted()) - - s.advertiseCommunityTo(c, s.owner, s.bob) - s.advertiseCommunityTo(c, s.owner, s.alice) + s.Require().Len(response.Communities(), 1) + community = response.Communities()[0] + s.Require().True(community.Encrypted()) + s.Require().True(community.ChannelEncrypted(chat.CommunityChatID())) s.owner.communitiesManager.PermissionChecker = &testPermissionChecker{} - s.joinCommunity(c, s.owner, s.bob) - s.joinCommunity(c, s.owner, s.alice) + s.advertiseCommunityTo(community, s.owner, s.bob) + s.advertiseCommunityTo(community, s.owner, s.alice) + s.joinCommunity(community, s.owner, s.bob) + s.joinCommunity(community, s.owner, s.alice) - // Check the Alice and Bob are members of the community - c, err = s.owner.GetCommunityByID(c.ID()) + // Check keys in the database + communityKeys, err := s.owner.sender.GetKeysForGroup(community.ID()) s.Require().NoError(err) - s.Require().True(c.HasMember(&s.alice.identity.PublicKey)) - s.Require().True(c.HasMember(&s.bob.identity.PublicKey)) + communityKeyCount := len(communityKeys) - // Check the keys in the database - keys, err := s.owner.sender.GetKeysForGroup(c.ID()) + channelKeys, err := s.owner.sender.GetKeysForGroup([]byte(chat.ID)) s.Require().NoError(err) - keyCount := len(keys) + channelKeyCount := len(channelKeys) // Check that rekeying is occurring by counting the number of keyIDs in the encryptor's DB // This test could be flaky, as the rekey function may not be finished before RekeyInterval * 2 has passed for i := 0; i < 5; i++ { time.Sleep(s.owner.communitiesManager.RekeyInterval * 2) - keys, err = s.owner.sender.GetKeysForGroup(c.ID()) + communityKeys, err = s.owner.sender.GetKeysForGroup(community.ID()) s.Require().NoError(err) - s.Require().Greater(len(keys), keyCount) - keyCount = len(keys) + s.Require().Greater(len(communityKeys), communityKeyCount) + communityKeyCount = len(communityKeys) + + channelKeys, err = s.owner.sender.GetKeysForGroup([]byte(chat.ID)) + s.Require().NoError(err) + s.Require().Greater(len(channelKeys), channelKeyCount) + channelKeyCount = len(channelKeys) } } diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 0d46f47bf..296062bab 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -37,7 +37,6 @@ import ( "github.com/status-im/status-go/protocol/communities" "github.com/status-im/status-go/protocol/communities/token" "github.com/status-im/status-go/protocol/discord" - "github.com/status-im/status-go/protocol/encryption" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/transport" @@ -511,6 +510,10 @@ func (m *Messenger) Communities() ([]*communities.Community, error) { return m.communitiesManager.All() } +func (m *Messenger) ControlledCommunities() ([]*communities.Community, error) { + return m.communitiesManager.Controlled() +} + func (m *Messenger) JoinedCommunities() ([]*communities.Community, error) { return m.communitiesManager.Joined() } @@ -2302,9 +2305,6 @@ func (m *Messenger) ImportCommunity(ctx context.Context, key *ecdsa.PrivateKey) return nil, err } - // TODO Init hash ratchet for community - _, err = m.encryptor.GenerateHashRatchetKey(community.ID()) - if err != nil { return nil, err } @@ -5733,33 +5733,8 @@ func chunkAttachmentsByByteSize(slice []*protobuf.DiscordMessageAttachment, maxF return chunks } -// GetCurrentKeyForGroup returns the latest key timestampID belonging to a key group -func (m *Messenger) GetCurrentKeyForGroup(groupID []byte) (*encryption.HashRatchetKeyCompatibility, error) { - return m.sender.GetCurrentKeyForGroup(groupID) -} - -// RekeyCommunity takes a communities.Community.config.ID and triggers a force rekey event for that community -func (m *Messenger) RekeyCommunity(cID types.HexBytes) error { - // Get the community as the member list could have changed - c, err := m.GetCommunityByID(cID) - if err != nil { - return err - } - - // RekeyCommunity - return m.communitiesKeyDistributor.Rekey(c) -} - -// NOTE: disabling rekey loop as it rekeys too aggressively - -var enableRekeyLoop = false - // startCommunityRekeyLoop creates a 5-minute ticker and starts a routine that attempts to rekey every community every tick func (m *Messenger) startCommunityRekeyLoop() { - if !enableRekeyLoop { - return - } - logger := m.logger.Named("CommunityRekeyLoop") var d time.Duration if m.communitiesManager.RekeyInterval != 0 { @@ -5777,7 +5752,7 @@ func (m *Messenger) startCommunityRekeyLoop() { for { select { case <-ticker.C: - m.rekeyAllCommunities(logger) + m.rekeyCommunities(logger) case <-m.quit: ticker.Stop() logger.Debug("CommunityRekeyLoop stopped") @@ -5787,47 +5762,60 @@ func (m *Messenger) startCommunityRekeyLoop() { }() } -// rekeyAllCommunities attempts to rekey every community in persistence. -// A community will be rekeyed if it meets all the following criteria: -// - Community.IsAdmin() -// - Community.Encrypted() -// - Community.RekeyedAt().Add(rki).Before(time.Now()) where rki is a defined rekey interval -func (m *Messenger) rekeyAllCommunities(logger *zap.Logger) { - // Determine the rekey interval, if the value is not set as a property of m.communitiesManager - // default to one hour +// rekeyCommunities loops over controlled communities and rekeys if rekey interval elapsed +func (m *Messenger) rekeyCommunities(logger *zap.Logger) { // TODO in future have a community level rki rather than a global rki - /* - var rki time.Duration - if m.communitiesManager.RekeyInterval == 0 { - rki = time.Hour - } else { - rki = m.communitiesManager.RekeyInterval - }*/ + var rekeyInterval time.Duration + if m.communitiesManager.RekeyInterval == 0 { + rekeyInterval = 48 * time.Hour + } else { + rekeyInterval = m.communitiesManager.RekeyInterval + } - // Get and loop over all communities in persistence - cs, err := m.Communities() + shouldRekey := func(hashRatchetGroupID []byte) bool { + key, err := m.sender.GetCurrentKeyForGroup(hashRatchetGroupID) + if err != nil { + logger.Error("failed to get current hash ratchet key", zap.Error(err)) + return false + } + + keyDistributedAt := time.UnixMilli(int64(key.Timestamp)) + return time.Now().After(keyDistributedAt.Add(rekeyInterval)) + } + + controlledCommunities, err := m.ControlledCommunities() if err != nil { logger.Error("error getting communities", zap.Error(err)) return } - for _, c := range cs { - if err != nil { - logger.Error("error getting current keyTimestampID for community", zap.Error(err), zap.Binary("community ID", c.ID())) - continue + + for _, c := range controlledCommunities { + keyActions := &communities.EncryptionKeyActions{ + CommunityKeyAction: communities.EncryptionKeyAction{}, + ChannelKeysActions: map[string]communities.EncryptionKeyAction{}, } - // TODO add functionality to encryptor.go that compares the timestamps and returns a bool - // c.RekeyedAt().Add(rki).Before(time.Now()) - // keyTimestampID + rki < time.Now() - // Just using the vars that will be used later - - if c.IsControlNode() && c.Encrypted() { // && c.RekeyedAt().Add(rki).Before(time.Now()) - err := m.RekeyCommunity(c.ID()) - if err != nil { - logger.Error("error sending rekey message", zap.Error(err), zap.Binary("community ID", c.ID())) - continue + if c.Encrypted() && shouldRekey(c.ID()) { + keyActions.CommunityKeyAction = communities.EncryptionKeyAction{ + ActionType: communities.EncryptionKeyRekey, + Members: c.Members(), } } + + for channelID, channel := range c.Chats() { + if c.ChannelEncrypted(channelID) && shouldRekey([]byte(c.IDString()+channelID)) { + keyActions.ChannelKeysActions[channelID] = communities.EncryptionKeyAction{ + ActionType: communities.EncryptionKeyRekey, + Members: channel.Members, + } + } + } + + err = m.communitiesKeyDistributor.Distribute(c, keyActions) + if err != nil { + logger.Error("failed to rekey community", zap.Error(err), zap.String("community ID", c.IDString())) + continue + } } }