feat: introduce channel-level encryption

- distribute ratchet keys at both community and channel levels
- use explicit `HashRatchetGroupID` in ecryption layer, instead of
  inheriting `groupID` from `CommunityID`
- populate `HashRatchetGroupID` with `CommunityID+ChannelID` for
  channels, and `CommunityID` for whole community
- hydrate channels with members; channel members are now subset of
  community members
- include channel permissions in periodic permissions check

closes: status-im/status-desktop#10998
This commit is contained in:
Patryk Osmaczko 2023-06-23 12:49:26 +02:00 committed by osmaczko
parent 30da8390bd
commit 367b7722d1
10 changed files with 405 additions and 112 deletions

View File

@ -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
}

View File

@ -35,4 +35,5 @@ type RawMessage struct {
CommunityKeyExMsgType CommKeyExMsgType
Ephemeral bool
BeforeDispatch func(*RawMessage) error
HashRatchetGroupID []byte
}

View File

@ -681,6 +681,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)
@ -736,18 +750,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) {
@ -905,17 +908,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) {
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()
}
@ -931,8 +944,7 @@ 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) {
@ -943,11 +955,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()
}
@ -1328,16 +1351,20 @@ func (o *Community) ToBytes() ([]byte, error) {
}
func (o *Community) Chats() map[string]*protobuf.CommunityChat {
response := make(map[string]*protobuf.CommunityChat)
// Why are we checking here for nil, it should be the responsibility of the caller
if o == nil {
return response
return make(map[string]*protobuf.CommunityChat)
}
o.mutex.Lock()
defer o.mutex.Unlock()
return o.chats()
}
func (o *Community) chats() map[string]*protobuf.CommunityChat {
response := make(map[string]*protobuf.CommunityChat)
if o.config != nil && o.config.CommunityDescription != nil {
for k, v := range o.config.CommunityDescription.Chats {
response[k] = v
@ -1391,7 +1418,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 {
@ -2064,6 +2105,8 @@ func (o *Community) createChat(chatID string, chat *protobuf.CommunityChat) erro
}
}
chat.Members = o.config.CommunityDescription.Members
o.config.CommunityDescription.Chats[chatID] = chat
return nil

View File

@ -63,7 +63,9 @@ 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 {
for channelID := range modified.config.CommunityDescription.Chats {
chatID := modified.IDString() + channelID
originChannelViewOnlyPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
originChannelViewAndPostPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
originChannelPermissions := append(originChannelViewOnlyPermissions, originChannelViewAndPostPermissions...)
@ -75,13 +77,13 @@ func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, chang
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

View File

@ -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))
@ -783,8 +785,10 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() {
newCommunity := createTestCommunity(s.identity)
chatID := "0x1234"
_, err := newCommunity.CreateChat(chatID, &protobuf.CommunityChat{
channelID := "0x1234"
chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID
_, err := newCommunity.CreateChat(channelID, &protobuf.CommunityChat{
Members: map[string]*protobuf.CommunityMember{},
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP},
Identity: &protobuf.ChatIdentity{},
@ -813,6 +817,6 @@ func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() {
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)
s.Require().NotNil(actions.ChannelKeysActions[channelID])
s.Require().Equal(actions.ChannelKeysActions[channelID].ActionType, EncryptionKeyAdd)
}

View File

@ -62,29 +62,29 @@ 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
RekeyInterval time.Duration
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
RekeyInterval time.Duration
}
type HistoryArchiveDownloadTask struct {
@ -615,16 +615,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)
@ -641,7 +637,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
@ -653,7 +649,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
@ -678,21 +674,67 @@ 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) {
_, err = community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_ADMIN})
if err != nil {
return err
}
}
}
// Skip further validation if user has admin permissions
continue
}
permissionResponse, err := m.checkPermissionToJoin(becomeMemberPermissions, accountsAndChainIDs, true)
if err != nil {
return err
}
if !permissionResponse.Satisfied {
_, err = community.RemoveUserFromOrg(memberPubKey)
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
}
}
// 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 {
_, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{})
if err != nil {
return err
}
}
} else if isMemberAlreadyInChannel {
_, err := community.RemoveUserFromChat(memberPubKey, channelID)
if err != nil {
return err
}
}
}
}
@ -704,21 +746,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()
@ -729,15 +763,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
}
}
@ -1525,6 +1559,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 {
@ -1565,6 +1625,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
}
@ -2798,8 +2870,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)
}

View File

@ -30,9 +30,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
}
@ -110,6 +110,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)

View File

@ -767,7 +767,157 @@ 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)
err = s.owner.communitiesManager.ReevaluateMembers(community, true)
s.Require().NoError(err)
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(

View File

@ -822,7 +822,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())
}
}
}
@ -2063,21 +2063,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 {
id, err = m.sender.SendCommunityMessage(ctx, rawMessage)
if err != nil {
return rawMessage, err
}
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))

View File

@ -1697,12 +1697,12 @@ func (m *Messenger) CreateCommunityTokenPermission(request *requests.CreateCommu
if community.IsControlNode() {
// 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())
}()
}
@ -1729,7 +1729,7 @@ func (m *Messenger) EditCommunityTokenPermission(request *requests.EditCommunity
// We do this in a separate routine to not block this function
if community.IsControlNode() {
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))
}
@ -1760,14 +1760,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)
}()
}