Skip to content

Commit

Permalink
feat: encrypt CommunityDescription fields
Browse files Browse the repository at this point in the history
Extended `CommunityDescription` with a `privateData` map. This map
associates each hash ratchet `key_id` and `seq_no` with an encrypted
`CommunityDescription`. Each encrypted instance includes only data
requiring encryption.

closes: status-im/status-desktop#12851
closes: status-im/status-desktop#12852
closes: status-im/status-desktop#12853
  • Loading branch information
osmaczko committed Dec 22, 2023
1 parent 9cbfda6 commit 1d3c618
Show file tree
Hide file tree
Showing 22 changed files with 833 additions and 288 deletions.
76 changes: 44 additions & 32 deletions protocol/communities/community.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ type Community struct {
config *Config
mutex sync.Mutex
timesource common.TimeSource
encryptor DescriptionEncryptor
}

func New(config Config, timesource common.TimeSource) (*Community, error) {
func New(config Config, timesource common.TimeSource, encryptor DescriptionEncryptor) (*Community, error) {
if config.MemberIdentity == nil {
return nil, errors.New("no member identity")
}
Expand All @@ -78,9 +79,11 @@ func New(config Config, timesource common.TimeSource) (*Community, error) {
config.Logger = logger
}

community := &Community{config: &config, timesource: timesource}
community.initialize()
return community, nil
if config.CommunityDescription == nil {
config.CommunityDescription = &protobuf.CommunityDescription{}
}

return &Community{config: &config, timesource: timesource, encryptor: encryptor}, nil
}

type CommunityAdminSettings struct {
Expand Down Expand Up @@ -501,13 +504,6 @@ func (o *Community) GetMemberPubkeys() []*ecdsa.PublicKey {
return nil
}

func (o *Community) initialize() {
if o.config.CommunityDescription == nil {
o.config.CommunityDescription = &protobuf.CommunityDescription{}

}
}

type CommunitySettings struct {
CommunityID string `json:"communityId"`
HistoryArchiveSupportEnabled bool `json:"historyArchiveSupportEnabled"`
Expand Down Expand Up @@ -1377,11 +1373,20 @@ func (o *Community) Description() *protobuf.CommunityDescription {
}

func (o *Community) marshaledDescription() ([]byte, error) {
clone := proto.Clone(o.config.CommunityDescription).(*protobuf.CommunityDescription)

// This is only workaround to lower the size of the message that goes over the wire,
// see https://github.com/status-im/status-desktop/issues/12188
clone := o.CreateDeepCopy()
clone.DehydrateChannelsMembers()
return proto.Marshal(clone.config.CommunityDescription)
dehydrateChannelsMembers(o.IDString(), clone)

if o.encryptor != nil {
err := encryptDescription(o.encryptor, o, clone)
if err != nil {
return nil, err
}
}

return proto.Marshal(clone)
}

func (o *Community) MarshaledDescription() ([]byte, error) {
Expand Down Expand Up @@ -1419,28 +1424,28 @@ func (o *Community) ToProtocolMessageBytes() ([]byte, error) {
return o.toProtocolMessageBytes()
}

func (o *Community) DehydrateChannelsMembers() {
// To save space, we don't attach members for channels without permissions,
// otherwise the message will hit waku msg size limit.
for channelID, channel := range o.chats() {
if !o.ChannelHasTokenPermissions(o.ChatID(channelID)) {
channel.Members = map[string]*protobuf.CommunityMember{} // clean members
func channelHasTokenPermissions(communityID string, channelID string, permissions map[string]*protobuf.CommunityTokenPermission) bool {
for _, tokenPermission := range permissions {
if includes(tokenPermission.ChatIds, communityID+channelID) {
return true
}
}
return false
}

func HydrateChannelsMembers(communityID string, description *protobuf.CommunityDescription) {
channelHasTokenPermissions := func(channelID string) bool {
for _, tokenPermission := range description.TokenPermissions {
if includes(tokenPermission.ChatIds, communityID+channelID) {
return true
}
func dehydrateChannelsMembers(communityID string, description *protobuf.CommunityDescription) {
// To save space, we don't attach members for channels without permissions,
// otherwise the message will hit waku msg size limit.
for channelID, channel := range description.Chats {
if !channelHasTokenPermissions(communityID, channelID, description.TokenPermissions) {
channel.Members = map[string]*protobuf.CommunityMember{} // clean members
}
return false
}
}

func hydrateChannelsMembers(communityID string, description *protobuf.CommunityDescription) {
for channelID, channel := range description.Chats {
if !channelHasTokenPermissions(channelID) {
if !channelHasTokenPermissions(communityID, channelID, description.TokenPermissions) {
channel.Members = make(map[string]*protobuf.CommunityMember)
for pubKey, member := range description.Members {
channel.Members[pubKey] = member
Expand Down Expand Up @@ -1597,14 +1602,15 @@ func (o *Community) HasTokenPermissions() bool {
return len(o.tokenPermissions()) > 0
}

func (o *Community) channelEncrypted(channelID string) bool {
return o.channelHasTokenPermissions(o.ChatID(channelID))
}

func (o *Community) ChannelEncrypted(channelID string) bool {
return o.ChannelHasTokenPermissions(o.ChatID(channelID))
}

func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
o.mutex.Lock()
defer o.mutex.Unlock()

func (o *Community) channelHasTokenPermissions(chatID string) bool {
for _, tokenPermission := range o.tokenPermissions() {
if includes(tokenPermission.ChatIds, chatID) {
return true
Expand All @@ -1614,6 +1620,12 @@ func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
return false
}

func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
o.mutex.Lock()
defer o.mutex.Unlock()
return o.channelHasTokenPermissions(chatID)
}

func TokenPermissionsByType(permissions map[string]*CommunityTokenPermission, permissionType protobuf.CommunityTokenPermission_Type) []*CommunityTokenPermission {
result := make([]*CommunityTokenPermission, 0)
for _, tokenPermission := range permissions {
Expand Down
95 changes: 95 additions & 0 deletions protocol/communities/community_description_encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package communities

import (
"github.com/golang/protobuf/proto"

"go.uber.org/zap"

"github.com/status-im/status-go/protocol/protobuf"
)

type DescriptionEncryptor interface {
encryptCommunityDescription(community *Community, d *protobuf.CommunityDescription) (string, []byte, error)
encryptCommunityDescriptionChannel(community *Community, channelID string, d *protobuf.CommunityDescription) (string, []byte, error)
decryptCommunityDescription(keyIDSeqNo string, d []byte) (*protobuf.CommunityDescription, error)
}

// Encrypts members and chats
func encryptDescription(encryptor DescriptionEncryptor, community *Community, description *protobuf.CommunityDescription) error {
description.PrivateData = make(map[string][]byte)

for channelID, channel := range description.Chats {
if !community.channelEncrypted(channelID) {
continue
}

descriptionToEncrypt := &protobuf.CommunityDescription{
Chats: map[string]*protobuf.CommunityChat{
channelID: proto.Clone(channel).(*protobuf.CommunityChat),
},
}

keyIDSeqNo, encryptedDescription, err := encryptor.encryptCommunityDescriptionChannel(community, channelID, descriptionToEncrypt)
if err != nil {
return err
}

// Set private data and cleanup unencrypted channel's members
description.PrivateData[keyIDSeqNo] = encryptedDescription
channel.Members = make(map[string]*protobuf.CommunityMember)
}

if community.Encrypted() {
descriptionToEncrypt := &protobuf.CommunityDescription{
Members: description.Members,
Chats: description.Chats,
}

keyIDSeqNo, encryptedDescription, err := encryptor.encryptCommunityDescription(community, descriptionToEncrypt)
if err != nil {
return err
}

// Set private data and cleanup unencrypted members and chats
description.PrivateData[keyIDSeqNo] = encryptedDescription
description.Members = make(map[string]*protobuf.CommunityMember)
description.Chats = make(map[string]*protobuf.CommunityChat)
}

return nil
}

// Decrypts members and chats
func decryptDescription(encryptor DescriptionEncryptor, description *protobuf.CommunityDescription, logger *zap.Logger) error {
for keyIDSeqNo, encryptedDescription := range description.PrivateData {
decryptedDescription, err := encryptor.decryptCommunityDescription(keyIDSeqNo, encryptedDescription)
if err != nil {
// ignore error, try to decrypt next data
logger.Debug("failed to decrypt community private data", zap.String("keyIDSeqNo", keyIDSeqNo), zap.Error(err))
continue
}

for pk, member := range decryptedDescription.Members {
if description.Members == nil {
description.Members = make(map[string]*protobuf.CommunityMember)
}
description.Members[pk] = member
}

for id, decryptedChannel := range decryptedDescription.Chats {
if description.Chats == nil {
description.Chats = make(map[string]*protobuf.CommunityChat)
}

if channel := description.Chats[id]; channel != nil {
if len(channel.Members) == 0 {
channel.Members = decryptedChannel.Members
}
} else {
description.Chats[id] = decryptedChannel
}
}
}

return nil
}
Loading

0 comments on commit 1d3c618

Please sign in to comment.