feat(communities)_: introduce bloom filter members list
iterates: status-im/status-desktop#15064
This commit is contained in:
parent
1715defec8
commit
cb20c4c64a
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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{})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue