feat(communities)_: introduce bloom filter members list

iterates: status-im/status-desktop#15064
This commit is contained in:
Patryk Osmaczko 2024-06-27 17:29:03 +02:00 committed by osmaczko
parent 1715defec8
commit cb20c4c64a
13 changed files with 770 additions and 449 deletions

View File

@ -52,7 +52,7 @@ type Config struct {
Logger *zap.Logger Logger *zap.Logger
RequestedToJoinAt uint64 RequestedToJoinAt uint64
RequestsToJoin []*RequestToJoin RequestsToJoin []*RequestToJoin
MemberIdentity *ecdsa.PublicKey MemberIdentity *ecdsa.PrivateKey
EventsData *EventsData EventsData *EventsData
Shard *shard.Shard Shard *shard.Shard
PubsubTopicPrivateKey *ecdsa.PrivateKey PubsubTopicPrivateKey *ecdsa.PrivateKey
@ -115,6 +115,7 @@ type CommunityChat struct {
CategoryID string `json:"categoryID"` CategoryID string `json:"categoryID"`
TokenGated bool `json:"tokenGated"` TokenGated bool `json:"tokenGated"`
HideIfPermissionsNotMet bool `json:"hideIfPermissionsNotMet"` HideIfPermissionsNotMet bool `json:"hideIfPermissionsNotMet"`
MissingEncryptionKey bool `json:"missingEncryptionKey"`
} }
type CommunityCategory struct { type CommunityCategory struct {
@ -189,15 +190,15 @@ func (o *Community) MarshalPublicAPIJSON() ([]byte, error) {
for id, c := range o.config.CommunityDescription.Chats { for id, c := range o.config.CommunityDescription.Chats {
// NOTE: Here `CanPost` is only set for ChatMessage and Emoji reactions. But it can be different for pin/etc. // NOTE: Here `CanPost` is only set for ChatMessage and Emoji reactions. But it can be different for pin/etc.
// Consider adding more properties to `CommunityChat` to reflect that. // Consider adding more properties to `CommunityChat` to reflect that.
canPost, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE) canPost, err := o.CanPost(o.MemberIdentity(), id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil { if err != nil {
return nil, err return nil, err
} }
canPostReactions, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_EMOJI_REACTION) canPostReactions, err := o.CanPost(o.MemberIdentity(), id, protobuf.ApplicationMetadataMessage_EMOJI_REACTION)
if err != nil { if err != nil {
return nil, err return nil, err
} }
canView := o.CanView(o.config.MemberIdentity, id) canView := o.CanView(o.MemberIdentity(), id)
chat := CommunityChat{ chat := CommunityChat{
ID: id, ID: id,
@ -314,10 +315,10 @@ func (o *Community) MarshalJSONWithMediaServer(mediaServer *server.MediaServer)
Joined: o.config.Joined, Joined: o.config.Joined,
JoinedAt: o.config.JoinedAt, JoinedAt: o.config.JoinedAt,
Spectated: o.config.Spectated, Spectated: o.config.Spectated,
CanRequestAccess: o.CanRequestAccess(o.config.MemberIdentity), CanRequestAccess: o.CanRequestAccess(o.MemberIdentity()),
CanJoin: o.canJoin(), CanJoin: o.canJoin(),
CanManageUsers: o.CanManageUsers(o.config.MemberIdentity), CanManageUsers: o.CanManageUsers(o.MemberIdentity()),
CanDeleteMessageForEveryone: o.CanDeleteMessageForEveryone(o.config.MemberIdentity), CanDeleteMessageForEveryone: o.CanDeleteMessageForEveryone(o.MemberIdentity()),
RequestedToJoinAt: o.RequestedToJoinAt(), RequestedToJoinAt: o.RequestedToJoinAt(),
IsMember: o.isMember(), IsMember: o.isMember(),
Muted: o.config.Muted, Muted: o.config.Muted,
@ -342,15 +343,15 @@ func (o *Community) MarshalJSONWithMediaServer(mediaServer *server.MediaServer)
for id, c := range o.config.CommunityDescription.Chats { for id, c := range o.config.CommunityDescription.Chats {
// NOTE: Here `CanPost` is only set for ChatMessage. But it can be different for reactions/pin/etc. // NOTE: Here `CanPost` is only set for ChatMessage. But it can be different for reactions/pin/etc.
// Consider adding more properties to `CommunityChat` to reflect that. // Consider adding more properties to `CommunityChat` to reflect that.
canPost, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE) canPost, err := o.CanPost(o.MemberIdentity(), id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil { if err != nil {
return nil, err return nil, err
} }
canPostReactions, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_EMOJI_REACTION) canPostReactions, err := o.CanPost(o.MemberIdentity(), id, protobuf.ApplicationMetadataMessage_EMOJI_REACTION)
if err != nil { if err != nil {
return nil, err return nil, err
} }
canView := o.CanView(o.config.MemberIdentity, id) canView := o.CanView(o.MemberIdentity(), id)
chat := CommunityChat{ chat := CommunityChat{
ID: id, ID: id,
@ -368,6 +369,7 @@ func (o *Community) MarshalJSONWithMediaServer(mediaServer *server.MediaServer)
CategoryID: c.CategoryId, CategoryID: c.CategoryId,
HideIfPermissionsNotMet: c.HideIfPermissionsNotMet, HideIfPermissionsNotMet: c.HideIfPermissionsNotMet,
Position: int(c.Position), Position: int(c.Position),
MissingEncryptionKey: !o.IsMemberInChat(o.MemberIdentity(), id) && o.IsMemberLikelyInChat(id),
} }
communityItem.Chats[id] = chat communityItem.Chats[id] = chat
} }
@ -466,10 +468,10 @@ func (o *Community) MarshalJSON() ([]byte, error) {
Joined: o.config.Joined, Joined: o.config.Joined,
JoinedAt: o.config.JoinedAt, JoinedAt: o.config.JoinedAt,
Spectated: o.config.Spectated, Spectated: o.config.Spectated,
CanRequestAccess: o.CanRequestAccess(o.config.MemberIdentity), CanRequestAccess: o.CanRequestAccess(o.MemberIdentity()),
CanJoin: o.canJoin(), CanJoin: o.canJoin(),
CanManageUsers: o.CanManageUsers(o.config.MemberIdentity), CanManageUsers: o.CanManageUsers(o.MemberIdentity()),
CanDeleteMessageForEveryone: o.CanDeleteMessageForEveryone(o.config.MemberIdentity), CanDeleteMessageForEveryone: o.CanDeleteMessageForEveryone(o.MemberIdentity()),
RequestedToJoinAt: o.RequestedToJoinAt(), RequestedToJoinAt: o.RequestedToJoinAt(),
IsMember: o.isMember(), IsMember: o.isMember(),
Muted: o.config.Muted, Muted: o.config.Muted,
@ -494,15 +496,15 @@ func (o *Community) MarshalJSON() ([]byte, error) {
for id, c := range o.config.CommunityDescription.Chats { for id, c := range o.config.CommunityDescription.Chats {
// NOTE: Here `CanPost` is only set for ChatMessage. But it can be different for reactions/pin/etc. // NOTE: Here `CanPost` is only set for ChatMessage. But it can be different for reactions/pin/etc.
// Consider adding more properties to `CommunityChat` to reflect that. // Consider adding more properties to `CommunityChat` to reflect that.
canPost, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE) canPost, err := o.CanPost(o.MemberIdentity(), id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil { if err != nil {
return nil, err return nil, err
} }
canPostReactions, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_EMOJI_REACTION) canPostReactions, err := o.CanPost(o.MemberIdentity(), id, protobuf.ApplicationMetadataMessage_EMOJI_REACTION)
if err != nil { if err != nil {
return nil, err return nil, err
} }
canView := o.CanView(o.config.MemberIdentity, id) canView := o.CanView(o.MemberIdentity(), id)
chat := CommunityChat{ chat := CommunityChat{
ID: id, ID: id,
@ -519,6 +521,7 @@ func (o *Community) MarshalJSON() ([]byte, error) {
CategoryID: c.CategoryId, CategoryID: c.CategoryId,
HideIfPermissionsNotMet: c.HideIfPermissionsNotMet, HideIfPermissionsNotMet: c.HideIfPermissionsNotMet,
Position: int(c.Position), Position: int(c.Position),
MissingEncryptionKey: !o.IsMemberInChat(o.MemberIdentity(), id) && o.IsMemberLikelyInChat(id),
} }
if chat.TokenGated { if chat.TokenGated {
@ -898,6 +901,32 @@ func (o *Community) IsMemberInChat(pk *ecdsa.PublicKey, chatID string) bool {
return o.getChatMember(pk, chatID) != nil return o.getChatMember(pk, chatID) != nil
} }
// Uses bloom filter members list to estimate presence in the channel.
// False positive rate is 0.1%.
func (o *Community) IsMemberLikelyInChat(chatID string) bool {
if o.IsControlNode() || o.IsPrivilegedMember(o.MemberIdentity()) || !o.channelEncrypted(chatID) {
return true
}
chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok {
return false
}
// For communities controlled by clients that haven't updated to newer version yet we assume no membership.
if chat.MembersList == nil {
return false
}
res, err := verifyMembershipWithBloomFilter(chat.MembersList, o.config.MemberIdentity, o.ControlNode(), chatID, o.Clock())
if err != nil {
o.config.Logger.Error("failed to estimate membership", zap.Error(err))
return false
}
return res
}
func (o *Community) RemoveUserFromChat(pk *ecdsa.PublicKey, chatID string) (*protobuf.CommunityDescription, error) { func (o *Community) RemoveUserFromChat(pk *ecdsa.PublicKey, chatID string) (*protobuf.CommunityDescription, error) {
o.mutex.Lock() o.mutex.Lock()
defer o.mutex.Unlock() defer o.mutex.Unlock()
@ -975,7 +1004,7 @@ func (o *Community) RemoveUserFromOrg(pk *ecdsa.PublicKey) (*protobuf.CommunityD
func (o *Community) RemoveAllUsersFromOrg() *CommunityChanges { func (o *Community) RemoveAllUsersFromOrg() *CommunityChanges {
o.increaseClock() o.increaseClock()
myPublicKey := common.PubkeyToHex(o.config.MemberIdentity) myPublicKey := common.PubkeyToHex(o.MemberIdentity())
member := o.config.CommunityDescription.Members[myPublicKey] member := o.config.CommunityDescription.Members[myPublicKey]
membersToRemove := o.config.CommunityDescription.Members membersToRemove := o.config.CommunityDescription.Members
@ -1274,7 +1303,7 @@ func (o *Community) MuteTill() time.Time {
} }
func (o *Community) MemberIdentity() *ecdsa.PublicKey { func (o *Community) MemberIdentity() *ecdsa.PublicKey {
return o.config.MemberIdentity return &o.config.MemberIdentity.PublicKey
} }
// UpdateCommunityDescription will update the community to the new community description and return a list of changes // UpdateCommunityDescription will update the community to the new community description and return a list of changes
@ -1412,15 +1441,15 @@ func (o *Community) IsControlNode() bool {
} }
func (o *Community) IsOwner() bool { func (o *Community) IsOwner() bool {
return o.IsMemberOwner(o.config.MemberIdentity) return o.IsMemberOwner(o.MemberIdentity())
} }
func (o *Community) IsTokenMaster() bool { func (o *Community) IsTokenMaster() bool {
return o.IsMemberTokenMaster(o.config.MemberIdentity) return o.IsMemberTokenMaster(o.MemberIdentity())
} }
func (o *Community) IsAdmin() bool { func (o *Community) IsAdmin() bool {
return o.IsMemberAdmin(o.config.MemberIdentity) return o.IsMemberAdmin(o.MemberIdentity())
} }
func (o *Community) GetPrivilegedMembers() []*ecdsa.PublicKey { func (o *Community) GetPrivilegedMembers() []*ecdsa.PublicKey {
@ -1460,15 +1489,15 @@ func (o *Community) GetFilteredPrivilegedMembers(skipMembers map[string]struct{}
} }
func (o *Community) HasPermissionToSendCommunityEvents() bool { func (o *Community) HasPermissionToSendCommunityEvents() bool {
return !o.IsControlNode() && o.hasRoles(o.config.MemberIdentity, manageCommunityRoles()) return !o.IsControlNode() && o.hasRoles(o.MemberIdentity(), manageCommunityRoles())
} }
func (o *Community) hasPermissionToSendCommunityEvent(event protobuf.CommunityEvent_EventType) bool { func (o *Community) hasPermissionToSendCommunityEvent(event protobuf.CommunityEvent_EventType) bool {
return !o.IsControlNode() && canRolesPerformEvent(o.rolesOf(o.config.MemberIdentity), event) return !o.IsControlNode() && canRolesPerformEvent(o.rolesOf(o.MemberIdentity()), event)
} }
func (o *Community) hasPermissionToSendTokenPermissionCommunityEvent(event protobuf.CommunityEvent_EventType, permissionType protobuf.CommunityTokenPermission_Type) bool { func (o *Community) hasPermissionToSendTokenPermissionCommunityEvent(event protobuf.CommunityEvent_EventType, permissionType protobuf.CommunityTokenPermission_Type) bool {
roles := o.rolesOf(o.config.MemberIdentity) roles := o.rolesOf(o.MemberIdentity())
return !o.IsControlNode() && canRolesPerformEvent(roles, event) && canRolesModifyPermission(roles, permissionType) return !o.IsControlNode() && canRolesPerformEvent(roles, event) && canRolesModifyPermission(roles, permissionType)
} }
@ -1672,6 +1701,11 @@ func (o *Community) marshaledDescription() ([]byte, error) {
// see https://github.com/status-im/status-desktop/issues/12188 // see https://github.com/status-im/status-desktop/issues/12188
dehydrateChannelsMembers(clone) dehydrateChannelsMembers(clone)
err := generateBloomFiltersForChannels(clone, o.PrivateKey())
if err != nil {
o.config.Logger.Error("failed to generate bloom filters", zap.Error(err))
}
if o.encryptor != nil { if o.encryptor != nil {
err := encryptDescription(o.encryptor, o, clone) err := encryptDescription(o.encryptor, o, clone)
if err != nil { if err != nil {
@ -2256,11 +2290,11 @@ func (o *Community) CanDeleteMessageForEveryone(pk *ecdsa.PublicKey) bool {
} }
func (o *Community) isMember() bool { func (o *Community) isMember() bool {
return o.hasMember(o.config.MemberIdentity) return o.hasMember(o.MemberIdentity())
} }
func (o *Community) CanMemberIdentityPost(chatID string, messageType protobuf.ApplicationMetadataMessage_Type) (bool, error) { func (o *Community) CanMemberIdentityPost(chatID string, messageType protobuf.ApplicationMetadataMessage_Type) (bool, error) {
return o.CanPost(o.config.MemberIdentity, chatID, messageType) return o.CanPost(o.MemberIdentity(), chatID, messageType)
} }
// CanJoin returns whether a user can join the community, only if it's // CanJoin returns whether a user can join the community, only if it's

View File

@ -0,0 +1,106 @@
package communities
import (
"crypto/ecdsa"
"encoding/binary"
"errors"
"math/bits"
"github.com/bits-and-blooms/bloom/v3"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/encryption"
"github.com/status-im/status-go/protocol/protobuf"
)
func generateBloomFiltersForChannels(description *protobuf.CommunityDescription, privateKey *ecdsa.PrivateKey) error {
for channelID, channel := range description.Chats {
if !channelEncrypted(ChatID(description.ID, channelID), description.TokenPermissions) {
continue
}
filter, err := generateBloomFilter(channel.Members, privateKey, channelID, description.Clock)
if err != nil {
return err
}
marshaledFilter, err := filter.MarshalBinary()
if err != nil {
return err
}
channel.MembersList = &protobuf.CommunityBloomFilter{
Data: marshaledFilter,
M: uint64(filter.Cap()),
K: uint64(filter.K()),
}
}
return nil
}
func nextPowerOfTwo(x int) uint {
return 1 << bits.Len(uint(x))
}
func max(x, y uint) uint {
if x > y {
return x
}
return y
}
func generateBloomFilter(members map[string]*protobuf.CommunityMember, privateKey *ecdsa.PrivateKey, channelID string, clock uint64) (*bloom.BloomFilter, error) {
membersCount := len(members)
if membersCount == 0 {
return nil, errors.New("invalid members count")
}
const falsePositiveRate = 0.001
numberOfItems := max(128, nextPowerOfTwo(membersCount)) // This makes it difficult to guess the exact number of members, even with knowledge of filter size and parameters.
filter := bloom.NewWithEstimates(numberOfItems, falsePositiveRate)
for pk := range members {
publicKey, err := common.HexToPubkey(pk)
if err != nil {
return nil, err
}
value, err := bloomFilterValue(privateKey, publicKey, channelID, clock)
if err != nil {
return nil, err
}
filter.Add(value)
}
return filter, nil
}
func verifyMembershipWithBloomFilter(membersList *protobuf.CommunityBloomFilter, privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, channelID string, clock uint64) (bool, error) {
filter := bloom.New(uint(membersList.M), uint(membersList.K))
err := filter.UnmarshalBinary(membersList.Data)
if err != nil {
return false, err
}
value, err := bloomFilterValue(privateKey, publicKey, channelID, clock)
if err != nil {
return false, err
}
return filter.Test(value), nil
}
func bloomFilterValue(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, channelID string, clock uint64) ([]byte, error) {
sharedSecret, err := encryption.GenerateSharedKey(privateKey, publicKey)
if err != nil {
return nil, err
}
clockBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(clockBytes, clock)
return crypto.Keccak256(sharedSecret, []byte(channelID), clockBytes), nil
}

View File

@ -0,0 +1,68 @@
package communities
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
func TestCommunityBloomFilter(t *testing.T) {
suite.Run(t, new(CommunityBloomFilterSuite))
}
type CommunityBloomFilterSuite struct {
suite.Suite
}
func (s *CommunityBloomFilterSuite) TestBasic() {
ownerIdentity, err := crypto.GenerateKey()
s.Require().NoError(err)
memberIdentity, err := crypto.GenerateKey()
s.Require().NoError(err)
nonMemberIdentity, err := crypto.GenerateKey()
s.Require().NoError(err)
communityID := "cid"
encryptedChannelID := "enc"
nonEncryptedChannelID := "non-enc"
description := &protobuf.CommunityDescription{
ID: communityID,
Clock: 1,
Chats: map[string]*protobuf.CommunityChat{
encryptedChannelID: {
Members: map[string]*protobuf.CommunityMember{
common.PubkeyToHex(&memberIdentity.PublicKey): {},
},
},
nonEncryptedChannelID: {
Members: map[string]*protobuf.CommunityMember{
common.PubkeyToHex(&memberIdentity.PublicKey): {},
},
},
},
TokenPermissions: map[string]*protobuf.CommunityTokenPermission{
"permissionID": {
Id: "permissionID",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: []*protobuf.TokenCriteria{{}},
ChatIds: []string{ChatID(communityID, encryptedChannelID)},
},
},
}
err = generateBloomFiltersForChannels(description, ownerIdentity)
s.Require().NoError(err)
s.Require().NotNil(description.Chats[encryptedChannelID].MembersList)
s.Require().Nil(description.Chats[nonEncryptedChannelID].MembersList)
filter := description.Chats[encryptedChannelID].MembersList
s.Require().True(verifyMembershipWithBloomFilter(filter, memberIdentity, &ownerIdentity.PublicKey, encryptedChannelID, description.Clock))
s.Require().False(verifyMembershipWithBloomFilter(filter, nonMemberIdentity, &ownerIdentity.PublicKey, encryptedChannelID, description.Clock))
}

View File

@ -30,7 +30,7 @@ func createTestCommunity(identity *ecdsa.PrivateKey) (*Community, error) {
ControlNode: &identity.PublicKey, ControlNode: &identity.PublicKey,
ControlDevice: true, ControlDevice: true,
Joined: true, Joined: true,
MemberIdentity: &identity.PublicKey, MemberIdentity: identity,
} }
return New(config, &TimeSourceStub{}, &DescriptionEncryptorMock{}) return New(config, &TimeSourceStub{}, &DescriptionEncryptorMock{})

View File

@ -390,7 +390,7 @@ func (s *CommunitySuite) TestValidateRequestToJoin() {
}, },
{ {
name: "not admin", name: "not admin",
config: Config{MemberIdentity: signer, CommunityDescription: description}, config: Config{MemberIdentity: key, CommunityDescription: description},
signer: signer, signer: signer,
request: request, request: request,
err: ErrNotAdmin, err: ErrNotAdmin,
@ -813,7 +813,7 @@ func (s *CommunitySuite) emptyCommunityDescriptionWithChat() *protobuf.Community
func (s *CommunitySuite) newConfig(identity *ecdsa.PrivateKey, description *protobuf.CommunityDescription) Config { func (s *CommunitySuite) newConfig(identity *ecdsa.PrivateKey, description *protobuf.CommunityDescription) Config {
return Config{ return Config{
MemberIdentity: &identity.PublicKey, MemberIdentity: identity,
ID: &identity.PublicKey, ID: &identity.PublicKey,
CommunityDescription: description, CommunityDescription: description,
PrivateKey: identity, PrivateKey: identity,
@ -998,7 +998,7 @@ func (s *CommunitySuite) TestMarshalJSON() {
s.Require().True(community.ChannelEncrypted(testChatID1)) s.Require().True(community.ChannelEncrypted(testChatID1))
communityDescription := community.config.CommunityDescription communityDescription := community.config.CommunityDescription
ownerKey, err := crypto.GenerateKey() ownerKey := s.identity
s.Require().NoError(err) s.Require().NoError(err)
memberKey, err := crypto.GenerateKey() memberKey, err := crypto.GenerateKey()
@ -1043,6 +1043,7 @@ func (s *CommunitySuite) TestMarshalJSON() {
"position": float64(0), "position": float64(0),
"tokenGated": true, "tokenGated": true,
"viewersCanPostReactions": false, "viewersCanPostReactions": false,
"missingEncryptionKey": false,
} }
expectedChats[testChatID1] = expectedChat expectedChats[testChatID1] = expectedChat
@ -1074,6 +1075,7 @@ func (s *CommunitySuite) TestMarshalJSON() {
"position": float64(0), "position": float64(0),
"tokenGated": false, "tokenGated": false,
"viewersCanPostReactions": false, "viewersCanPostReactions": false,
"missingEncryptionKey": false,
} }
expectedChats[testChatID1] = expectedChat expectedChats[testChatID1] = expectedChat

View File

@ -845,7 +845,7 @@ func (m *Manager) CreateCommunity(request *requests.CreateCommunity, publish boo
Logger: m.logger, Logger: m.logger,
Joined: true, Joined: true,
JoinedAt: time.Now().Unix(), JoinedAt: time.Now().Unix(),
MemberIdentity: &m.identity.PublicKey, MemberIdentity: m.identity,
CommunityDescription: description, CommunityDescription: description,
Shard: nil, Shard: nil,
LastOpenedAt: 0, LastOpenedAt: 0,
@ -1709,7 +1709,7 @@ func (m *Manager) ImportCommunity(key *ecdsa.PrivateKey, clock uint64) (*Communi
Logger: m.logger, Logger: m.logger,
Joined: true, Joined: true,
JoinedAt: time.Now().Unix(), JoinedAt: time.Now().Unix(),
MemberIdentity: &m.identity.PublicKey, MemberIdentity: m.identity,
CommunityDescription: description, CommunityDescription: description,
LastOpenedAt: 0, LastOpenedAt: 0,
} }
@ -2134,7 +2134,7 @@ func (m *Manager) HandleCommunityDescriptionMessage(signer *ecdsa.PublicKey, des
CommunityDescription: processedDescription, CommunityDescription: processedDescription,
Logger: m.logger, Logger: m.logger,
CommunityDescriptionProtocolMessage: payload, CommunityDescriptionProtocolMessage: payload,
MemberIdentity: &m.identity.PublicKey, MemberIdentity: m.identity,
ID: pubKey, ID: pubKey,
ControlNode: signer, ControlNode: signer,
Shard: shard.FromProtobuff(communityShard), Shard: shard.FromProtobuff(communityShard),
@ -3851,7 +3851,7 @@ func (m *Manager) dbRecordBundleToCommunity(r *CommunityRecordBundle) (*Communit
descriptionEncryptor = m descriptionEncryptor = m
} }
return recordBundleToCommunity(r, &m.identity.PublicKey, m.installationID, m.logger, m.timesource, descriptionEncryptor, func(community *Community) error { return recordBundleToCommunity(r, m.identity, m.installationID, m.logger, m.timesource, descriptionEncryptor, func(community *Community) error {
_, description, err := m.preprocessDescription(community.ID(), community.config.CommunityDescription) _, description, err := m.preprocessDescription(community.ID(), community.config.CommunityDescription)
if err != nil { if err != nil {
return err return err

View File

@ -71,7 +71,7 @@ func recordToRequestToJoin(r *RequestToJoinRecord) *RequestToJoin {
} }
} }
func recordBundleToCommunity(r *CommunityRecordBundle, memberIdentity *ecdsa.PublicKey, installationID string, func recordBundleToCommunity(r *CommunityRecordBundle, memberIdentity *ecdsa.PrivateKey, installationID string,
logger *zap.Logger, timesource common.TimeSource, encryptor DescriptionEncryptor, initializer func(*Community) error) (*Community, error) { logger *zap.Logger, timesource common.TimeSource, encryptor DescriptionEncryptor, initializer func(*Community) error) (*Community, error) {
var privateKey *ecdsa.PrivateKey var privateKey *ecdsa.PrivateKey
var controlNode *ecdsa.PublicKey var controlNode *ecdsa.PublicKey

View File

@ -48,7 +48,7 @@ func (s *PersistenceSuite) SetupTest() {
s.Require().NoError(err) s.Require().NoError(err)
s.db = &Persistence{db: db, recordBundleToCommunity: func(r *CommunityRecordBundle) (*Community, error) { s.db = &Persistence{db: db, recordBundleToCommunity: func(r *CommunityRecordBundle) (*Community, error) {
return recordBundleToCommunity(r, &s.identity.PublicKey, "", nil, &TimeSourceStub{}, &DescriptionEncryptorMock{}, nil) return recordBundleToCommunity(r, s.identity, "", nil, &TimeSourceStub{}, &DescriptionEncryptorMock{}, nil)
}} }}
} }
@ -259,7 +259,7 @@ func (s *PersistenceSuite) makeNewCommunity(identity *ecdsa.PrivateKey) *Communi
s.Require().NoError(err, "crypto.GenerateKey shouldn't give any error") s.Require().NoError(err, "crypto.GenerateKey shouldn't give any error")
com, err := New(Config{ com, err := New(Config{
MemberIdentity: &identity.PublicKey, MemberIdentity: identity,
PrivateKey: comPrivKey, PrivateKey: comPrivKey,
ControlNode: &comPrivKey.PublicKey, ControlNode: &comPrivKey.PublicKey,
ControlDevice: true, ControlDevice: true,

View File

@ -1208,6 +1208,11 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) testViewChannelPermissions(v
) )
s.Require().NoError(err) s.Require().NoError(err)
// bob should not be in the bloom filter list
community, err = s.bob.communitiesManager.GetByID(community.ID())
s.Require().NoError(err)
s.Require().False(community.IsMemberLikelyInChat(chat.CommunityChatID()))
// make bob satisfy channel criteria // make bob satisfy channel criteria
s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, channelPermissionRequest.TokenCriteria[0]) s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, channelPermissionRequest.TokenCriteria[0])
defer s.resetMockedBalances() // reset mocked balances, this test in run with different test cases defer s.resetMockedBalances() // reset mocked balances, this test in run with different test cases
@ -1245,6 +1250,11 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) testViewChannelPermissions(v
s.Require().Len(response.Messages(), 1) s.Require().Len(response.Messages(), 1)
s.Require().Equal(msg.Text, response.Messages()[0].Text) s.Require().Equal(msg.Text, response.Messages()[0].Text)
// bob should be in the bloom filter list
community, err = s.bob.communitiesManager.GetByID(community.ID())
s.Require().NoError(err)
s.Require().True(community.IsMemberLikelyInChat(chat.CommunityChatID()))
// bob can/can't post reactions // bob can/can't post reactions
response, err = s.bob.SendEmojiReaction(context.Background(), chat.ID, msg.ID, protobuf.EmojiReaction_THUMBS_UP) response, err = s.bob.SendEmojiReaction(context.Background(), chat.ID, msg.ID, protobuf.EmojiReaction_THUMBS_UP)
if !viewersCanAddReactions { if !viewersCanAddReactions {

View File

@ -66,7 +66,7 @@ func encrypt(plaintext []byte, key []byte, reader io.Reader) ([]byte, error) {
return gcm.Seal(nonce, nonce, plaintext, nil), nil return gcm.Seal(nonce, nonce, plaintext, nil), nil
} }
func generateSharedKey(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey) ([]byte, error) { func GenerateSharedKey(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey) ([]byte, error) {
const encryptedPayloadKeyLength = 16 const encryptedPayloadKeyLength = 16
@ -87,7 +87,7 @@ func buildGroupRekeyMessage(privateKey *ecdsa.PrivateKey, groupID []byte, timest
for _, k := range keys { for _, k := range keys {
sharedKey, err := generateSharedKey(privateKey, k) sharedKey, err := GenerateSharedKey(privateKey, k)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -138,7 +138,7 @@ func decryptGroupRekeyMessage(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.Pub
return nil, nil return nil, nil
} }
sharedKey, err := generateSharedKey(privateKey, publicKey) sharedKey, err := GenerateSharedKey(privateKey, publicKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -427,7 +427,7 @@ func (p *Protocol) EncryptCommunityGrants(privateKey *ecdsa.PrivateKey, recipien
grants := make(map[uint32][]byte) grants := make(map[uint32][]byte)
for recipientKey, grant := range recipientGrants { for recipientKey, grant := range recipientGrants {
sharedKey, err := generateSharedKey(privateKey, recipientKey) sharedKey, err := GenerateSharedKey(privateKey, recipientKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -452,7 +452,7 @@ func (p *Protocol) DecryptCommunityGrant(myIdentityKey *ecdsa.PrivateKey, sender
return nil, errors.New("can't find related grant in the map") return nil, errors.New("can't find related grant in the map")
} }
sharedKey, err := generateSharedKey(myIdentityKey, senderKey) sharedKey, err := GenerateSharedKey(myIdentityKey, senderKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

File diff suppressed because it is too large Load Diff

View File

@ -144,6 +144,13 @@ message CommunityChat {
int32 position = 5; int32 position = 5;
bool viewers_can_post_reactions = 6; bool viewers_can_post_reactions = 6;
bool hide_if_permissions_not_met = 7; bool hide_if_permissions_not_met = 7;
CommunityBloomFilter members_list = 8;
}
message CommunityBloomFilter {
bytes data = 1;
uint64 m = 2;
uint64 k = 3;
} }
message CommunityCategory { message CommunityCategory {