From 57b2432290347f15e0e452e6a38d76acef601bc5 Mon Sep 17 00:00:00 2001 From: mprakhov Date: Fri, 5 May 2023 00:17:54 +0200 Subject: [PATCH] Community encryption should automatically be enabled/disabled depending on community permissions (closed/open) --- protocol/communities/community.go | 5 +- protocol/communities/manager.go | 14 +++ protocol/communities_messenger_test.go | 22 ---- protocol/encryption/encryptor.go | 2 +- protocol/encryption/persistence.go | 4 +- protocol/messenger_communities.go | 108 +++++++++++++----- protocol/requests/create_community_request.go | 3 +- 7 files changed, 101 insertions(+), 57 deletions(-) diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 344de5636..3b421610b 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -969,8 +969,8 @@ func (o *Community) Encrypted() bool { return o.config.CommunityDescription.Encrypted } -func (o *Community) Encrypt() { - o.config.CommunityDescription.Encrypted = true +func (o *Community) SetEncrypted(encrypted bool) { + o.config.CommunityDescription.Encrypted = encrypted } func (o *Community) Joined() bool { @@ -1468,6 +1468,7 @@ func (o *Community) AddTokenPermission(permission *protobuf.CommunityTokenPermis } o.config.CommunityDescription.TokenPermissions[permission.Id] = permission + o.increaseClock() changes := o.emptyCommunityChanges() diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 81f0b42cc..058ea7c85 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -3334,3 +3334,17 @@ func (m *Manager) SetCommunityActiveMembersCount(communityID string, activeMembe return nil } + +// UpdateCommunity takes a Community persists it and republishes it. +// The clock is incremented meaning even a no change update will be republished by the admin, and parsed by the member. +func (m *Manager) UpdateCommunity(c *Community) error { + c.increaseClock() + + err := m.persistence.SaveCommunity(c) + if err != nil { + return err + } + + m.publish(&Subscription{Community: c}) + return nil +} diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index 9d1642730..1a14e5239 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - "github.com/golang/protobuf/proto" "github.com/google/uuid" "github.com/stretchr/testify/suite" "go.uber.org/zap" @@ -28,7 +27,6 @@ import ( "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/communities" "github.com/status-im/status-go/protocol/discord" - "github.com/status-im/status-go/protocol/encryption" "github.com/status-im/status-go/protocol/encryption/multidevice" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" @@ -640,7 +638,6 @@ func (s *MessengerCommunitiesSuite) TestPostToCommunityChat() { Name: "status", Color: "#ffffff", Description: "status community description", - Encrypted: true, } // Create an community chat @@ -2578,7 +2575,6 @@ func (s *MessengerCommunitiesSuite) TestSyncCommunity() { createCommunityReq := &requests.CreateCommunity{ Membership: protobuf.CommunityPermissions_ON_REQUEST, Name: "new community", - Encrypted: true, Color: "#000000", Description: "new community description", } @@ -2593,15 +2589,6 @@ func (s *MessengerCommunitiesSuite) TestSyncCommunity() { } s.Require().NotNil(newCommunity) - // Check HR keys are created - encodedKeys, err := s.alice.encryptor.GetAllHREncodedKeys(newCommunity.ID()) - s.Require().NoError(err) - s.Require().NotNil(encodedKeys) - - keys := &encryption.HRKeys{} - s.Require().NoError(proto.Unmarshal(encodedKeys, keys)) - s.Require().Len(keys.Keys, 1) - // Check that Alice has 2 communities cs, err := s.alice.communitiesManager.All() s.Require().NoError(err, "communitiesManager.All") @@ -2629,15 +2616,6 @@ func (s *MessengerCommunitiesSuite) TestSyncCommunity() { s.Require().NoError(err) s.Len(tcs, 2, "There must be 2 communities") - // Check HR keys are synced - encodedKeys, err = alicesOtherDevice.encryptor.GetAllHREncodedKeys(newCommunity.ID()) - s.Require().NoError(err) - s.Require().NotNil(encodedKeys) - - keys = &encryption.HRKeys{} - s.Require().NoError(proto.Unmarshal(encodedKeys, keys)) - s.Require().Len(keys.Keys, 1) - s.logger.Debug("", zap.Any("tcs", tcs)) // Get the new community from their db diff --git a/protocol/encryption/encryptor.go b/protocol/encryption/encryptor.go index e0e01e7f8..5ba55064a 100644 --- a/protocol/encryption/encryptor.go +++ b/protocol/encryption/encryptor.go @@ -623,7 +623,7 @@ func (s *encryptor) getNextHashRatchetKeyID(groupID []byte) (uint32, error) { return latestKeyID + 1, nil } -// Generates and stores a hash ratchet key given a group ID +// GenerateHashRatchetKey Generates and stores a hash ratchet key given a group ID func (s *encryptor) GenerateHashRatchetKey(groupID []byte) (uint32, error) { // Randomly generate a hash ratchet key diff --git a/protocol/encryption/persistence.go b/protocol/encryption/persistence.go index cd67f7fd6..89a810a91 100644 --- a/protocol/encryption/persistence.go +++ b/protocol/encryption/persistence.go @@ -790,7 +790,7 @@ func (s *sqlitePersistence) GetHashRatchetKeyByID(groupID []byte, keyID uint32, } } -// GetCurrentKeyIDForGroup retrieves a key ID for given group ID +// GetCurrentKeyForGroup retrieves a key ID for given group ID // (with an assumption that key ids are shared in the group, and // at any given time there is a single key used) func (s *sqlitePersistence) GetCurrentKeyForGroup(groupID []byte) (uint32, error) { @@ -845,7 +845,7 @@ func (s *sqlitePersistence) GetKeyIDsForGroup(groupID []byte) ([]uint32, error) return keyIDs, nil } -// SaveHashRachetKeyHash saves a hash ratchet key cache data +// SaveHashRatchetKeyHash saves a hash ratchet key cache data func (s *sqlitePersistence) SaveHashRatchetKeyHash( groupID []byte, keyID uint32, diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 2105d3e7a..bc409ff42 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ecdsa" "encoding/json" + "errors" _errors "errors" "fmt" "os" @@ -1238,15 +1239,6 @@ func (m *Messenger) CreateCommunity(request *requests.CreateCommunity, createDef response.AddChat(chatResponse.Chats()[0]) } - if request.Encrypted { - // Init hash ratchet for community - _, err = m.encryptor.GenerateHashRatchetKey(community.ID()) - - if err != nil { - return nil, err - } - } - response.AddCommunity(community) response.AddCommunitySettings(&communitySettings) err = m.syncCommunity(context.Background(), community, m.dispatchMessage) @@ -1271,8 +1263,16 @@ func (m *Messenger) CreateCommunityTokenPermission(request *requests.CreateCommu return nil, err } - response := &MessengerResponse{} - response.AddCommunity(community) + response, err := m.UpdateCommunityEncryption(community) + if err != nil { + return nil, err + } + + if response == nil { + response = &MessengerResponse{} + response.AddCommunity(community) + } + response.CommunityChanges = []*communities.CommunityChanges{changes} return response, nil @@ -1288,8 +1288,16 @@ func (m *Messenger) EditCommunityTokenPermission(request *requests.EditCommunity return nil, err } - response := &MessengerResponse{} - response.AddCommunity(community) + response, err := m.UpdateCommunityEncryption(community) + if err != nil { + return nil, err + } + + if response == nil { + response = &MessengerResponse{} + response.AddCommunity(community) + } + response.CommunityChanges = []*communities.CommunityChanges{changes} return response, nil @@ -1305,8 +1313,16 @@ func (m *Messenger) DeleteCommunityTokenPermission(request *requests.DeleteCommu return nil, err } - response := &MessengerResponse{} - response.AddCommunity(community) + response, err := m.UpdateCommunityEncryption(community) + if err != nil { + return nil, err + } + + if response == nil { + response = &MessengerResponse{} + response.AddCommunity(community) + } + response.CommunityChanges = []*communities.CommunityChanges{changes} return response, nil } @@ -2624,19 +2640,6 @@ func (m *Messenger) RequestImportDiscordCommunity(request *requests.ImportDiscor return } - if createCommunityRequest.Encrypted { - // Init hash ratchet for community - _, err = m.encryptor.GenerateHashRatchetKey(discordCommunity.ID()) - - if err != nil { - m.cleanUpImport(discordCommunity.IDString()) - importProgress.AddTaskError(discord.CommunityCreationTask, discord.Error(err.Error())) - importProgress.StopTask(discord.CommunityCreationTask) - progressUpdates <- importProgress - return - } - } - communityID := discordCommunity.IDString() // marking import as not cancelled @@ -3536,3 +3539,52 @@ func (m *Messenger) AddCommunityToken(token *communities.CommunityToken) (*commu func (m *Messenger) UpdateCommunityTokenState(contractAddress string, deployState communities.DeployState) error { return m.communitiesManager.UpdateCommunityTokenState(contractAddress, deployState) } + +// UpdateCommunityEncryption takes a community and encrypts / decrypts the community +// based on TokenPermission. Community is republished along with any keys if needed. +// +// Note: This function cannot decrypt previously encrypted messages, and it cannot encrypt previous unencrypted messages. +// This functionality introduces some race conditions: +// - community description is processed by members before the receiving the key exchange messages +// - members maybe sending encrypted messages after the community description is updated and a new member joins +func (m *Messenger) UpdateCommunityEncryption(community *communities.Community) (*MessengerResponse, error) { + if community == nil { + return nil, errors.New("community is nil") + } + + becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER) + isEncrypted := len(becomeMemberPermissions) > 0 + + // Check isEncrypted is different to Community's value + // If not different return + if community.Encrypted() == isEncrypted { + return nil, nil + } + + if isEncrypted { + // 🪄 The magic that encrypts a community + _, err := m.encryptor.GenerateHashRatchetKey(community.ID()) + if err != nil { + return nil, err + } + + err = m.SendKeyExchangeMessage(community.ID(), community.GetMemberPubkeys(), common.KeyExMsgReuse) + if err != nil { + return nil, err + } + } + + // 🧙 There is no magic that decrypts a community, we just need to tell everyone to not use encryption + + // Republish the community. + community.SetEncrypted(isEncrypted) + err := m.communitiesManager.UpdateCommunity(community) + if err != nil { + return nil, err + } + + response := &MessengerResponse{} + response.AddCommunity(community) + + return response, nil +} diff --git a/protocol/requests/create_community_request.go b/protocol/requests/create_community_request.go index b17ac67e3..5d6b453c6 100644 --- a/protocol/requests/create_community_request.go +++ b/protocol/requests/create_community_request.go @@ -42,7 +42,6 @@ type CreateCommunity struct { Banner images.CroppedImage `json:"banner"` HistoryArchiveSupportEnabled bool `json:"historyArchiveSupportEnabled,omitempty"` PinMessageAllMembersEnabled bool `json:"pinMessageAllMembersEnabled,omitempty"` - Encrypted bool `json:"encrypted,omitempty"` Tags []string `json:"tags,omitempty"` } @@ -129,7 +128,7 @@ func (c *CreateCommunity) ToCommunityDescription() (*protobuf.CommunityDescripti }, IntroMessage: c.IntroMessage, OutroMessage: c.OutroMessage, - Encrypted: c.Encrypted, + Encrypted: false, Tags: c.Tags, } return description, nil