feat: introduce CommunitiesKeyDistributor

This component decouples key distribution from the Messenger, enhancing
code maintainability, extensibility and testability.
It also alleviates the need to impact all methods potentially affecting
encryption keys.
Moreover, it allows key distribution inspection for integration tests.

part of: status-im/status-desktop#10998
This commit is contained in:
Patryk Osmaczko 2023-07-17 18:40:09 +02:00 committed by osmaczko
parent fa5b316324
commit 30da8390bd
10 changed files with 431 additions and 213 deletions

View File

@ -449,6 +449,7 @@ func (o *Community) GetMemberPubkeys() []*ecdsa.PublicKey {
} }
return nil return nil
} }
func (o *Community) initialize() { func (o *Community) initialize() {
if o.config.CommunityDescription == nil { if o.config.CommunityDescription == nil {
o.config.CommunityDescription = &protobuf.CommunityDescription{} o.config.CommunityDescription = &protobuf.CommunityDescription{}
@ -987,10 +988,6 @@ func (o *Community) Encrypted() bool {
return o.config.CommunityDescription.Encrypted return o.config.CommunityDescription.Encrypted
} }
func (o *Community) SetEncrypted(encrypted bool) {
o.config.CommunityDescription.Encrypted = encrypted
}
func (o *Community) Joined() bool { func (o *Community) Joined() bool {
return o.config.Joined return o.config.Joined
} }
@ -1430,6 +1427,10 @@ func includes(channelIDs []string, channelID string) bool {
return false return false
} }
func (o *Community) updateEncrypted() {
o.config.CommunityDescription.Encrypted = len(o.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER)) > 0
}
func (o *Community) AddTokenPermission(permission *protobuf.CommunityTokenPermission) (*CommunityChanges, error) { func (o *Community) AddTokenPermission(permission *protobuf.CommunityTokenPermission) (*CommunityChanges, error) {
o.mutex.Lock() o.mutex.Lock()
defer o.mutex.Unlock() defer o.mutex.Unlock()
@ -1454,6 +1455,7 @@ func (o *Community) AddTokenPermission(permission *protobuf.CommunityTokenPermis
} }
if isControlNode { if isControlNode {
o.updateEncrypted()
o.increaseClock() o.increaseClock()
} }
@ -1484,6 +1486,7 @@ func (o *Community) UpdateTokenPermission(permissionID string, tokenPermission *
} }
if isControlNode { if isControlNode {
o.updateEncrypted()
o.increaseClock() o.increaseClock()
} }
@ -1520,6 +1523,7 @@ func (o *Community) DeleteTokenPermission(permissionID string) (*CommunityChange
} }
if isControlNode { if isControlNode {
o.updateEncrypted()
o.increaseClock() o.increaseClock()
} }
@ -1924,7 +1928,7 @@ func (o *Community) AddMemberWithRevealedAccounts(dbRequest *RequestToJoin, role
return changes, nil return changes, nil
} }
func (o *Community) createDeepCopy() *Community { func (o *Community) CreateDeepCopy() *Community {
return &Community{ return &Community{
config: &Config{ config: &Config{
PrivateKey: o.config.PrivateKey, PrivateKey: o.config.PrivateKey,

View File

@ -26,6 +26,24 @@ type EncryptionKeyActions struct {
} }
func EvaluateCommunityEncryptionKeyActions(origin, modified *Community) *EncryptionKeyActions { func EvaluateCommunityEncryptionKeyActions(origin, modified *Community) *EncryptionKeyActions {
if origin == nil {
// `modified` is a new community, create empty `origin` community
origin = &Community{
config: &Config{
CommunityDescription: &protobuf.CommunityDescription{
Members: map[string]*protobuf.CommunityMember{},
Permissions: &protobuf.CommunityPermissions{},
Identity: &protobuf.ChatIdentity{},
Chats: map[string]*protobuf.CommunityChat{},
Categories: map[string]*protobuf.CommunityCategory{},
AdminSettings: &protobuf.CommunityAdminSettings{},
TokenPermissions: map[string]*protobuf.CommunityTokenPermission{},
CommunityTokensMetadata: []*protobuf.CommunityTokenMetadata{},
},
},
}
}
changes := EvaluateCommunityChanges(origin.Description(), modified.Description()) changes := EvaluateCommunityChanges(origin.Description(), modified.Description())
result := &EncryptionKeyActions{ result := &EncryptionKeyActions{

View File

@ -365,7 +365,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestCommunityLevelKeyActions_Permiss
for _, tc := range testCases { for _, tc := range testCases {
s.Run(tc.name, func() { s.Run(tc.name, func() {
origin := createTestCommunity(s.identity) origin := createTestCommunity(s.identity)
modified := origin.createDeepCopy() modified := origin.CreateDeepCopy()
for _, permission := range tc.originPermissions { for _, permission := range tc.originPermissions {
_, err := origin.AddTokenPermission(permission) _, err := origin.AddTokenPermission(permission)
@ -498,7 +498,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestCommunityLevelKeyActions_Members
_, err := origin.AddTokenPermission(permission) _, err := origin.AddTokenPermission(permission)
s.Require().NoError(err) s.Require().NoError(err)
} }
modified := origin.createDeepCopy() modified := origin.CreateDeepCopy()
for _, member := range tc.originMembers { for _, member := range tc.originMembers {
_, err := origin.AddMember(member, []protobuf.CommunityMember_Roles{}) _, err := origin.AddMember(member, []protobuf.CommunityMember_Roles{})
@ -595,7 +595,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestCommunityLevelKeyActions_Permiss
for _, tc := range testCases { for _, tc := range testCases {
s.Run(tc.name, func() { s.Run(tc.name, func() {
origin := createTestCommunity(s.identity) origin := createTestCommunity(s.identity)
modified := origin.createDeepCopy() modified := origin.CreateDeepCopy()
for _, permission := range tc.originPermissions { for _, permission := range tc.originPermissions {
_, err := origin.AddTokenPermission(permission) _, err := origin.AddTokenPermission(permission)
@ -743,7 +743,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
}) })
s.Require().NoError(err) s.Require().NoError(err)
modified := origin.createDeepCopy() modified := origin.CreateDeepCopy()
for _, permission := range tc.originPermissions { for _, permission := range tc.originPermissions {
_, err := origin.AddTokenPermission(permission) _, err := origin.AddTokenPermission(permission)
@ -779,3 +779,40 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
}) })
} }
} }
func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() {
newCommunity := createTestCommunity(s.identity)
chatID := "0x1234"
_, err := newCommunity.CreateChat(chatID, &protobuf.CommunityChat{
Members: map[string]*protobuf.CommunityMember{},
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP},
Identity: &protobuf.ChatIdentity{},
})
s.Require().NoError(err)
newCommunityPermissions := []*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},
},
}
for _, permission := range newCommunityPermissions {
_, err := newCommunity.AddTokenPermission(permission)
s.Require().NoError(err)
}
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)
}

View File

@ -188,7 +188,7 @@ func (o *Community) UpdateCommunityByEvents(communityEventMessage *CommunityEven
} }
// Create a deep copy of current community so we can update CommunityDescription by new admin events // Create a deep copy of current community so we can update CommunityDescription by new admin events
copy := o.createDeepCopy() copy := o.CreateDeepCopy()
// Merge community admin events to existing community. Admin events must be stored to the db // Merge community admin events to existing community. Admin events must be stored to the db
// during saving the community // during saving the community

View File

@ -283,10 +283,6 @@ type Subscription struct {
DownloadingHistoryArchivesFinishedSignal *signal.DownloadingHistoryArchivesFinishedSignal DownloadingHistoryArchivesFinishedSignal *signal.DownloadingHistoryArchivesFinishedSignal
ImportingHistoryArchiveMessagesSignal *signal.ImportingHistoryArchiveMessagesSignal ImportingHistoryArchiveMessagesSignal *signal.ImportingHistoryArchiveMessagesSignal
CommunityEventsMessage *CommunityEventsMessage CommunityEventsMessage *CommunityEventsMessage
MemberPermissionsCheckedSignal *MemberPermissionsCheckedSignal
}
type MemberPermissionsCheckedSignal struct {
} }
type CommunityResponse struct { type CommunityResponse struct {
@ -700,10 +696,11 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool
} }
} }
m.publish(&Subscription{ err := m.saveAndPublish(community)
Community: community, if err != nil {
MemberPermissionsCheckedSignal: &MemberPermissionsCheckedSignal{}, return err
}) }
return nil return nil
} }

View File

@ -0,0 +1,120 @@
package protocol
import (
"context"
"crypto/ecdsa"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/encryption"
"github.com/status-im/status-go/protocol/protobuf"
)
type CommunitiesKeyDistributor interface {
Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error
Rekey(community *communities.Community) error
}
type CommunitiesKeyDistributorImpl struct {
sender *common.MessageSender
encryptor *encryption.Protocol
}
func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error {
if !community.IsControlNode() {
return communities.ErrNotControlNode
}
err := ckd.distributeKey(community.ID(), community.ID(), &keyActions.CommunityKeyAction)
if err != nil {
return err
}
for chatID := range keyActions.ChannelKeysActions {
keyAction := keyActions.ChannelKeysActions[chatID]
err := ckd.distributeKey(community.ID(), []byte(chatID), &keyAction)
if err != nil {
return err
}
}
return nil
}
func (ckd *CommunitiesKeyDistributorImpl) Rekey(community *communities.Community) error {
if !community.IsControlNode() {
return communities.ErrNotControlNode
}
err := ckd.distributeKey(community.ID(), 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.ID(), []byte(community.IDString()+channelID), &communities.EncryptionKeyAction{
ActionType: communities.EncryptionKeyRekey,
Members: channel.Members,
})
if err != nil {
return err
}
}
return nil
}
func (ckd *CommunitiesKeyDistributorImpl) distributeKey(communityID, hashRatchetGroupID []byte, keyAction *communities.EncryptionKeyAction) error {
pubkeys := make([]*ecdsa.PublicKey, len(keyAction.Members))
i := 0
for hex := range keyAction.Members {
pubkeys[i], _ = common.HexToPubkey(hex)
i++
}
switch keyAction.ActionType {
case communities.EncryptionKeyAdd:
_, err := ckd.encryptor.GenerateHashRatchetKey(hashRatchetGroupID)
if err != nil {
return err
}
err = ckd.sendKeyExchangeMessage(communityID, hashRatchetGroupID, pubkeys, common.KeyExMsgReuse)
if err != nil {
return err
}
case communities.EncryptionKeyRekey:
err := ckd.sendKeyExchangeMessage(communityID, hashRatchetGroupID, pubkeys, common.KeyExMsgRekey)
if err != nil {
return err
}
case communities.EncryptionKeySendToMembers:
err := ckd.sendKeyExchangeMessage(communityID, hashRatchetGroupID, pubkeys, common.KeyExMsgReuse)
if err != nil {
return err
}
}
return nil
}
func (ckd *CommunitiesKeyDistributorImpl) sendKeyExchangeMessage(communityID, hashRatchetGroupID []byte, pubkeys []*ecdsa.PublicKey, msgType common.CommKeyExMsgType) error {
rawMessage := common.RawMessage{
SkipProtocolLayer: false,
CommunityID: communityID,
CommunityKeyExMsgType: msgType,
Recipients: pubkeys,
MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE,
}
_, err := ckd.sender.SendCommunityMessage(context.Background(), rawMessage)
if err != nil {
return err
}
return nil
}

View File

@ -3450,47 +3450,49 @@ func (s *MessengerCommunitiesSuite) TestStartCommunityRekeyLoop() {
s.Require().False(c.Encrypted()) s.Require().False(c.Encrypted())
// TODO some check that there are no keys for the community. Alt for s.Require().Zero(c.RekeyedAt().Unix()) // TODO some check that there are no keys for the community. Alt for s.Require().Zero(c.RekeyedAt().Unix())
// Update the community to use encryption and check the values // FIXME recover me once the key_id issue is resolved
err = s.admin.UpdateCommunityEncryption(c, true) /*
s.Require().NoError(err) // Update the community to use encryption and check the values
err = s.admin.UpdateCommunityEncryption(c, true)
c, err = s.admin.GetCommunityByID(c.ID())
s.Require().NoError(err)
s.Require().True(c.Encrypted())
// Add Alice and Bob to the community
response, err = s.admin.InviteUsersToCommunity(
&requests.InviteUsersToCommunity{
CommunityID: c.ID(),
Users: []types.HexBytes{
common.PubkeyToHexBytes(&s.alice.identity.PublicKey),
common.PubkeyToHexBytes(&s.bob.identity.PublicKey),
},
},
)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
// Check the Alice and Bob are members of the community
c, err = s.admin.GetCommunityByID(c.ID())
s.Require().NoError(err)
s.Require().True(c.HasMember(&s.alice.identity.PublicKey))
s.Require().True(c.HasMember(&s.bob.identity.PublicKey))
// TODO reinstate once the key_id issue is resolved
// Check the keys in the database
/*keys, err := s.admin.sender.GetKeyIDsForGroup(c.ID())
s.Require().NoError(err)
keyCount := len(keys)*/
// 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.admin.communitiesManager.RekeyInterval * 2)
keys, err = s.admin.sender.GetKeyIDsForGroup(c.ID())
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Greater(len(keys), keyCount)
keyCount = len(keys) c, err = s.admin.GetCommunityByID(c.ID())
}*/ s.Require().NoError(err)
s.Require().True(c.Encrypted())
// Add Alice and Bob to the community
response, err = s.admin.InviteUsersToCommunity(
&requests.InviteUsersToCommunity{
CommunityID: c.ID(),
Users: []types.HexBytes{
common.PubkeyToHexBytes(&s.alice.identity.PublicKey),
common.PubkeyToHexBytes(&s.bob.identity.PublicKey),
},
},
)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
// Check the Alice and Bob are members of the community
c, err = s.admin.GetCommunityByID(c.ID())
s.Require().NoError(err)
s.Require().True(c.HasMember(&s.alice.identity.PublicKey))
s.Require().True(c.HasMember(&s.bob.identity.PublicKey))
// Check the keys in the database
keys, err := s.admin.sender.GetKeyIDsForGroup(c.ID())
s.Require().NoError(err)
keyCount := len(keys)
// 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.admin.communitiesManager.RekeyInterval * 2)
keys, err = s.admin.sender.GetKeyIDsForGroup(c.ID())
s.Require().NoError(err)
s.Require().Greater(len(keys), keyCount)
keyCount = len(keys)
}
*/
} }

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"math/big" "math/big"
"sync"
"testing" "testing"
"time" "time"
@ -76,6 +77,77 @@ func (m *TokenManagerMock) GetBalancesByChain(ctx context.Context, accounts, tok
return *m.Balances, nil return *m.Balances, nil
} }
type CommunityAndKeyActions struct {
community *communities.Community
keyActions *communities.EncryptionKeyActions
}
type TestCommunitiesKeyDistributor struct {
CommunitiesKeyDistributorImpl
subscriptions map[chan *CommunityAndKeyActions]bool
mutex sync.RWMutex
}
func (tckd *TestCommunitiesKeyDistributor) Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error {
err := tckd.CommunitiesKeyDistributorImpl.Distribute(community, keyActions)
if err != nil {
return err
}
// notify distribute finished
tckd.mutex.RLock()
for s := range tckd.subscriptions {
s <- &CommunityAndKeyActions{
community: community,
keyActions: keyActions,
}
}
tckd.mutex.RUnlock()
return nil
}
func (tckd *TestCommunitiesKeyDistributor) waitOnKeyDistribution(condition func(*CommunityAndKeyActions) bool) <-chan error {
errCh := make(chan error, 1)
subscription := make(chan *CommunityAndKeyActions)
tckd.mutex.Lock()
tckd.subscriptions[subscription] = true
tckd.mutex.Unlock()
go func() {
defer func() {
close(errCh)
tckd.mutex.Lock()
delete(tckd.subscriptions, subscription)
tckd.mutex.Unlock()
close(subscription)
}()
for {
select {
case s, more := <-subscription:
if !more {
errCh <- errors.New("channel closed when waiting for key distribution")
return
}
if condition(s) {
return
}
case <-time.After(500 * time.Millisecond):
errCh <- errors.New("timed out when waiting for key distribution")
return
}
}
}()
return errCh
}
func TestMessengerCommunitiesTokenPermissionsSuite(t *testing.T) { func TestMessengerCommunitiesTokenPermissionsSuite(t *testing.T) {
suite.Run(t, new(MessengerCommunitiesTokenPermissionsSuite)) suite.Run(t, new(MessengerCommunitiesTokenPermissionsSuite))
} }
@ -143,6 +215,14 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) newMessenger(password string
messenger, err := newCommunitiesTestMessenger(s.shh, privateKey, s.logger, accountsManagerMock, tokenManagerMock) messenger, err := newCommunitiesTestMessenger(s.shh, privateKey, s.logger, accountsManagerMock, tokenManagerMock)
s.Require().NoError(err) s.Require().NoError(err)
currentDistributorObj, ok := messenger.communitiesKeyDistributor.(*CommunitiesKeyDistributorImpl)
s.Require().True(ok)
messenger.communitiesKeyDistributor = &TestCommunitiesKeyDistributor{
CommunitiesKeyDistributorImpl: *currentDistributorObj,
subscriptions: map[chan *CommunityAndKeyActions]bool{},
mutex: sync.RWMutex{},
}
// add wallet account with keypair // add wallet account with keypair
for _, walletAddress := range walletAddresses { for _, walletAddress := range walletAddresses {
kp := accounts.GetProfileKeypairForTest(false, true, false) kp := accounts.GetProfileKeypairForTest(false, true, false)
@ -225,11 +305,10 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) waitOnCommunitiesEvent(user
return errCh return errCh
} }
func (s *MessengerCommunitiesTokenPermissionsSuite) waitOnCommunityEncryption(community *communities.Community) <-chan error { func (s *MessengerCommunitiesTokenPermissionsSuite) waitOnKeyDistribution(condition func(*CommunityAndKeyActions) bool) <-chan error {
s.Require().False(community.Encrypted()) testCommunitiesKeyDistributor, ok := s.owner.communitiesKeyDistributor.(*TestCommunitiesKeyDistributor)
return s.waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { s.Require().True(ok)
return sub.Community != nil && sub.Community.IDString() == community.IDString() && sub.Community.Encrypted() return testCommunitiesKeyDistributor.waitOnKeyDistribution(condition)
})
} }
func (s *MessengerCommunitiesTokenPermissionsSuite) TestCreateTokenPermission() { func (s *MessengerCommunitiesTokenPermissionsSuite) TestCreateTokenPermission() {
@ -607,15 +686,28 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions(
}, },
} }
waitOnCommunityEncryptionErrCh := s.waitOnCommunityEncryption(community) waitOnBobToBeKicked := s.waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool {
return len(sub.Community.Members()) == 1
})
waitOnCommunityToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool {
return len(sub.community.Description().Members) == 1 &&
sub.keyActions.CommunityKeyAction.ActionType == communities.EncryptionKeyRekey
})
response, err = s.owner.CreateCommunityTokenPermission(&permissionRequest) response, err = s.owner.CreateCommunityTokenPermission(&permissionRequest)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.Communities(), 1) s.Require().Len(response.Communities(), 1)
err = <-waitOnCommunityEncryptionErrCh err = <-waitOnBobToBeKicked
s.Require().NoError(err) s.Require().NoError(err)
// bob should be kicked from the community,
// because he doesn't meet the criteria
community, err = s.owner.communitiesManager.GetByID(community.ID())
s.Require().NoError(err)
s.Require().Len(community.Members(), 1)
// bob receives community changes
_, err = WaitOnMessengerResponse( _, err = WaitOnMessengerResponse(
s.bob, s.bob,
func(r *MessengerResponse) bool { func(r *MessengerResponse) bool {
@ -625,11 +717,8 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions(
) )
s.Require().NoError(err) s.Require().NoError(err)
// bob should be kicked from the community, err = <-waitOnCommunityToBeRekeyedOnceBobIsKicked
// because he doesn't meet the criteria
community, err = s.owner.communitiesManager.GetByID(community.ID())
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(community.Members(), 1)
// send message to channel // 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")
@ -665,9 +754,18 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions(
// make bob satisfy the criteria // make bob satisfy the criteria
s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, permissionRequest.TokenCriteria[0]) s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, permissionRequest.TokenCriteria[0])
waitOnCommunityKeyToBeDistributedToBob := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool {
return len(sub.community.Description().Members) == 2 &&
len(sub.keyActions.CommunityKeyAction.Members) == 1 &&
sub.keyActions.CommunityKeyAction.ActionType == communities.EncryptionKeySendToMembers
})
// bob re-joins the community // bob re-joins the community
s.joinCommunity(community, s.bob, bobPassword, []string{}) s.joinCommunity(community, s.bob, bobPassword, []string{})
err = <-waitOnCommunityKeyToBeDistributedToBob
s.Require().NoError(err)
// send message to channel // 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")

View File

@ -96,24 +96,25 @@ var messageCacheIntervalMs uint64 = 1000 * 60 * 60 * 48
// Similarly, it needs to expose an interface to manage // Similarly, it needs to expose an interface to manage
// mailservers because they can also be managed by the user. // mailservers because they can also be managed by the user.
type Messenger struct { type Messenger struct {
node types.Node node types.Node
server *p2p.Server server *p2p.Server
peerStore *mailservers.PeerStore peerStore *mailservers.PeerStore
config *config config *config
identity *ecdsa.PrivateKey identity *ecdsa.PrivateKey
persistence *sqlitePersistence persistence *sqlitePersistence
transport *transport.Transport transport *transport.Transport
encryptor *encryption.Protocol encryptor *encryption.Protocol
sender *common.MessageSender sender *common.MessageSender
ensVerifier *ens.Verifier ensVerifier *ens.Verifier
anonMetricsClient *anonmetrics.Client anonMetricsClient *anonmetrics.Client
anonMetricsServer *anonmetrics.Server anonMetricsServer *anonmetrics.Server
pushNotificationClient *pushnotificationclient.Client pushNotificationClient *pushnotificationclient.Client
pushNotificationServer *pushnotificationserver.Server pushNotificationServer *pushnotificationserver.Server
communitiesManager *communities.Manager communitiesManager *communities.Manager
accountsManager account.Manager communitiesKeyDistributor CommunitiesKeyDistributor
mentionsManager *MentionManager accountsManager account.Manager
logger *zap.Logger mentionsManager *MentionManager
logger *zap.Logger
outputCSV bool outputCSV bool
csvFile *os.File csvFile *os.File
@ -471,19 +472,23 @@ func NewMessenger(
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
messenger = &Messenger{ messenger = &Messenger{
config: &c, config: &c,
node: node, node: node,
identity: identity, identity: identity,
persistence: sqlitePersistence, persistence: sqlitePersistence,
transport: transp, transport: transp,
encryptor: encryptionProtocol, encryptor: encryptionProtocol,
sender: sender, sender: sender,
anonMetricsClient: anonMetricsClient, anonMetricsClient: anonMetricsClient,
anonMetricsServer: anonMetricsServer, anonMetricsServer: anonMetricsServer,
telemetryClient: telemetryClient, telemetryClient: telemetryClient,
pushNotificationClient: pushNotificationClient, pushNotificationClient: pushNotificationClient,
pushNotificationServer: pushNotificationServer, pushNotificationServer: pushNotificationServer,
communitiesManager: communitiesManager, communitiesManager: communitiesManager,
communitiesKeyDistributor: &CommunitiesKeyDistributorImpl{
sender: sender,
encryptor: encryptionProtocol,
},
accountsManager: accountsManager, accountsManager: accountsManager,
ensVerifier: ensVerifier, ensVerifier: ensVerifier,
featureFlags: c.featureFlags, featureFlags: c.featureFlags,

View File

@ -210,11 +210,44 @@ func (m *Messenger) handleCommunitiesHistoryArchivesSubscription(c chan *communi
// handleCommunitiesSubscription handles events from communities // handleCommunitiesSubscription handles events from communities
func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscription) { func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscription) {
var lastPublished int64 var lastPublished int64
// We check every 5 minutes if we need to publish // We check every 5 minutes if we need to publish
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
recentlyPublishedOrgs := func() map[string]*communities.Community {
result := make(map[string]*communities.Community)
ownedOrgs, err := m.communitiesManager.Created()
if err != nil {
m.logger.Warn("failed to retrieve orgs", zap.Error(err))
return result
}
for _, org := range ownedOrgs {
result[org.IDString()] = org
}
return result
}()
publishOrgAndDistributeEncryptionKeys := func(community *communities.Community) {
err := m.publishOrg(community)
if err != nil {
m.logger.Warn("failed to publish org", zap.Error(err))
return
}
m.logger.Debug("published org")
// evaluate and distribute encryption keys (if any)
encryptionKeyActions := communities.EvaluateCommunityEncryptionKeyActions(recentlyPublishedOrgs[community.IDString()], community)
err = m.communitiesKeyDistributor.Distribute(community, encryptionKeyActions)
if err != nil {
m.logger.Warn("failed to distribute encryption keys", zap.Error(err))
}
recentlyPublishedOrgs[community.IDString()] = community.CreateDeepCopy()
}
go func() { go func() {
for { for {
select { select {
@ -223,10 +256,7 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti
return return
} }
if sub.Community != nil { if sub.Community != nil {
err := m.publishOrg(sub.Community) publishOrgAndDistributeEncryptionKeys(sub.Community)
if err != nil {
m.logger.Warn("failed to publish org", zap.Error(err))
}
for _, invitation := range sub.Invitations { for _, invitation := range sub.Invitations {
err := m.publishOrgInvitation(sub.Community, invitation) err := m.publishOrgInvitation(sub.Community, invitation)
@ -234,16 +264,6 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti
m.logger.Warn("failed to publish org invitation", zap.Error(err)) m.logger.Warn("failed to publish org invitation", zap.Error(err))
} }
} }
if sub.MemberPermissionsCheckedSignal != nil {
becomeMemberPermissions := sub.Community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER)
err := m.UpdateCommunityEncryption(sub.Community, len(becomeMemberPermissions) > 0)
if err != nil {
m.logger.Warn("failed to update community encryption", zap.Error(err))
}
}
m.logger.Debug("published org")
} }
if sub.CommunityEventsMessage != nil { if sub.CommunityEventsMessage != nil {
@ -273,10 +293,7 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti
org := orgs[idx] org := orgs[idx]
_, beingImported := m.importingCommunities[org.IDString()] _, beingImported := m.importingCommunities[org.IDString()]
if !beingImported { if !beingImported {
err := m.publishOrg(org) publishOrgAndDistributeEncryptionKeys(org)
if err != nil {
m.logger.Warn("failed to publish org", zap.Error(err))
}
} }
} }
@ -1252,11 +1269,6 @@ func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequest
return nil, err return nil, err
} }
err = m.SendKeyExchangeMessage(community.ID(), []*ecdsa.PublicKey{pk}, common.KeyExMsgReuse)
if err != nil {
return nil, err
}
rawMessage := &common.RawMessage{ rawMessage := &common.RawMessage{
Payload: payload, Payload: payload,
Sender: community.PrivateKey(), Sender: community.PrivateKey(),
@ -1909,11 +1921,6 @@ func (m *Messenger) InviteUsersToCommunity(request *requests.InviteUsersToCommun
} }
} }
err = m.SendKeyExchangeMessage(community.ID(), publicKeys, common.KeyExMsgReuse)
if err != nil {
return nil, err
}
community, err = m.communitiesManager.InviteUsersToCommunity(request.CommunityID, publicKeys) community, err = m.communitiesManager.InviteUsersToCommunity(request.CommunityID, publicKeys)
if err != nil { if err != nil {
return nil, err return nil, err
@ -2018,34 +2025,6 @@ func (m *Messenger) RemoveUserFromCommunity(id types.HexBytes, pkString string)
return response, nil return response, nil
} }
func (m *Messenger) SendKeyExchangeMessage(communityID []byte, pubkeys []*ecdsa.PublicKey, msgType common.CommKeyExMsgType) error {
rawMessage := common.RawMessage{
SkipProtocolLayer: false,
CommunityID: communityID,
CommunityKeyExMsgType: msgType,
Recipients: pubkeys,
MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE,
}
_, err := m.sender.SendCommunityMessage(context.Background(), rawMessage)
if err != nil {
return err
}
return nil
}
// 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.SendKeyExchangeMessage(c.ID(), c.GetMemberPubkeys(), common.KeyExMsgRekey)
}
func (m *Messenger) UnbanUserFromCommunity(request *requests.UnbanUserFromCommunity) (*MessengerResponse, error) { func (m *Messenger) UnbanUserFromCommunity(request *requests.UnbanUserFromCommunity) (*MessengerResponse, error) {
community, err := m.communitiesManager.UnbanUserFromCommunity(request) community, err := m.communitiesManager.UnbanUserFromCommunity(request)
if err != nil { if err != nil {
@ -2063,13 +2042,6 @@ func (m *Messenger) BanUserFromCommunity(request *requests.BanUserFromCommunity)
return nil, err return nil, err
} }
if community.Encrypted() {
err = m.SendKeyExchangeMessage(community.ID(), community.GetMemberPubkeys(), common.KeyExMsgRekey)
if err != nil {
return nil, err
}
}
response := &MessengerResponse{} response := &MessengerResponse{}
response, err = m.DeclineAllPendingGroupInvitesFromUser(response, request.User.String()) response, err = m.DeclineAllPendingGroupInvitesFromUser(response, request.User.String())
if err != nil { if err != nil {
@ -4084,49 +4056,6 @@ func (m *Messenger) RemoveCommunityToken(chainID int, contractAddress string) er
return m.communitiesManager.RemoveCommunityToken(chainID, contractAddress) return m.communitiesManager.RemoveCommunityToken(chainID, contractAddress)
} }
// UpdateCommunityEncryption takes a communityID string and an encryption state, then finds the community and
// encrypts / decrypts the community. 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, useEncryption bool) error {
if community == nil {
return errors.New("community is nil")
}
// Check isEncrypted is different to Community's value
// If not different return
if community.Encrypted() == useEncryption {
return nil
}
if useEncryption {
// 🪄 The magic that encrypts a community
_, err := m.encryptor.GenerateHashRatchetKey(community.ID())
if err != nil {
return err
}
err = m.SendKeyExchangeMessage(community.ID(), community.GetMemberPubkeys(), common.KeyExMsgReuse)
if err != nil {
return err
}
}
// 🧙 There is no magic that decrypts a community, we just need to tell everyone to not use encryption
// Republish the community.
community.SetEncrypted(useEncryption)
err := m.communitiesManager.UpdateCommunity(community)
if err != nil {
return err
}
return nil
}
func (m *Messenger) CheckPermissionsToJoinCommunity(request *requests.CheckPermissionToJoinCommunity) (*communities.CheckPermissionToJoinResponse, error) { func (m *Messenger) CheckPermissionsToJoinCommunity(request *requests.CheckPermissionToJoinCommunity) (*communities.CheckPermissionToJoinResponse, error) {
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
return nil, err return nil, err
@ -4232,6 +4161,18 @@ func (m *Messenger) GetCurrentKeyForGroup(groupID []byte) (uint32, error) {
return m.sender.GetCurrentKeyForGroup(groupID) 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)
}
var rekeyCommunities = false var rekeyCommunities = false
// startCommunityRekeyLoop creates a 5-minute ticker and starts a routine that attempts to rekey every community every tick // startCommunityRekeyLoop creates a 5-minute ticker and starts a routine that attempts to rekey every community every tick
@ -4253,10 +4194,6 @@ func (m *Messenger) startCommunityRekeyLoop() {
d = 5 * time.Minute d = 5 * time.Minute
} }
if d > 0 { // Always return
return
}
ticker := time.NewTicker(d) ticker := time.NewTicker(d)
go func() { go func() {
for { for {