feat: encrypt `CommunityDescription` fields

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
This commit is contained in:
Patryk Osmaczko 2023-11-29 18:21:21 +01:00 committed by osmaczko
parent 9cbfda69da
commit 1d3c618fb4
22 changed files with 846 additions and 301 deletions

View File

@ -59,9 +59,10 @@ type Community struct {
config *Config config *Config
mutex sync.Mutex mutex sync.Mutex
timesource common.TimeSource 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 { if config.MemberIdentity == nil {
return nil, errors.New("no member identity") return nil, errors.New("no member identity")
} }
@ -78,9 +79,11 @@ func New(config Config, timesource common.TimeSource) (*Community, error) {
config.Logger = logger config.Logger = logger
} }
community := &Community{config: &config, timesource: timesource} if config.CommunityDescription == nil {
community.initialize() config.CommunityDescription = &protobuf.CommunityDescription{}
return community, nil }
return &Community{config: &config, timesource: timesource, encryptor: encryptor}, nil
} }
type CommunityAdminSettings struct { type CommunityAdminSettings struct {
@ -501,13 +504,6 @@ func (o *Community) GetMemberPubkeys() []*ecdsa.PublicKey {
return nil return nil
} }
func (o *Community) initialize() {
if o.config.CommunityDescription == nil {
o.config.CommunityDescription = &protobuf.CommunityDescription{}
}
}
type CommunitySettings struct { type CommunitySettings struct {
CommunityID string `json:"communityId"` CommunityID string `json:"communityId"`
HistoryArchiveSupportEnabled bool `json:"historyArchiveSupportEnabled"` HistoryArchiveSupportEnabled bool `json:"historyArchiveSupportEnabled"`
@ -1377,11 +1373,20 @@ func (o *Community) Description() *protobuf.CommunityDescription {
} }
func (o *Community) marshaledDescription() ([]byte, error) { 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, // 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 // see https://github.com/status-im/status-desktop/issues/12188
clone := o.CreateDeepCopy() dehydrateChannelsMembers(o.IDString(), clone)
clone.DehydrateChannelsMembers()
return proto.Marshal(clone.config.CommunityDescription) 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) { func (o *Community) MarshaledDescription() ([]byte, error) {
@ -1419,28 +1424,28 @@ func (o *Community) ToProtocolMessageBytes() ([]byte, error) {
return o.toProtocolMessageBytes() return o.toProtocolMessageBytes()
} }
func (o *Community) DehydrateChannelsMembers() { 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 dehydrateChannelsMembers(communityID string, description *protobuf.CommunityDescription) {
// To save space, we don't attach members for channels without permissions, // To save space, we don't attach members for channels without permissions,
// otherwise the message will hit waku msg size limit. // otherwise the message will hit waku msg size limit.
for channelID, channel := range o.chats() { for channelID, channel := range description.Chats {
if !o.ChannelHasTokenPermissions(o.ChatID(channelID)) { if !channelHasTokenPermissions(communityID, channelID, description.TokenPermissions) {
channel.Members = map[string]*protobuf.CommunityMember{} // clean members channel.Members = map[string]*protobuf.CommunityMember{} // clean members
} }
} }
} }
func HydrateChannelsMembers(communityID string, description *protobuf.CommunityDescription) { 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
}
}
return false
}
for channelID, channel := range description.Chats { for channelID, channel := range description.Chats {
if !channelHasTokenPermissions(channelID) { if !channelHasTokenPermissions(communityID, channelID, description.TokenPermissions) {
channel.Members = make(map[string]*protobuf.CommunityMember) channel.Members = make(map[string]*protobuf.CommunityMember)
for pubKey, member := range description.Members { for pubKey, member := range description.Members {
channel.Members[pubKey] = member channel.Members[pubKey] = member
@ -1597,14 +1602,15 @@ func (o *Community) HasTokenPermissions() bool {
return len(o.tokenPermissions()) > 0 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 { func (o *Community) ChannelEncrypted(channelID string) bool {
return o.ChannelHasTokenPermissions(o.ChatID(channelID)) return o.ChannelHasTokenPermissions(o.ChatID(channelID))
} }
func (o *Community) ChannelHasTokenPermissions(chatID string) bool { func (o *Community) channelHasTokenPermissions(chatID string) bool {
o.mutex.Lock()
defer o.mutex.Unlock()
for _, tokenPermission := range o.tokenPermissions() { for _, tokenPermission := range o.tokenPermissions() {
if includes(tokenPermission.ChatIds, chatID) { if includes(tokenPermission.ChatIds, chatID) {
return true return true
@ -1614,6 +1620,12 @@ func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
return false 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 { func TokenPermissionsByType(permissions map[string]*CommunityTokenPermission, permissionType protobuf.CommunityTokenPermission_Type) []*CommunityTokenPermission {
result := make([]*CommunityTokenPermission, 0) result := make([]*CommunityTokenPermission, 0)
for _, tokenPermission := range permissions { for _, tokenPermission := range permissions {

View File

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

View File

@ -0,0 +1,181 @@
package communities
import (
"crypto/ecdsa"
"errors"
"testing"
"github.com/golang/protobuf/proto"
"github.com/google/uuid"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
"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/protobuf"
)
func TestCommunityEncryptionDescriptionSuite(t *testing.T) {
suite.Run(t, new(CommunityEncryptionDescriptionSuite))
}
type CommunityEncryptionDescriptionSuite struct {
suite.Suite
descriptionEncryptor *DescriptionEncryptorMock
identity *ecdsa.PrivateKey
communityID []byte
logger *zap.Logger
}
func (s *CommunityEncryptionDescriptionSuite) SetupTest() {
s.descriptionEncryptor = &DescriptionEncryptorMock{
descriptions: map[string]*protobuf.CommunityDescription{},
channelIDToKeyIDSeqNo: map[string]string{},
}
identity, err := crypto.GenerateKey()
s.Require().NoError(err)
s.identity = identity
s.communityID = crypto.CompressPubkey(&identity.PublicKey)
s.logger, err = zap.NewDevelopment()
s.Require().NoError(err)
}
type DescriptionEncryptorMock struct {
descriptions map[string]*protobuf.CommunityDescription
channelIDToKeyIDSeqNo map[string]string
}
func (dem *DescriptionEncryptorMock) encryptCommunityDescription(community *Community, d *protobuf.CommunityDescription) (string, []byte, error) {
keyIDSeqNo := uuid.New().String()
dem.descriptions[keyIDSeqNo] = d
return keyIDSeqNo, []byte("encryptedDescription"), nil
}
func (dem *DescriptionEncryptorMock) encryptCommunityDescriptionChannel(community *Community, channelID string, d *protobuf.CommunityDescription) (string, []byte, error) {
keyIDSeqNo := uuid.New().String()
dem.descriptions[keyIDSeqNo] = d
dem.channelIDToKeyIDSeqNo[channelID] = keyIDSeqNo
return keyIDSeqNo, []byte("encryptedDescription"), nil
}
func (dem *DescriptionEncryptorMock) decryptCommunityDescription(keyIDSeqNo string, d []byte) (*protobuf.CommunityDescription, error) {
description := dem.descriptions[keyIDSeqNo]
if description == nil {
return nil, errors.New("no key to decrypt private data")
}
return description, nil
}
func (dem *DescriptionEncryptorMock) forgetAllKeys() {
dem.descriptions = make(map[string]*protobuf.CommunityDescription)
}
func (dem *DescriptionEncryptorMock) forgetChannelKeys() {
for _, keyIDSeqNo := range dem.channelIDToKeyIDSeqNo {
delete(dem.descriptions, keyIDSeqNo)
}
}
func (s *CommunityEncryptionDescriptionSuite) description() *protobuf.CommunityDescription {
return &protobuf.CommunityDescription{
IntroMessage: "one of not encrypted fields",
Members: map[string]*protobuf.CommunityMember{
"memberA": &protobuf.CommunityMember{},
"memberB": &protobuf.CommunityMember{},
},
Chats: map[string]*protobuf.CommunityChat{
"channelA": &protobuf.CommunityChat{
Members: map[string]*protobuf.CommunityMember{
"memberA": &protobuf.CommunityMember{},
"memberB": &protobuf.CommunityMember{},
},
},
"channelB": &protobuf.CommunityChat{
Members: map[string]*protobuf.CommunityMember{
"memberA": &protobuf.CommunityMember{},
},
},
},
PrivateData: map[string][]byte{},
// ensure community and channel encryption
TokenPermissions: map[string]*protobuf.CommunityTokenPermission{
"community-level-permission": &protobuf.CommunityTokenPermission{
Id: "community-level-permission",
Type: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenCriteria: []*protobuf.TokenCriteria{},
ChatIds: []string{},
},
"channel-level-permission": &protobuf.CommunityTokenPermission{
Id: "community-level-permission",
Type: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenCriteria: []*protobuf.TokenCriteria{},
ChatIds: []string{types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + "channelB"},
},
},
}
}
func (s *CommunityEncryptionDescriptionSuite) TestEncryptionDecryption() {
description := s.description()
err := encryptDescription(s.descriptionEncryptor, &Community{
config: &Config{ID: &s.identity.PublicKey, CommunityDescription: description},
}, description)
s.Require().NoError(err)
s.Require().Len(description.PrivateData, 2)
// members and chats should become empty (encrypted)
s.Require().Empty(description.Members)
s.Require().Empty(description.Chats)
s.Require().Equal(description.IntroMessage, "one of not encrypted fields")
// members and chats should be brought back
err = decryptDescription(s.descriptionEncryptor, description, s.logger)
s.Require().NoError(err)
s.Require().Len(description.Members, 2)
s.Require().Len(description.Chats, 2)
s.Require().Len(description.Chats["channelA"].Members, 2)
s.Require().Len(description.Chats["channelB"].Members, 1)
s.Require().Equal(description.IntroMessage, "one of not encrypted fields")
}
func (s *CommunityEncryptionDescriptionSuite) TestDecryption_NoKeys() {
encryptedDescription := func() *protobuf.CommunityDescription {
description := s.description()
err := encryptDescription(s.descriptionEncryptor, &Community{
config: &Config{ID: &s.identity.PublicKey, CommunityDescription: description},
}, description)
s.Require().NoError(err)
return description
}()
description := proto.Clone(encryptedDescription).(*protobuf.CommunityDescription)
// forget channel keys, so channel members can't be decrypted
s.descriptionEncryptor.forgetChannelKeys()
// encrypted channel should have no members
err := decryptDescription(s.descriptionEncryptor, description, s.logger)
s.Require().NoError(err)
s.Require().Len(description.Members, 2)
s.Require().Len(description.Chats, 2)
s.Require().Len(description.Chats["channelA"].Members, 2)
s.Require().Len(description.Chats["channelB"].Members, 0) // encrypted channel
s.Require().Equal(description.IntroMessage, "one of not encrypted fields")
description = proto.Clone(encryptedDescription).(*protobuf.CommunityDescription)
// forget the keys, so chats and members can't be decrypted
s.descriptionEncryptor.forgetAllKeys()
// members and chats should be empty
err = decryptDescription(s.descriptionEncryptor, description, s.logger)
s.Require().NoError(err)
s.Require().Empty(description.Members)
s.Require().Empty(description.Chats)
s.Require().Equal(description.IntroMessage, "one of not encrypted fields")
}

View File

@ -2,6 +2,11 @@ package communities
import "github.com/status-im/status-go/protocol/protobuf" import "github.com/status-im/status-go/protocol/protobuf"
type KeyDistributor interface {
Generate(community *Community, keyActions *EncryptionKeyActions) error
Distribute(community *Community, keyActions *EncryptionKeyActions) error
}
type EncryptionKeyActionType int type EncryptionKeyActionType int
const ( const (

View File

@ -33,7 +33,7 @@ func createTestCommunity(identity *ecdsa.PrivateKey) (*Community, error) {
MemberIdentity: &identity.PublicKey, MemberIdentity: &identity.PublicKey,
} }
return New(config, &TimeSourceStub{}) return New(config, &TimeSourceStub{}, &DescriptionEncryptorMock{})
} }
func TestCommunityEncryptionKeyActionSuite(t *testing.T) { func TestCommunityEncryptionKeyActionSuite(t *testing.T) {

View File

@ -200,6 +200,13 @@ func (o *Community) UpdateCommunityByEvents(communityEventMessage *CommunityEven
// during saving the community // during saving the community
o.mergeCommunityEvents(communityEventMessage) o.mergeCommunityEvents(communityEventMessage)
if o.encryptor != nil {
err = decryptDescription(o.encryptor, description, o.config.Logger)
if err != nil {
return err
}
}
o.config.CommunityDescription = description o.config.CommunityDescription = description
o.config.CommunityDescriptionProtocolMessage = communityEventMessage.EventsBaseCommunityDescription o.config.CommunityDescriptionProtocolMessage = communityEventMessage.EventsBaseCommunityDescription

View File

@ -440,7 +440,7 @@ func (s *CommunitySuite) TestValidateRequestToJoin() {
for _, tc := range testCases { for _, tc := range testCases {
s.Run(tc.name, func() { s.Run(tc.name, func() {
org, err := New(tc.config, &TimeSourceStub{}) org, err := New(tc.config, &TimeSourceStub{}, &DescriptionEncryptorMock{})
s.Require().NoError(err) s.Require().NoError(err)
err = org.ValidateRequestToJoin(tc.signer, tc.request) err = org.ValidateRequestToJoin(tc.signer, tc.request)
s.Require().Equal(tc.err, err) s.Require().Equal(tc.err, err)
@ -512,7 +512,7 @@ func (s *CommunitySuite) TestCanPost() {
s.Run(tc.name, func() { s.Run(tc.name, func() {
var grant []byte var grant []byte
var err error var err error
org, err := New(tc.config, &TimeSourceStub{}) org, err := New(tc.config, &TimeSourceStub{}, &DescriptionEncryptorMock{})
s.Require().NoError(err) s.Require().NoError(err)
if tc.grant == validGrant { if tc.grant == validGrant {
@ -882,7 +882,7 @@ func (s *CommunitySuite) buildCommunity(owner *ecdsa.PublicKey) *Community {
config.ID = owner config.ID = owner
config.CommunityDescription = s.buildCommunityDescription() config.CommunityDescription = s.buildCommunityDescription()
org, err := New(config, &TimeSourceStub{}) org, err := New(config, &TimeSourceStub{}, &DescriptionEncryptorMock{})
s.Require().NoError(err) s.Require().NoError(err)
return org return org
} }

View File

@ -4,11 +4,13 @@ import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"database/sql" "database/sql"
"encoding/hex"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -98,6 +100,7 @@ type Manager struct {
stopped bool stopped bool
RekeyInterval time.Duration RekeyInterval time.Duration
PermissionChecker PermissionChecker PermissionChecker PermissionChecker
keyDistributor KeyDistributor
} }
type HistoryArchiveDownloadTask struct { type HistoryArchiveDownloadTask struct {
@ -218,7 +221,7 @@ type OwnerVerifier interface {
SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error) SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error)
} }
func NewManager(identity *ecdsa.PrivateKey, installationID string, db *sql.DB, encryptor *encryption.Protocol, logger *zap.Logger, ensverifier *ens.Verifier, ownerVerifier OwnerVerifier, transport *transport.Transport, timesource common.TimeSource, torrentConfig *params.TorrentConfig, opts ...ManagerOption) (*Manager, error) { func NewManager(identity *ecdsa.PrivateKey, installationID string, db *sql.DB, encryptor *encryption.Protocol, logger *zap.Logger, ensverifier *ens.Verifier, ownerVerifier OwnerVerifier, transport *transport.Transport, timesource common.TimeSource, keyDistributor KeyDistributor, torrentConfig *params.TorrentConfig, opts ...ManagerOption) (*Manager, error) {
if identity == nil { if identity == nil {
return nil, errors.New("empty identity") return nil, errors.New("empty identity")
} }
@ -257,6 +260,7 @@ func NewManager(identity *ecdsa.PrivateKey, installationID string, db *sql.DB, e
torrentConfig: torrentConfig, torrentConfig: torrentConfig,
torrentTasks: make(map[string]metainfo.Hash), torrentTasks: make(map[string]metainfo.Hash),
historyArchiveDownloadTasks: make(map[string]*HistoryArchiveDownloadTask), historyArchiveDownloadTasks: make(map[string]*HistoryArchiveDownloadTask),
keyDistributor: keyDistributor,
} }
manager.persistence = &Persistence{ manager.persistence = &Persistence{
@ -741,7 +745,12 @@ func (m *Manager) CreateCommunity(request *requests.CreateCommunity, publish boo
CommunityDescription: description, CommunityDescription: description,
Shard: nil, Shard: nil,
} }
community, err := New(config, m.timesource)
var descriptionEncryptor DescriptionEncryptor
if m.encryptor != nil {
descriptionEncryptor = m
}
community, err := New(config, m.timesource, descriptionEncryptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -777,11 +786,23 @@ func (m *Manager) CreateCommunityTokenPermission(request *requests.CreateCommuni
return nil, nil, err return nil, nil, err
} }
originCommunity := community.CreateDeepCopy()
community, changes, err := m.createCommunityTokenPermission(request, community) community, changes, err := m.createCommunityTokenPermission(request, community)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// ensure key is generated before marshaling,
// as it requires key to encrypt description
if m.keyDistributor != nil && community.IsControlNode() {
encryptionKeyActions := EvaluateCommunityEncryptionKeyActions(originCommunity, community)
err := m.keyDistributor.Generate(community, encryptionKeyActions)
if err != nil {
return nil, nil, err
}
}
err = m.saveAndPublish(community) err = m.saveAndPublish(community)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -1209,11 +1230,15 @@ func (m *Manager) ImportCommunity(key *ecdsa.PrivateKey, clock uint64) (*Communi
MemberIdentity: &m.identity.PublicKey, MemberIdentity: &m.identity.PublicKey,
CommunityDescription: description, CommunityDescription: description,
} }
community, err = New(config, m.timesource)
var descriptionEncryptor DescriptionEncryptor
if m.encryptor != nil {
descriptionEncryptor = m
}
community, err = New(config, m.timesource, descriptionEncryptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else { } else {
community.config.PrivateKey = key community.config.PrivateKey = key
community.config.ControlDevice = true community.config.ControlDevice = true
@ -1541,13 +1566,15 @@ func (m *Manager) HandleCommunityDescriptionMessage(signer *ecdsa.PublicKey, des
id = crypto.CompressPubkey(signer) id = crypto.CompressPubkey(signer)
} }
community, err := m.GetByID(id) err = m.preprocessDescription(id, description)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Workaround for https://github.com/status-im/status-desktop/issues/12188 community, err := m.GetByID(id)
HydrateChannelsMembers(types.EncodeHex(id), description) if err != nil {
return nil, err
}
// We should queue only if the community has a token owner, and the owner has been verified // We should queue only if the community has a token owner, and the owner has been verified
hasTokenOwnership := HasTokenOwnership(description) hasTokenOwnership := HasTokenOwnership(description)
@ -1568,7 +1595,11 @@ func (m *Manager) HandleCommunityDescriptionMessage(signer *ecdsa.PublicKey, des
Shard: shard.FromProtobuff(communityShard), Shard: shard.FromProtobuff(communityShard),
} }
community, err = New(config, m.timesource) var descriptionEncryptor DescriptionEncryptor
if m.encryptor != nil {
descriptionEncryptor = m
}
community, err = New(config, m.timesource, descriptionEncryptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1608,7 +1639,20 @@ func (m *Manager) HandleCommunityDescriptionMessage(signer *ecdsa.PublicKey, des
return m.handleCommunityDescriptionMessageCommon(community, description, payload, verifiedOwner) return m.handleCommunityDescriptionMessageCommon(community, description, payload, verifiedOwner)
} }
func (m *Manager) preprocessDescription(id types.HexBytes, description *protobuf.CommunityDescription) error {
err := decryptDescription(m, description, m.logger)
if err != nil {
return err
}
// Workaround for https://github.com/status-im/status-desktop/issues/12188
hydrateChannelsMembers(types.EncodeHex(id), description)
return nil
}
func (m *Manager) handleCommunityDescriptionMessageCommon(community *Community, description *protobuf.CommunityDescription, payload []byte, newControlNode *ecdsa.PublicKey) (*CommunityResponse, error) { func (m *Manager) handleCommunityDescriptionMessageCommon(community *Community, description *protobuf.CommunityDescription, payload []byte, newControlNode *ecdsa.PublicKey) (*CommunityResponse, error) {
changes, err := community.UpdateCommunityDescription(description, payload, newControlNode) changes, err := community.UpdateCommunityDescription(description, payload, newControlNode)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1797,6 +1841,15 @@ func (m *Manager) HandleCommunityEventsMessage(signer *ecdsa.PublicKey, message
if community.IsControlNode() { if community.IsControlNode() {
community.config.EventsData = nil // clear events, they are already applied community.config.EventsData = nil // clear events, they are already applied
community.increaseClock() community.increaseClock()
if m.keyDistributor != nil {
encryptionKeyActions := EvaluateCommunityEncryptionKeyActions(originCommunity, community)
err := m.keyDistributor.Generate(community, encryptionKeyActions)
if err != nil {
return nil, err
}
}
err = m.persistence.SaveCommunity(community) err = m.persistence.SaveCommunity(community)
if err != nil { if err != nil {
return nil, err return nil, err
@ -2828,6 +2881,11 @@ func (m *Manager) HandleCommunityRequestToJoinResponse(signer *ecdsa.PublicKey,
return nil, ErrNotAuthorized return nil, ErrNotAuthorized
} }
err = m.preprocessDescription(community.ID(), request.Community)
if err != nil {
return nil, err
}
_, err = community.UpdateCommunityDescription(request.Community, appMetadataMsg, nil) _, err = community.UpdateCommunityDescription(request.Community, appMetadataMsg, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -3195,8 +3253,18 @@ func (m *Manager) BanUserFromCommunity(request *requests.BanUserFromCommunity) (
} }
func (m *Manager) dbRecordBundleToCommunity(r *CommunityRecordBundle) (*Community, error) { func (m *Manager) dbRecordBundleToCommunity(r *CommunityRecordBundle) (*Community, error) {
return recordBundleToCommunity(r, &m.identity.PublicKey, m.installationID, m.logger, m.timesource, func(community *Community) error { var descriptionEncryptor DescriptionEncryptor
err := community.updateCommunityDescriptionByEvents() if m.encryptor != nil {
descriptionEncryptor = m
}
return recordBundleToCommunity(r, &m.identity.PublicKey, m.installationID, m.logger, m.timesource, descriptionEncryptor, func(community *Community) error {
err := m.preprocessDescription(community.ID(), community.config.CommunityDescription)
if err != nil {
return err
}
err = community.updateCommunityDescriptionByEvents()
if err != nil { if err != nil {
return err return err
} }
@ -3210,9 +3278,6 @@ func (m *Manager) dbRecordBundleToCommunity(r *CommunityRecordBundle) (*Communit
community.config.PubsubTopicPrivateKey = privKey community.config.PubsubTopicPrivateKey = privKey
} }
// Workaround for https://github.com/status-im/status-desktop/issues/12188
HydrateChannelsMembers(community.IDString(), community.config.CommunityDescription)
return nil return nil
}) })
} }
@ -5114,6 +5179,65 @@ func (m *Manager) SetCuratedCommunities(communities *CuratedCommunities) error {
return m.persistence.SetCuratedCommunities(communities) return m.persistence.SetCuratedCommunities(communities)
} }
func (m *Manager) encryptCommunityDescriptionImpl(groupID []byte, d *protobuf.CommunityDescription) (string, []byte, error) {
payload, err := proto.Marshal(d)
if err != nil {
return "", nil, err
}
encryptedPayload, ratchet, newSeqNo, err := m.encryptor.EncryptWithHashRatchet(groupID, payload)
if err != nil {
return "", nil, err
}
keyID, err := ratchet.GetKeyID()
if err != nil {
return "", nil, err
}
keyIDSeqNo := fmt.Sprintf("%s%d", hex.EncodeToString(keyID), newSeqNo)
return keyIDSeqNo, encryptedPayload, nil
}
func (m *Manager) encryptCommunityDescription(community *Community, d *protobuf.CommunityDescription) (string, []byte, error) {
return m.encryptCommunityDescriptionImpl(community.ID(), d)
}
func (m *Manager) encryptCommunityDescriptionChannel(community *Community, channelID string, d *protobuf.CommunityDescription) (string, []byte, error) {
return m.encryptCommunityDescriptionImpl([]byte(community.IDString()+channelID), d)
}
func (m *Manager) decryptCommunityDescription(keyIDSeqNo string, d []byte) (*protobuf.CommunityDescription, error) {
const hashHexLength = 64
if len(keyIDSeqNo) <= hashHexLength {
return nil, errors.New("invalid keyIDSeqNo")
}
keyID, err := hex.DecodeString(keyIDSeqNo[:hashHexLength])
if err != nil {
return nil, err
}
seqNo, err := strconv.ParseUint(keyIDSeqNo[hashHexLength:], 10, 32)
if err != nil {
return nil, err
}
decryptedPayload, err := m.encryptor.DecryptWithHashRatchet(keyID, uint32(seqNo), d)
if err != nil {
return nil, err
}
var description protobuf.CommunityDescription
err = proto.Unmarshal(decryptedPayload, &description)
if err != nil {
return nil, err
}
return &description, nil
}
func ToLinkPreveiwThumbnail(image images.IdentityImage) (*common.LinkPreviewThumbnail, error) { func ToLinkPreveiwThumbnail(image images.IdentityImage) (*common.LinkPreviewThumbnail, error) {
thumbnail := &common.LinkPreviewThumbnail{} thumbnail := &common.LinkPreviewThumbnail{}

View File

@ -55,7 +55,7 @@ func (s *ManagerSuite) buildManager(ownerVerifier OwnerVerifier) *Manager {
key, err := crypto.GenerateKey() key, err := crypto.GenerateKey()
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NoError(err) s.Require().NoError(err)
m, err := NewManager(key, "", db, nil, nil, nil, ownerVerifier, nil, &TimeSourceStub{}, nil) m, err := NewManager(key, "", db, nil, nil, nil, ownerVerifier, nil, &TimeSourceStub{}, nil, nil)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NoError(m.Start()) s.Require().NoError(m.Start())
return m return m
@ -169,7 +169,7 @@ func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testCollect
WithTokenManager(tm), WithTokenManager(tm),
} }
m, err := NewManager(key, "", db, nil, nil, nil, nil, nil, &TimeSourceStub{}, nil, options...) m, err := NewManager(key, "", db, nil, nil, nil, nil, nil, &TimeSourceStub{}, nil, nil, options...)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NoError(m.Start()) s.Require().NoError(m.Start())

View File

@ -260,32 +260,7 @@ func (p *Persistence) queryCommunities(memberIdentity *ecdsa.PublicKey, query st
return nil, err return nil, err
} }
defer func() { return p.rowsToCommunities(rows)
if err != nil {
// Don't shadow original error
_ = rows.Close()
return
}
err = rows.Close()
}()
for rows.Next() {
r, err := scanCommunity(rows.Scan)
if err != nil {
return nil, err
}
org, err := p.recordBundleToCommunity(r)
if err != nil {
return nil, err
}
response = append(response, org)
}
return response, nil
} }
func (p *Persistence) AllCommunities(memberIdentity *ecdsa.PublicKey) ([]*Community, error) { func (p *Persistence) AllCommunities(memberIdentity *ecdsa.PublicKey) ([]*Community, error) {
@ -302,7 +277,7 @@ func (p *Persistence) SpectatedCommunities(memberIdentity *ecdsa.PublicKey) ([]*
return p.queryCommunities(memberIdentity, query) return p.queryCommunities(memberIdentity, query)
} }
func (p *Persistence) rowsToCommunities(memberIdentity *ecdsa.PublicKey, rows *sql.Rows) (comms []*Community, err error) { func (p *Persistence) rowsToCommunityRecords(rows *sql.Rows) (result []*CommunityRecordBundle, err error) {
defer func() { defer func() {
if err != nil { if err != nil {
// Don't shadow original error // Don't shadow original error
@ -318,8 +293,20 @@ func (p *Persistence) rowsToCommunities(memberIdentity *ecdsa.PublicKey, rows *s
if err != nil { if err != nil {
return nil, err return nil, err
} }
result = append(result, r)
}
org, err := p.recordBundleToCommunity(r) return result, nil
}
func (p *Persistence) rowsToCommunities(rows *sql.Rows) (comms []*Community, err error) {
records, err := p.rowsToCommunityRecords(rows)
if err != nil {
return nil, err
}
for _, record := range records {
org, err := p.recordBundleToCommunity(record)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -338,7 +325,7 @@ func (p *Persistence) JoinedAndPendingCommunitiesWithRequests(memberIdentity *ec
return nil, err return nil, err
} }
return p.rowsToCommunities(memberIdentity, rows) return p.rowsToCommunities(rows)
} }
func (p *Persistence) DeletedCommunities(memberIdentity *ecdsa.PublicKey) (comms []*Community, err error) { func (p *Persistence) DeletedCommunities(memberIdentity *ecdsa.PublicKey) (comms []*Community, err error) {
@ -349,7 +336,7 @@ func (p *Persistence) DeletedCommunities(memberIdentity *ecdsa.PublicKey) (comms
return nil, err return nil, err
} }
return p.rowsToCommunities(memberIdentity, rows) return p.rowsToCommunities(rows)
} }
func (p *Persistence) CommunitiesWithPrivateKey(memberIdentity *ecdsa.PublicKey) ([]*Community, error) { func (p *Persistence) CommunitiesWithPrivateKey(memberIdentity *ecdsa.PublicKey) ([]*Community, error) {

View File

@ -70,7 +70,7 @@ func recordToRequestToJoin(r *RequestToJoinRecord) *RequestToJoin {
} }
func recordBundleToCommunity(r *CommunityRecordBundle, memberIdentity *ecdsa.PublicKey, installationID string, func recordBundleToCommunity(r *CommunityRecordBundle, memberIdentity *ecdsa.PublicKey, installationID string,
logger *zap.Logger, timesource common.TimeSource, 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
var err error var err error
@ -135,7 +135,7 @@ func recordBundleToCommunity(r *CommunityRecordBundle, memberIdentity *ecdsa.Pub
Shard: s, Shard: s,
} }
community, err := New(config, timesource) community, err := New(config, timesource, encryptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -46,7 +46,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{}, nil) return recordBundleToCommunity(r, &s.identity.PublicKey, "", nil, &TimeSourceStub{}, &DescriptionEncryptorMock{}, nil)
}} }}
} }
@ -263,7 +263,7 @@ func (s *PersistenceSuite) makeNewCommunity(identity *ecdsa.PrivateKey) *Communi
ControlNode: &comPrivKey.PublicKey, ControlNode: &comPrivKey.PublicKey,
ControlDevice: true, ControlDevice: true,
ID: &comPrivKey.PublicKey, ID: &comPrivKey.PublicKey,
}, &TimeSourceStub{}) }, &TimeSourceStub{}, &DescriptionEncryptorMock{})
s.NoError(err, "New shouldn't give any error") s.NoError(err, "New shouldn't give any error")
md, err := com.MarshaledDescription() md, err := com.MarshaledDescription()

View File

@ -10,28 +10,34 @@ import (
"github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/protobuf"
) )
type CommunitiesKeyDistributor interface {
Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error
}
type CommunitiesKeyDistributorImpl struct { type CommunitiesKeyDistributorImpl struct {
sender *common.MessageSender sender *common.MessageSender
encryptor *encryption.Protocol encryptor *encryption.Protocol
} }
func (ckd *CommunitiesKeyDistributorImpl) Generate(community *communities.Community, keyActions *communities.EncryptionKeyActions) error {
if !community.IsControlNode() {
return communities.ErrNotControlNode
}
return iterateActions(community, keyActions, ckd.generateKey)
}
func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error { func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error {
if !community.IsControlNode() { if !community.IsControlNode() {
return communities.ErrNotControlNode return communities.ErrNotControlNode
} }
return iterateActions(community, keyActions, ckd.distributeKey)
}
err := ckd.distributeKey(community, community.ID(), &keyActions.CommunityKeyAction) func iterateActions(community *communities.Community, keyActions *communities.EncryptionKeyActions, fn func(community *communities.Community, hashRatchetGroupID []byte, keyAction *communities.EncryptionKeyAction) error) error {
err := fn(community, community.ID(), &keyActions.CommunityKeyAction)
if err != nil { if err != nil {
return err return err
} }
for channelID := range keyActions.ChannelKeysActions { for channelID := range keyActions.ChannelKeysActions {
keyAction := keyActions.ChannelKeysActions[channelID] keyAction := keyActions.ChannelKeysActions[channelID]
err := ckd.distributeKey(community, []byte(community.IDString()+channelID), &keyAction) err := fn(community, []byte(community.IDString()+channelID), &keyAction)
if err != nil { if err != nil {
return err return err
} }
@ -40,6 +46,14 @@ func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Comm
return nil return nil
} }
func (ckd *CommunitiesKeyDistributorImpl) generateKey(community *communities.Community, hashRatchetGroupID []byte, keyAction *communities.EncryptionKeyAction) error {
if keyAction.ActionType != communities.EncryptionKeyAdd {
return nil
}
_, err := ckd.encryptor.GenerateHashRatchetKey(hashRatchetGroupID)
return err
}
func (ckd *CommunitiesKeyDistributorImpl) distributeKey(community *communities.Community, hashRatchetGroupID []byte, keyAction *communities.EncryptionKeyAction) error { func (ckd *CommunitiesKeyDistributorImpl) distributeKey(community *communities.Community, hashRatchetGroupID []byte, keyAction *communities.EncryptionKeyAction) error {
pubkeys := make([]*ecdsa.PublicKey, len(keyAction.Members)) pubkeys := make([]*ecdsa.PublicKey, len(keyAction.Members))
i := 0 i := 0
@ -50,7 +64,11 @@ func (ckd *CommunitiesKeyDistributorImpl) distributeKey(community *communities.C
switch keyAction.ActionType { switch keyAction.ActionType {
case communities.EncryptionKeyAdd: case communities.EncryptionKeyAdd:
fallthrough // key must be already generated
err := ckd.sendKeyExchangeMessage(community, hashRatchetGroupID, pubkeys, common.KeyExMsgReuse)
if err != nil {
return err
}
case communities.EncryptionKeyRekey: case communities.EncryptionKeyRekey:
err := ckd.sendKeyExchangeMessage(community, hashRatchetGroupID, pubkeys, common.KeyExMsgRekey) err := ckd.sendKeyExchangeMessage(community, hashRatchetGroupID, pubkeys, common.KeyExMsgRekey)

View File

@ -47,6 +47,10 @@ type TestCommunitiesKeyDistributor struct {
mutex sync.RWMutex mutex sync.RWMutex
} }
func (tckd *TestCommunitiesKeyDistributor) Generate(community *communities.Community, keyActions *communities.EncryptionKeyActions) error {
return tckd.CommunitiesKeyDistributorImpl.Generate(community, keyActions)
}
func (tckd *TestCommunitiesKeyDistributor) Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error { func (tckd *TestCommunitiesKeyDistributor) Distribute(community *communities.Community, keyActions *communities.EncryptionKeyActions) error {
err := tckd.CommunitiesKeyDistributorImpl.Distribute(community, keyActions) err := tckd.CommunitiesKeyDistributorImpl.Distribute(community, keyActions)
if err != nil { if err != nil {
@ -659,12 +663,14 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions(
s.Require().Len(community.Members(), 1) s.Require().Len(community.Members(), 1)
// bob receives community changes // bob receives community changes
// chats and members should be empty,
// this info is available only to members
_, err = WaitOnMessengerResponse( _, err = WaitOnMessengerResponse(
s.bob, s.bob,
func(r *MessengerResponse) bool { func(r *MessengerResponse) bool {
return len(r.Communities()) > 0 return len(r.Communities()) > 0 && len(r.Communities()[0].Members()) == 0 && len(r.Communities()[0].Chats()) == 0
}, },
"no community", "no community that satisfies criteria",
) )
s.Require().NoError(err) s.Require().NoError(err)
@ -984,6 +990,22 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions()
err = <-waitOnChannelToBeRekeyedOnceBobIsKicked err = <-waitOnChannelToBeRekeyedOnceBobIsKicked
s.Require().NoError(err) s.Require().NoError(err)
// bob receives community changes
// channel members should be empty,
// this info is available only to channel members
_, err = WaitOnMessengerResponse(
s.bob,
func(r *MessengerResponse) bool {
if len(r.Communities()) == 0 {
return false
}
channel := r.Communities()[0].Chats()[chat.CommunityChatID()]
return channel != nil && len(channel.Members) == 0
},
"no community that satisfies criteria",
)
s.Require().NoError(err)
// send message to the channel // send message to the channel
msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel") msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel")

View File

@ -348,7 +348,7 @@ func (s *encryptor) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentit
ratchet.Timestamp = uint64(header.DeprecatedKeyId) ratchet.Timestamp = uint64(header.DeprecatedKeyId)
} }
decryptedPayload, err := s.decryptWithHR(ratchet, header.SeqNo, payload) decryptedPayload, err := s.DecryptWithHR(ratchet, header.SeqNo, payload)
return decryptedPayload, err return decryptedPayload, err
} }
@ -650,43 +650,11 @@ func (s *encryptor) EncryptHashRatchetPayload(ratchet *HashRatchetKeyCompatibili
defer s.mutex.Unlock() defer s.mutex.Unlock()
logger.Debug("encrypting hash ratchet message") logger.Debug("encrypting hash ratchet message")
dmp, err := s.encryptWithHR(ratchet, payload) encryptedPayload, newSeqNo, err := s.EncryptWithHR(ratchet, payload)
response := make(map[string]*EncryptedMessageProtocol)
response[noInstallationID] = dmp
return response, err
}
func samePublicKeys(pubKey1, pubKey2 ecdsa.PublicKey) bool {
return pubKey1.X.Cmp(pubKey2.X) == 0 && pubKey1.Y.Cmp(pubKey2.Y) == 0
}
func (s *encryptor) encryptWithHR(ratchet *HashRatchetKeyCompatibility, payload []byte) (*EncryptedMessageProtocol, error) {
hrCache, err := s.persistence.GetHashRatchetKeyByID(ratchet, 0) // Get latest seqNo
if err != nil { if err != nil {
return nil, err return nil, err
} }
if hrCache == nil {
return nil, errors.New("no encryption key found for the community")
}
var dbHash []byte
if len(hrCache.Hash) == 0 {
dbHash = hrCache.Key
} else {
dbHash = hrCache.Hash
}
hash := crypto.Keccak256Hash(dbHash)
encryptedPayload, err := crypto.EncryptSymmetric(hash.Bytes(), payload)
if err != nil {
return nil, err
}
newSeqNo := hrCache.SeqNo + 1
err = s.persistence.SaveHashRatchetKeyHash(ratchet, hash.Bytes(), newSeqNo)
if err != nil {
return nil, err
}
keyID, err := ratchet.GetKeyID() keyID, err := ratchet.GetKeyID()
if err != nil { if err != nil {
return nil, err return nil, err
@ -701,16 +669,54 @@ func (s *encryptor) encryptWithHR(ratchet *HashRatchetKeyCompatibility, payload
}, },
Payload: encryptedPayload, Payload: encryptedPayload,
} }
return dmp, nil
response := make(map[string]*EncryptedMessageProtocol)
response[noInstallationID] = dmp
return response, err
} }
func (s *encryptor) decryptWithHR(ratchet *HashRatchetKeyCompatibility, seqNo uint32, payload []byte) ([]byte, error) { func samePublicKeys(pubKey1, pubKey2 ecdsa.PublicKey) bool {
return pubKey1.X.Cmp(pubKey2.X) == 0 && pubKey1.Y.Cmp(pubKey2.Y) == 0
}
func (s *encryptor) EncryptWithHR(ratchet *HashRatchetKeyCompatibility, payload []byte) ([]byte, uint32, error) {
hrCache, err := s.persistence.GetHashRatchetCache(ratchet, 0) // Get latest seqNo
if err != nil {
return nil, 0, err
}
if hrCache == nil {
return nil, 0, errors.New("no encryption key found for the community")
}
var dbHash []byte
if len(hrCache.Hash) == 0 {
dbHash = hrCache.Key
} else {
dbHash = hrCache.Hash
}
hash := crypto.Keccak256Hash(dbHash)
encryptedPayload, err := crypto.EncryptSymmetric(hash.Bytes(), payload)
if err != nil {
return nil, 0, err
}
newSeqNo := hrCache.SeqNo + 1
err = s.persistence.SaveHashRatchetKeyHash(ratchet, hash.Bytes(), newSeqNo)
if err != nil {
return nil, 0, err
}
return encryptedPayload, newSeqNo, nil
}
func (s *encryptor) DecryptWithHR(ratchet *HashRatchetKeyCompatibility, seqNo uint32, payload []byte) ([]byte, error) {
// Key exchange message, nothing to decrypt // Key exchange message, nothing to decrypt
if seqNo == 0 { if seqNo == 0 {
return payload, nil return payload, nil
} }
hrCache, err := s.persistence.GetHashRatchetKeyByID(ratchet, seqNo) hrCache, err := s.persistence.GetHashRatchetCache(ratchet, seqNo)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -738,10 +738,10 @@ type HRCache struct {
SeqNo uint32 SeqNo uint32
} }
// GetHashRatchetKeyByID retrieves a hash ratchet key by group ID and seqNo. // GetHashRatchetCache retrieves a hash ratchet key by group ID and seqNo.
// If cache data with given seqNo (e.g. 0) is not found, // If cache data with given seqNo (e.g. 0) is not found,
// then the query will return the cache data with the latest seqNo // then the query will return the cache data with the latest seqNo
func (s *sqlitePersistence) GetHashRatchetKeyByID(ratchet *HashRatchetKeyCompatibility, seqNo uint32) (*HRCache, error) { func (s *sqlitePersistence) GetHashRatchetCache(ratchet *HashRatchetKeyCompatibility, seqNo uint32) (*HRCache, error) {
stmt, err := s.DB.Prepare(`WITH input AS ( stmt, err := s.DB.Prepare(`WITH input AS (
select ? AS group_id, ? AS key_id, ? as seq_no, ? AS old_key_id select ? AS group_id, ? AS key_id, ? as seq_no, ? AS old_key_id
), ),
@ -983,3 +983,23 @@ func (s *sqlitePersistence) SaveHashRatchetKey(ratchet *HashRatchetKeyCompatibil
return err return err
} }
func (s *sqlitePersistence) GetHashRatchetKeyByID(keyID []byte) (*HashRatchetKeyCompatibility, error) {
ratchet := &HashRatchetKeyCompatibility{
keyID: keyID,
}
err := s.DB.QueryRow(`
SELECT group_id, key_timestamp, key
FROM hash_ratchet_encryption
WHERE key_id = ?`, keyID).Scan(&ratchet.GroupID, &ratchet.Timestamp, &ratchet.Key)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return ratchet, nil
}

View File

@ -1,6 +1,7 @@
package encryption package encryption
import ( import (
"reflect"
"testing" "testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -336,3 +337,27 @@ func (s *SQLLitePersistenceTestSuite) TestRatchetInfoNoBundle() {
} }
// TODO: Add test for MarkBundleExpired // TODO: Add test for MarkBundleExpired
func (s *SQLLitePersistenceTestSuite) TestGetHashRatchetKeyByID() {
key := &HashRatchetKeyCompatibility{
GroupID: []byte{1, 2, 3},
keyID: []byte{4, 5, 6},
Timestamp: 1,
Key: []byte{7, 8, 9},
}
err := s.service.SaveHashRatchetKey(key)
s.Require().NoError(err)
retrievedKey, err := s.service.GetHashRatchetKeyByID(key.keyID)
s.Require().NoError(err)
s.Require().True(reflect.DeepEqual(key.GroupID, retrievedKey.GroupID))
s.Require().True(reflect.DeepEqual(key.keyID, retrievedKey.keyID))
s.Require().True(reflect.DeepEqual(key.Key, retrievedKey.Key))
s.Require().Equal(key.Timestamp, retrievedKey.Timestamp)
cachedKey, err := s.service.GetHashRatchetCache(retrievedKey, 0)
s.Require().NoError(err)
s.Require().True(reflect.DeepEqual(key.keyID, cachedKey.KeyID))
s.Require().True(reflect.DeepEqual(key.Key, cachedKey.Key))
s.Require().EqualValues(0, cachedKey.SeqNo)
}

View File

@ -751,3 +751,29 @@ func getProtocolVersion(bundles []*Bundle, installationID string) uint32 {
return defaultMinVersion return defaultMinVersion
} }
func (p *Protocol) EncryptWithHashRatchet(groupID []byte, payload []byte) ([]byte, *HashRatchetKeyCompatibility, uint32, error) {
ratchet, err := p.encryptor.persistence.GetCurrentKeyForGroup(groupID)
if err != nil {
return nil, nil, 0, err
}
encryptedPayload, newSeqNo, err := p.encryptor.EncryptWithHR(ratchet, payload)
if err != nil {
return nil, nil, 0, err
}
return encryptedPayload, ratchet, newSeqNo, nil
}
func (p *Protocol) DecryptWithHashRatchet(keyID []byte, seqNo uint32, payload []byte) ([]byte, error) {
ratchet, err := p.encryptor.persistence.GetHashRatchetKeyByID(keyID)
if err != nil {
return nil, err
}
if ratchet == nil {
return nil, errors.New("no ratchet key for given keyID")
}
return p.encryptor.DecryptWithHR(ratchet, seqNo, payload)
}

View File

@ -111,7 +111,7 @@ type Messenger struct {
pushNotificationClient *pushnotificationclient.Client pushNotificationClient *pushnotificationclient.Client
pushNotificationServer *pushnotificationserver.Server pushNotificationServer *pushnotificationserver.Server
communitiesManager *communities.Manager communitiesManager *communities.Manager
communitiesKeyDistributor CommunitiesKeyDistributor communitiesKeyDistributor communities.KeyDistributor
accountsManager account.Manager accountsManager account.Manager
mentionsManager *MentionManager mentionsManager *MentionManager
storeNodeRequestsManager *StoreNodeRequestManager storeNodeRequestsManager *StoreNodeRequestManager
@ -467,7 +467,12 @@ func NewMessenger(
managerOptions = append(managerOptions, communities.WithCommunityTokensService(c.communityTokensService)) managerOptions = append(managerOptions, communities.WithCommunityTokensService(c.communityTokensService))
} }
communitiesManager, err := communities.NewManager(identity, installationID, database, encryptionProtocol, logger, ensVerifier, c.communityTokensService, transp, transp, c.torrentConfig, managerOptions...) communitiesKeyDistributor := &CommunitiesKeyDistributorImpl{
sender: sender,
encryptor: encryptionProtocol,
}
communitiesManager, err := communities.NewManager(identity, installationID, database, encryptionProtocol, logger, ensVerifier, c.communityTokensService, transp, transp, communitiesKeyDistributor, c.torrentConfig, managerOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -487,24 +492,21 @@ func NewMessenger(
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
messenger = &Messenger{ messenger = &Messenger{
config: &c, config: &c,
node: node, node: node,
identity: identity, identity: identity,
persistence: sqlitePersistence, persistence: sqlitePersistence,
transport: transp, transport: transp,
encryptor: encryptionProtocol, encryptor: encryptionProtocol,
sender: sender, sender: sender,
anonMetricsClient: anonMetricsClient, anonMetricsClient: anonMetricsClient,
anonMetricsServer: anonMetricsServer, anonMetricsServer: anonMetricsServer,
telemetryClient: telemetryClient, telemetryClient: telemetryClient,
communityTokensService: c.communityTokensService, communityTokensService: c.communityTokensService,
pushNotificationClient: pushNotificationClient, pushNotificationClient: pushNotificationClient,
pushNotificationServer: pushNotificationServer, pushNotificationServer: pushNotificationServer,
communitiesManager: communitiesManager, communitiesManager: communitiesManager,
communitiesKeyDistributor: &CommunitiesKeyDistributorImpl{ communitiesKeyDistributor: communitiesKeyDistributor,
sender: sender,
encryptor: encryptionProtocol,
},
accountsManager: accountsManager, accountsManager: accountsManager,
ensVerifier: ensVerifier, ensVerifier: ensVerifier,
featureFlags: c.featureFlags, featureFlags: c.featureFlags,

View File

@ -292,7 +292,16 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti
}() }()
publishOrgAndDistributeEncryptionKeys := func(community *communities.Community) { publishOrgAndDistributeEncryptionKeys := func(community *communities.Community) {
err := m.publishOrg(community) recentlyPublishedOrg := recentlyPublishedOrgs[community.IDString()]
// evaluate and distribute encryption keys (if any)
encryptionKeyActions := communities.EvaluateCommunityEncryptionKeyActions(recentlyPublishedOrg, community)
err := m.communitiesKeyDistributor.Distribute(community, encryptionKeyActions)
if err != nil {
m.logger.Warn("failed to distribute encryption keys", zap.Error(err))
}
err = m.publishOrg(community)
if err != nil { if err != nil {
m.logger.Warn("failed to publish org", zap.Error(err)) m.logger.Warn("failed to publish org", zap.Error(err))
return return
@ -307,8 +316,6 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti
} }
m.logger.Debug("published public shard info") m.logger.Debug("published public shard info")
recentlyPublishedOrg := recentlyPublishedOrgs[community.IDString()]
// signal client with published community // signal client with published community
if m.config.messengerSignalsHandler != nil { if m.config.messengerSignalsHandler != nil {
if recentlyPublishedOrg == nil || community.Clock() > recentlyPublishedOrg.Clock() { if recentlyPublishedOrg == nil || community.Clock() > recentlyPublishedOrg.Clock() {
@ -318,13 +325,6 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti
} }
} }
// evaluate and distribute encryption keys (if any)
encryptionKeyActions := communities.EvaluateCommunityEncryptionKeyActions(recentlyPublishedOrg, community)
err = m.communitiesKeyDistributor.Distribute(community, encryptionKeyActions)
if err != nil {
m.logger.Warn("failed to distribute encryption keys", zap.Error(err))
}
recentlyPublishedOrgs[community.IDString()] = community.CreateDeepCopy() recentlyPublishedOrgs[community.IDString()] = community.CreateDeepCopy()
} }

View File

@ -568,9 +568,11 @@ type CommunityDescription struct {
CommunityTokensMetadata []*CommunityTokenMetadata `protobuf:"bytes,16,rep,name=community_tokens_metadata,json=communityTokensMetadata,proto3" json:"community_tokens_metadata,omitempty"` CommunityTokensMetadata []*CommunityTokenMetadata `protobuf:"bytes,16,rep,name=community_tokens_metadata,json=communityTokensMetadata,proto3" json:"community_tokens_metadata,omitempty"`
ActiveMembersCount uint64 `protobuf:"varint,17,opt,name=active_members_count,json=activeMembersCount,proto3" json:"active_members_count,omitempty"` ActiveMembersCount uint64 `protobuf:"varint,17,opt,name=active_members_count,json=activeMembersCount,proto3" json:"active_members_count,omitempty"`
ID string `protobuf:"bytes,18,opt,name=ID,proto3" json:"ID,omitempty"` ID string `protobuf:"bytes,18,opt,name=ID,proto3" json:"ID,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` // key is hash ratchet key_id + seq_no
XXX_unrecognized []byte `json:"-"` PrivateData map[string][]byte `protobuf:"bytes,100,rep,name=privateData,proto3" json:"privateData,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
XXX_sizecache int32 `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
} }
func (m *CommunityDescription) Reset() { *m = CommunityDescription{} } func (m *CommunityDescription) Reset() { *m = CommunityDescription{} }
@ -718,6 +720,13 @@ func (m *CommunityDescription) GetID() string {
return "" return ""
} }
func (m *CommunityDescription) GetPrivateData() map[string][]byte {
if m != nil {
return m.PrivateData
}
return nil
}
type CommunityAdminSettings struct { type CommunityAdminSettings struct {
PinMessageAllMembersEnabled bool `protobuf:"varint,1,opt,name=pin_message_all_members_enabled,json=pinMessageAllMembersEnabled,proto3" json:"pin_message_all_members_enabled,omitempty"` PinMessageAllMembersEnabled bool `protobuf:"varint,1,opt,name=pin_message_all_members_enabled,json=pinMessageAllMembersEnabled,proto3" json:"pin_message_all_members_enabled,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
@ -1671,6 +1680,7 @@ func init() {
proto.RegisterMapType((map[string]*CommunityCategory)(nil), "protobuf.CommunityDescription.CategoriesEntry") proto.RegisterMapType((map[string]*CommunityCategory)(nil), "protobuf.CommunityDescription.CategoriesEntry")
proto.RegisterMapType((map[string]*CommunityChat)(nil), "protobuf.CommunityDescription.ChatsEntry") proto.RegisterMapType((map[string]*CommunityChat)(nil), "protobuf.CommunityDescription.ChatsEntry")
proto.RegisterMapType((map[string]*CommunityMember)(nil), "protobuf.CommunityDescription.MembersEntry") proto.RegisterMapType((map[string]*CommunityMember)(nil), "protobuf.CommunityDescription.MembersEntry")
proto.RegisterMapType((map[string][]byte)(nil), "protobuf.CommunityDescription.PrivateDataEntry")
proto.RegisterMapType((map[string]*CommunityTokenPermission)(nil), "protobuf.CommunityDescription.TokenPermissionsEntry") proto.RegisterMapType((map[string]*CommunityTokenPermission)(nil), "protobuf.CommunityDescription.TokenPermissionsEntry")
proto.RegisterType((*CommunityAdminSettings)(nil), "protobuf.CommunityAdminSettings") proto.RegisterType((*CommunityAdminSettings)(nil), "protobuf.CommunityAdminSettings")
proto.RegisterType((*CommunityChat)(nil), "protobuf.CommunityChat") proto.RegisterType((*CommunityChat)(nil), "protobuf.CommunityChat")
@ -1696,139 +1706,141 @@ func init() {
} }
var fileDescriptor_f937943d74c1cd8b = []byte{ var fileDescriptor_f937943d74c1cd8b = []byte{
// 2130 bytes of a gzipped FileDescriptorProto // 2166 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x58, 0x4f, 0x73, 0x23, 0x47, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x58, 0x51, 0x73, 0x23, 0x47,
0x15, 0xcf, 0x68, 0x24, 0x59, 0x7a, 0x92, 0x6c, 0xb9, 0xb3, 0x6b, 0xcf, 0x7a, 0x37, 0x59, 0xed, 0x11, 0xbe, 0xd5, 0x4a, 0xb6, 0xd4, 0x92, 0x6c, 0x79, 0x72, 0x67, 0xef, 0xf9, 0xee, 0x72, 0xba,
0x40, 0x0a, 0x67, 0x29, 0xb4, 0x89, 0x81, 0x62, 0x2b, 0x21, 0x7f, 0xb4, 0xb2, 0x58, 0x94, 0xb5, 0x85, 0x14, 0xce, 0x51, 0xe8, 0x12, 0x03, 0xc5, 0x55, 0x42, 0x2e, 0xd1, 0xc9, 0xe2, 0x50, 0xce,
0x46, 0xa6, 0x2d, 0x67, 0x49, 0x0a, 0x98, 0x6a, 0xcf, 0xb4, 0xed, 0xae, 0x95, 0x66, 0xc4, 0x74, 0x5a, 0x39, 0x63, 0x39, 0x47, 0x52, 0xc0, 0xd6, 0x78, 0x77, 0x6c, 0x4f, 0x9d, 0xb4, 0x2b, 0x76,
0xcb, 0x85, 0x38, 0x70, 0x00, 0x4e, 0x1c, 0xe1, 0x03, 0x70, 0xe0, 0x0e, 0x1f, 0x81, 0x03, 0x55, 0x46, 0x2e, 0xc4, 0x03, 0x0f, 0xc0, 0x2f, 0x80, 0x67, 0x8a, 0x07, 0xde, 0xe1, 0x27, 0xf0, 0x40,
0x1c, 0x73, 0xe7, 0x03, 0x70, 0xe7, 0x23, 0x50, 0xdd, 0x3d, 0x33, 0x9a, 0x91, 0xe4, 0xf5, 0x86, 0x15, 0x8f, 0x79, 0xe7, 0x07, 0xf0, 0xce, 0x4f, 0xa0, 0x66, 0x66, 0x77, 0xb5, 0x2b, 0xc9, 0xe7,
0x40, 0x15, 0x27, 0xcd, 0x7b, 0xfd, 0xfa, 0xf5, 0xeb, 0xf7, 0x7e, 0xfd, 0xfa, 0xd7, 0x82, 0x6d, 0x0b, 0x81, 0xaa, 0x3c, 0x69, 0xbb, 0xa7, 0xa7, 0xa7, 0xbb, 0xe7, 0xeb, 0x9e, 0x6e, 0xc1, 0x96,
0x2f, 0x9c, 0x4c, 0x66, 0x01, 0x13, 0x8c, 0xf2, 0xf6, 0x34, 0x0a, 0x45, 0x88, 0x2a, 0xea, 0xe7, 0x17, 0x8e, 0xc7, 0xd3, 0x80, 0x09, 0x46, 0x79, 0x6b, 0x12, 0x85, 0x22, 0x44, 0x65, 0xf5, 0x73,
0x6c, 0x76, 0xbe, 0xf7, 0xba, 0x77, 0x49, 0x84, 0xcb, 0x7c, 0x1a, 0x08, 0x26, 0xe6, 0x7a, 0x78, 0x3a, 0x3d, 0xdb, 0x7d, 0xc3, 0xbb, 0x20, 0xc2, 0x65, 0x3e, 0x0d, 0x04, 0x13, 0x33, 0xbd, 0xbc,
0xaf, 0x46, 0x83, 0xd9, 0x84, 0x27, 0x02, 0xbf, 0x24, 0x91, 0xaf, 0x05, 0xfb, 0x0a, 0x4a, 0x4f, 0x5b, 0xa5, 0xc1, 0x74, 0xcc, 0x13, 0x82, 0x5f, 0x90, 0xc8, 0xd7, 0x84, 0x7d, 0x09, 0xa5, 0x67,
0x23, 0x12, 0x08, 0xf4, 0x00, 0xea, 0x89, 0xdb, 0xb9, 0xcb, 0x7c, 0xcb, 0x68, 0x19, 0xfb, 0x75, 0x11, 0x09, 0x04, 0x7a, 0x00, 0xb5, 0x44, 0xed, 0xcc, 0x65, 0xbe, 0x65, 0x34, 0x8d, 0xbd, 0x1a,
0x5c, 0x4b, 0x75, 0x7d, 0x1f, 0xdd, 0x85, 0xea, 0x84, 0x4e, 0xce, 0x68, 0x24, 0xc7, 0x0b, 0x6a, 0xae, 0xa6, 0xbc, 0x9e, 0x8f, 0xee, 0x40, 0x65, 0x4c, 0xc7, 0xa7, 0x34, 0x92, 0xeb, 0x05, 0xb5,
0xbc, 0xa2, 0x15, 0x7d, 0x1f, 0xed, 0xc2, 0x46, 0xbc, 0xb2, 0x65, 0xb6, 0x8c, 0xfd, 0x2a, 0x2e, 0x5e, 0xd6, 0x8c, 0x9e, 0x8f, 0x76, 0x60, 0x3d, 0x3e, 0xd9, 0x32, 0x9b, 0xc6, 0x5e, 0x05, 0xaf,
0x4b, 0xb1, 0xef, 0xa3, 0x5b, 0x50, 0xf2, 0xc6, 0xa1, 0xf7, 0xc2, 0x2a, 0xb6, 0x8c, 0xfd, 0x22, 0x49, 0xb2, 0xe7, 0xa3, 0x9b, 0x50, 0xf2, 0x46, 0xa1, 0xf7, 0xd2, 0x2a, 0x36, 0x8d, 0xbd, 0x22,
0xd6, 0x82, 0xfd, 0xf7, 0x02, 0x6c, 0x75, 0x13, 0xdf, 0x03, 0xe5, 0x04, 0x7d, 0x17, 0x4a, 0x51, 0xd6, 0x84, 0xfd, 0x8f, 0x02, 0x6c, 0x76, 0x12, 0xdd, 0x7d, 0xa5, 0x04, 0x7d, 0x1f, 0x4a, 0x51,
0x38, 0xa6, 0xdc, 0x32, 0x5a, 0xe6, 0xfe, 0xe6, 0xc1, 0xfd, 0x76, 0xb2, 0xa9, 0xf6, 0x92, 0x65, 0x38, 0xa2, 0xdc, 0x32, 0x9a, 0xe6, 0xde, 0xc6, 0xfe, 0xfd, 0x56, 0xe2, 0x54, 0x6b, 0x41, 0xb2,
0x1b, 0x4b, 0x33, 0xac, 0xad, 0xd1, 0x27, 0xb0, 0x1d, 0xd1, 0x2b, 0x4a, 0xc6, 0xd4, 0x77, 0x89, 0x85, 0xa5, 0x18, 0xd6, 0xd2, 0xe8, 0x63, 0xd8, 0x8a, 0xe8, 0x25, 0x25, 0x23, 0xea, 0xbb, 0xc4,
0xe7, 0x85, 0xb3, 0x40, 0x70, 0xab, 0xd0, 0x32, 0xf7, 0x6b, 0x07, 0x77, 0x16, 0x2e, 0x70, 0x6c, 0xf3, 0xc2, 0x69, 0x20, 0xb8, 0x55, 0x68, 0x9a, 0x7b, 0xd5, 0xfd, 0xdb, 0x73, 0x15, 0x38, 0x16,
0xd2, 0xd1, 0x16, 0x4f, 0x0a, 0x96, 0x81, 0x9b, 0x51, 0x5e, 0xc9, 0xd1, 0x43, 0xd8, 0x1e, 0x13, 0x69, 0x6b, 0x89, 0xa7, 0x05, 0xcb, 0xc0, 0x8d, 0x28, 0xcf, 0xe4, 0xe8, 0x21, 0x6c, 0x8d, 0x08,
0x2e, 0xdc, 0xd9, 0xd4, 0x27, 0x82, 0xba, 0x3a, 0x70, 0x53, 0x05, 0xbe, 0x25, 0x07, 0x4e, 0x95, 0x17, 0xee, 0x74, 0xe2, 0x13, 0x41, 0x5d, 0x6d, 0xb8, 0xa9, 0x0c, 0xdf, 0x94, 0x0b, 0x27, 0x8a,
0xbe, 0xab, 0xb6, 0xf0, 0x1b, 0x03, 0x4a, 0x2a, 0x10, 0xd4, 0x80, 0x2a, 0x1e, 0x1e, 0xf5, 0x5c, 0xdf, 0x51, 0x2e, 0xfc, 0xd6, 0x80, 0x92, 0x32, 0x04, 0xd5, 0xa1, 0x82, 0x07, 0x87, 0x5d, 0xd7,
0x67, 0xe8, 0xf4, 0x9a, 0xaf, 0xa1, 0x4d, 0x00, 0x25, 0x0e, 0x9f, 0x3b, 0x3d, 0xdc, 0x34, 0x52, 0x19, 0x38, 0xdd, 0xc6, 0x0d, 0xb4, 0x01, 0xa0, 0xc8, 0xc1, 0x0b, 0xa7, 0x8b, 0x1b, 0x46, 0x4a,
0xb9, 0x73, 0x38, 0xe8, 0x3b, 0xcd, 0x22, 0xba, 0x0d, 0xdb, 0x4a, 0x1e, 0x0d, 0x9f, 0xf5, 0x1c, 0xb7, 0x0f, 0xfa, 0x3d, 0xa7, 0x51, 0x44, 0xb7, 0x60, 0x4b, 0xd1, 0xc3, 0xc1, 0xf3, 0xae, 0xe3,
0x77, 0xd0, 0x39, 0x19, 0xf5, 0x70, 0xb3, 0x64, 0x17, 0x2b, 0x85, 0x66, 0xc1, 0x2e, 0x56, 0xcc, 0xf6, 0xdb, 0xc7, 0xc3, 0x2e, 0x6e, 0x94, 0xec, 0x62, 0xb9, 0xd0, 0x28, 0xd8, 0xc5, 0xb2, 0xd9,
0xa6, 0xf9, 0x50, 0x1b, 0x0c, 0x3a, 0x4e, 0xe7, 0x69, 0xcf, 0x3d, 0x3d, 0xe9, 0xe1, 0x93, 0x87, 0x30, 0x1f, 0x6a, 0x81, 0x7e, 0xdb, 0x69, 0x3f, 0xeb, 0xba, 0x27, 0xc7, 0x5d, 0x7c, 0xfc, 0xf0,
0xb7, 0xb5, 0x6a, 0x78, 0xd8, 0xc3, 0x9d, 0x51, 0xcf, 0xed, 0x0e, 0x9d, 0x51, 0xcf, 0x19, 0xd9, 0x96, 0x66, 0x0d, 0x0e, 0xba, 0xb8, 0x3d, 0xec, 0xba, 0x9d, 0x81, 0x33, 0xec, 0x3a, 0x43, 0xfb,
0xbf, 0x36, 0x61, 0x27, 0x4d, 0xcf, 0x28, 0x7c, 0x41, 0x83, 0x01, 0x15, 0xc4, 0x27, 0x82, 0xa0, 0x37, 0x26, 0x6c, 0xa7, 0xe1, 0x19, 0x86, 0x2f, 0x69, 0xd0, 0xa7, 0x82, 0xf8, 0x44, 0x10, 0x74,
0x73, 0x40, 0x5e, 0x18, 0x88, 0x88, 0x78, 0xc2, 0x25, 0xbe, 0x1f, 0x51, 0xce, 0xe3, 0xe4, 0xd6, 0x06, 0xc8, 0x0b, 0x03, 0x11, 0x11, 0x4f, 0xb8, 0xc4, 0xf7, 0x23, 0xca, 0x79, 0x1c, 0xdc, 0xea,
0x0e, 0xbe, 0xb7, 0x26, 0xb9, 0xb9, 0xd9, 0xed, 0x6e, 0x3c, 0xb5, 0x93, 0xcc, 0xec, 0x05, 0x22, 0xfe, 0x0f, 0x56, 0x04, 0x37, 0xb7, 0xbb, 0xd5, 0x89, 0xb7, 0xb6, 0x93, 0x9d, 0xdd, 0x40, 0x44,
0x9a, 0xe3, 0x6d, 0x6f, 0x59, 0x8f, 0x5a, 0x50, 0xf3, 0x29, 0xf7, 0x22, 0x36, 0x15, 0x2c, 0x0c, 0x33, 0xbc, 0xe5, 0x2d, 0xf2, 0x51, 0x13, 0xaa, 0x3e, 0xe5, 0x5e, 0xc4, 0x26, 0x82, 0x85, 0x81,
0x14, 0x32, 0xaa, 0x38, 0xab, 0x92, 0x18, 0x60, 0x13, 0x72, 0x41, 0x63, 0x68, 0x68, 0x01, 0xbd, 0x42, 0x46, 0x05, 0x67, 0x59, 0x12, 0x03, 0x6c, 0x4c, 0xce, 0x69, 0x0c, 0x0d, 0x4d, 0xa0, 0xf7,
0x07, 0x55, 0x21, 0x97, 0x1c, 0xcd, 0xa7, 0x54, 0xa1, 0x63, 0xf3, 0xe0, 0xde, 0x75, 0x61, 0x49, 0xa0, 0x22, 0xe4, 0x91, 0xc3, 0xd9, 0x84, 0x2a, 0x74, 0x6c, 0xec, 0xdf, 0xbd, 0xca, 0x2c, 0x29,
0x1b, 0xbc, 0x30, 0x47, 0x3b, 0x50, 0xe6, 0xf3, 0xc9, 0x59, 0x38, 0xb6, 0x4a, 0x1a, 0x6d, 0x5a, 0x83, 0xe7, 0xe2, 0x68, 0x1b, 0xd6, 0xf8, 0x6c, 0x7c, 0x1a, 0x8e, 0xac, 0x92, 0x46, 0x9b, 0xa6,
0x42, 0x08, 0x8a, 0x01, 0x99, 0x50, 0xab, 0xac, 0xb4, 0xea, 0x1b, 0xed, 0x41, 0xc5, 0xa7, 0x1e, 0x10, 0x82, 0x62, 0x40, 0xc6, 0xd4, 0x5a, 0x53, 0x5c, 0xf5, 0x8d, 0x76, 0xa1, 0xec, 0x53, 0x8f,
0x9b, 0x90, 0x31, 0xb7, 0x36, 0x5a, 0xc6, 0x7e, 0x03, 0xa7, 0xf2, 0xde, 0xa1, 0xcc, 0xde, 0xba, 0x8d, 0xc9, 0x88, 0x5b, 0xeb, 0x4d, 0x63, 0xaf, 0x8e, 0x53, 0x7a, 0xf7, 0x40, 0x46, 0x6f, 0x95,
0x8d, 0xa2, 0x26, 0x98, 0x2f, 0xe8, 0x5c, 0x9d, 0x83, 0x22, 0x96, 0x9f, 0x72, 0x17, 0x57, 0x64, 0xa3, 0xa8, 0x01, 0xe6, 0x4b, 0x3a, 0x53, 0x79, 0x50, 0xc4, 0xf2, 0x53, 0x7a, 0x71, 0x49, 0x46,
0x3c, 0xa3, 0xf1, 0x0e, 0xb5, 0xf0, 0x5e, 0xe1, 0xb1, 0x61, 0xff, 0xd3, 0x80, 0x5b, 0x69, 0xbc, 0x53, 0x1a, 0x7b, 0xa8, 0x89, 0xf7, 0x0a, 0x8f, 0x0d, 0xfb, 0x5f, 0x06, 0xdc, 0x4c, 0xed, 0x3d,
0xc7, 0x34, 0x9a, 0x30, 0xce, 0x59, 0x18, 0x70, 0x74, 0x07, 0x2a, 0x34, 0xe0, 0x6e, 0x18, 0x8c, 0xa2, 0xd1, 0x98, 0x71, 0xce, 0xc2, 0x80, 0xa3, 0xdb, 0x50, 0xa6, 0x01, 0x77, 0xc3, 0x60, 0xa4,
0xb5, 0xa7, 0x0a, 0xde, 0xa0, 0x01, 0x1f, 0x06, 0xe3, 0x39, 0xb2, 0x60, 0x63, 0x1a, 0xb1, 0x2b, 0x35, 0x95, 0xf1, 0x3a, 0x0d, 0xf8, 0x20, 0x18, 0xcd, 0x90, 0x05, 0xeb, 0x93, 0x88, 0x5d, 0x12,
0x22, 0xb4, 0xbf, 0x0a, 0x4e, 0x44, 0xf4, 0x01, 0x94, 0x89, 0xe7, 0x51, 0xce, 0x55, 0xba, 0x36, 0xa1, 0xf5, 0x95, 0x71, 0x42, 0xa2, 0x0f, 0x60, 0x8d, 0x78, 0x1e, 0xe5, 0x5c, 0x85, 0x6b, 0x63,
0x0f, 0xde, 0x5a, 0x93, 0x94, 0xcc, 0x22, 0xed, 0x8e, 0x32, 0xc6, 0xf1, 0x24, 0xfb, 0x33, 0x28, 0xff, 0xad, 0x15, 0x41, 0xc9, 0x1c, 0xd2, 0x6a, 0x2b, 0x61, 0x1c, 0x6f, 0xb2, 0x3f, 0x83, 0x35,
0x6b, 0x0d, 0x42, 0xb0, 0x79, 0xea, 0x3c, 0x73, 0x86, 0xcf, 0x1d, 0xb7, 0xd3, 0xed, 0xf6, 0x4e, 0xcd, 0x41, 0x08, 0x36, 0x4e, 0x9c, 0xe7, 0xce, 0xe0, 0x85, 0xe3, 0xb6, 0x3b, 0x9d, 0xee, 0xf1,
0x4e, 0x9a, 0xaf, 0xa1, 0x2d, 0xa8, 0x75, 0x4e, 0x47, 0x43, 0xa5, 0x38, 0x1e, 0x35, 0x0d, 0xb4, 0x71, 0xe3, 0x06, 0xda, 0x84, 0x6a, 0xfb, 0x64, 0x38, 0x50, 0x8c, 0xa3, 0x61, 0xc3, 0x40, 0x3b,
0x0b, 0x5b, 0x7d, 0xe7, 0xd3, 0xfe, 0xa8, 0x33, 0xea, 0x0f, 0x1d, 0x77, 0xe8, 0x1c, 0x7d, 0xd6, 0xb0, 0xd9, 0x73, 0x3e, 0xed, 0x0d, 0xdb, 0xc3, 0xde, 0xc0, 0x71, 0x07, 0xce, 0xe1, 0x67, 0x8d,
0x2c, 0xec, 0x15, 0x2a, 0x06, 0xda, 0x86, 0xc6, 0xa0, 0xe3, 0x9c, 0x76, 0x8e, 0x12, 0x5b, 0xd3, 0xc2, 0x6e, 0xa1, 0x6c, 0xa0, 0x2d, 0xa8, 0xf7, 0xdb, 0xce, 0x49, 0xfb, 0x30, 0x91, 0x35, 0xed,
0xfe, 0xad, 0x09, 0x0d, 0x55, 0x8e, 0x6e, 0xc4, 0x04, 0x8d, 0x18, 0x41, 0x3f, 0x7d, 0x09, 0xc6, 0xdf, 0x99, 0x50, 0x57, 0xd7, 0xd1, 0x89, 0x98, 0xa0, 0x11, 0x23, 0xe8, 0x67, 0xaf, 0xc0, 0x58,
0xda, 0x8b, 0xb8, 0x73, 0x93, 0xbe, 0x04, 0xb4, 0xde, 0x81, 0xa2, 0x90, 0xe8, 0x28, 0xbc, 0x02, 0x6b, 0x6e, 0x77, 0x6e, 0xd3, 0x97, 0x80, 0xd6, 0x3b, 0x50, 0x14, 0x12, 0x1d, 0x85, 0xd7, 0x40,
0x3a, 0x94, 0x65, 0x06, 0x18, 0xe6, 0x5a, 0x60, 0x14, 0x33, 0xc0, 0xd8, 0x81, 0x32, 0x99, 0xc8, 0x87, 0x92, 0xcc, 0x00, 0xc3, 0x5c, 0x09, 0x8c, 0x62, 0x06, 0x18, 0xdb, 0xb0, 0x46, 0xc6, 0x32,
0x83, 0x9f, 0x80, 0x48, 0x4b, 0xb2, 0xd1, 0x29, 0xa4, 0xb9, 0xcc, 0xe7, 0x56, 0xb9, 0x65, 0xee, 0xf1, 0x13, 0x10, 0x69, 0x4a, 0x16, 0x3a, 0x85, 0x34, 0x97, 0xf9, 0xdc, 0x5a, 0x6b, 0x9a, 0x7b,
0x17, 0x71, 0x45, 0x29, 0xfa, 0x3e, 0x47, 0xf7, 0xa1, 0x26, 0x4b, 0x3a, 0x25, 0x42, 0xd0, 0x28, 0x45, 0x5c, 0x56, 0x8c, 0x9e, 0xcf, 0xd1, 0x7d, 0xa8, 0xca, 0x2b, 0x9d, 0x10, 0x21, 0x68, 0x14,
0x50, 0x80, 0xaa, 0x62, 0xa0, 0x01, 0x3f, 0xd6, 0x9a, 0x1c, 0xdc, 0x2a, 0x0a, 0x3d, 0xff, 0x6d, 0x28, 0x40, 0x55, 0x30, 0xd0, 0x80, 0x1f, 0x69, 0x4e, 0x0e, 0x6e, 0x65, 0x85, 0x9e, 0xff, 0x35,
0xb8, 0xfd, 0xc1, 0x04, 0x2b, 0x9f, 0x80, 0x05, 0x1c, 0xd0, 0x26, 0x14, 0xe2, 0xf6, 0x5d, 0xc5, 0xdc, 0xfe, 0x60, 0x82, 0x95, 0x0f, 0xc0, 0x1c, 0x0e, 0x68, 0x03, 0x0a, 0x71, 0xf9, 0xae, 0xe0,
0x05, 0xe6, 0xa3, 0xf7, 0x73, 0x29, 0xfc, 0xc6, 0x75, 0x29, 0x5c, 0x78, 0x68, 0x67, 0xb2, 0xf9, 0x02, 0xf3, 0xd1, 0xfb, 0xb9, 0x10, 0x7e, 0xeb, 0xaa, 0x10, 0xce, 0x35, 0xb4, 0x32, 0xd1, 0x7c,
0x21, 0x6c, 0xea, 0x4c, 0x78, 0x71, 0xed, 0x2c, 0x53, 0x95, 0x76, 0xf7, 0x9a, 0xd2, 0xe2, 0x86, 0x02, 0x1b, 0x3a, 0x12, 0x5e, 0x7c, 0x77, 0x96, 0xa9, 0xae, 0x76, 0xe7, 0x8a, 0xab, 0xc5, 0x75,
0xc8, 0xc1, 0xe3, 0x0e, 0x54, 0xe2, 0x5b, 0x81, 0x5b, 0xc5, 0x96, 0xb9, 0x5f, 0xc5, 0x1b, 0xfa, 0x91, 0x83, 0xc7, 0x6d, 0x28, 0xc7, 0xaf, 0x02, 0xb7, 0x8a, 0x4d, 0x73, 0xaf, 0x82, 0xd7, 0xf5,
0x5a, 0xe0, 0xe8, 0x0d, 0x00, 0xc6, 0xdd, 0xe4, 0x08, 0x94, 0xd4, 0x11, 0xa8, 0x32, 0x7e, 0xac, 0xb3, 0xc0, 0xd1, 0x3d, 0x00, 0xc6, 0xdd, 0x24, 0x05, 0x4a, 0x2a, 0x05, 0x2a, 0x8c, 0x1f, 0x69,
0x15, 0xf6, 0x5f, 0x0c, 0x28, 0xaa, 0x93, 0x7e, 0x0f, 0xac, 0x04, 0xc4, 0xba, 0x61, 0x1e, 0xf7, 0x86, 0xfd, 0x57, 0x03, 0x8a, 0x2a, 0xd3, 0xef, 0x82, 0x95, 0x80, 0x58, 0x17, 0xcc, 0xa3, 0x2e,
0xf0, 0xa0, 0x7f, 0x72, 0xd2, 0x1f, 0x3a, 0xcd, 0xd7, 0x50, 0x13, 0xea, 0x4f, 0x7a, 0xdd, 0xe1, 0xee, 0xf7, 0x8e, 0x8f, 0x7b, 0x03, 0xa7, 0x71, 0x03, 0x35, 0xa0, 0xf6, 0xb4, 0xdb, 0x19, 0xf4,
0x20, 0xe9, 0xae, 0x0a, 0xb6, 0xb1, 0x66, 0xd0, 0x1b, 0x3c, 0xe9, 0xe1, 0x66, 0x01, 0xdd, 0x82, 0x93, 0xea, 0xaa, 0x60, 0x1b, 0x73, 0xfa, 0xdd, 0xfe, 0xd3, 0x2e, 0x6e, 0x14, 0xd0, 0x4d, 0x68,
0x66, 0xb7, 0xe3, 0xb8, 0x9f, 0xf6, 0x7b, 0xcf, 0xdd, 0xee, 0x0f, 0x3b, 0x8e, 0xd3, 0x3b, 0x6a, 0x74, 0xda, 0x8e, 0xfb, 0x69, 0xaf, 0xfb, 0xc2, 0xed, 0xfc, 0xb8, 0xed, 0x38, 0xdd, 0xc3, 0x86,
0x9a, 0xe8, 0x0d, 0xb8, 0x93, 0x6a, 0x3b, 0xce, 0xa1, 0x7b, 0x3c, 0x3c, 0x19, 0xa5, 0xc3, 0x45, 0x89, 0xee, 0xc1, 0xed, 0x94, 0xdb, 0x76, 0x0e, 0xdc, 0xa3, 0xc1, 0xf1, 0x30, 0x5d, 0x2e, 0xa2,
0xb4, 0x0b, 0xaf, 0xc7, 0x7e, 0xf2, 0x7d, 0x1a, 0xed, 0x00, 0xca, 0x0d, 0xe8, 0x36, 0x5f, 0xb6, 0x1d, 0x78, 0x23, 0xd6, 0x93, 0xaf, 0xd3, 0x68, 0x1b, 0x50, 0x6e, 0x41, 0x97, 0xf9, 0x35, 0xfb,
0x7f, 0x07, 0x99, 0x26, 0x70, 0x98, 0xef, 0x7e, 0xfa, 0x22, 0x31, 0x32, 0x37, 0x20, 0xea, 0xc1, 0x8f, 0xd5, 0x4c, 0x11, 0x38, 0xc8, 0x57, 0x3f, 0xfd, 0x90, 0x18, 0x99, 0x17, 0x10, 0x75, 0x61,
0x86, 0xbe, 0x3c, 0x93, 0xcb, 0xea, 0x9b, 0x6b, 0x4a, 0x93, 0x71, 0xd3, 0xd6, 0x77, 0x5f, 0x7c, 0x5d, 0x3f, 0x9e, 0xc9, 0x63, 0xf5, 0xed, 0x15, 0x57, 0x93, 0x51, 0xd3, 0xd2, 0x6f, 0x5f, 0x9c,
0x56, 0x92, 0xb9, 0xe8, 0x63, 0xa8, 0x4d, 0x17, 0xbd, 0x40, 0x81, 0xbe, 0x76, 0xf0, 0xe6, 0xcb, 0x2b, 0xc9, 0x5e, 0xf4, 0x11, 0x54, 0x27, 0xf3, 0x5a, 0xa0, 0x40, 0x5f, 0xdd, 0x7f, 0xf3, 0xd5,
0x3b, 0x06, 0xce, 0x4e, 0x41, 0x07, 0x50, 0x49, 0xe8, 0x82, 0x2a, 0x43, 0xed, 0x60, 0x27, 0x33, 0x15, 0x03, 0x67, 0xb7, 0xa0, 0x7d, 0x28, 0x27, 0xed, 0x82, 0xba, 0x86, 0xea, 0xfe, 0x76, 0x66,
0x5d, 0x55, 0x4b, 0x8f, 0xe2, 0xd4, 0x0e, 0x7d, 0x04, 0x25, 0x59, 0x47, 0x7d, 0x3a, 0x6a, 0x07, 0xbb, 0xba, 0x2d, 0xbd, 0x8a, 0x53, 0x39, 0xf4, 0x21, 0x94, 0xe4, 0x3d, 0xea, 0xec, 0xa8, 0xee,
0x6f, 0xdf, 0x10, 0xba, 0xf4, 0x12, 0x07, 0xae, 0xe7, 0x49, 0x60, 0x9c, 0x91, 0xc0, 0x1d, 0x33, 0xbf, 0x7d, 0x8d, 0xe9, 0x52, 0x4b, 0x6c, 0xb8, 0xde, 0x27, 0x81, 0x71, 0x4a, 0x02, 0x77, 0xc4,
0x2e, 0xac, 0x0d, 0x0d, 0x8c, 0x33, 0x12, 0x1c, 0x31, 0x2e, 0x90, 0x03, 0xe0, 0x11, 0x41, 0x2f, 0xb8, 0xb0, 0xd6, 0x35, 0x30, 0x4e, 0x49, 0x70, 0xc8, 0xb8, 0x40, 0x0e, 0x80, 0x47, 0x04, 0x3d,
0xc2, 0x88, 0x51, 0x79, 0x82, 0x96, 0x5a, 0xc9, 0xfa, 0x05, 0xd2, 0x09, 0x7a, 0x95, 0x8c, 0x07, 0x0f, 0x23, 0x46, 0x65, 0x06, 0x2d, 0x94, 0x92, 0xd5, 0x07, 0xa4, 0x1b, 0xf4, 0x29, 0x19, 0x0d,
0xf4, 0x18, 0x2c, 0x12, 0x79, 0x97, 0xec, 0x8a, 0xba, 0x13, 0x72, 0x11, 0x50, 0x31, 0x66, 0xc1, 0xe8, 0x31, 0x58, 0x24, 0xf2, 0x2e, 0xd8, 0x25, 0x75, 0xc7, 0xe4, 0x3c, 0xa0, 0x62, 0xc4, 0x82,
0x8b, 0xf8, 0x6a, 0xaf, 0xaa, 0x8a, 0xec, 0xc4, 0xe3, 0x83, 0x74, 0x58, 0xdd, 0xf0, 0xe8, 0x29, 0x97, 0xf1, 0xd3, 0x5e, 0x51, 0x37, 0xb2, 0x1d, 0xaf, 0xf7, 0xd3, 0x65, 0xf5, 0xc2, 0xa3, 0x67,
0x6c, 0x12, 0x7f, 0xc2, 0x02, 0x97, 0x53, 0x21, 0x58, 0x70, 0xc1, 0x2d, 0x50, 0xf9, 0x69, 0xad, 0xb0, 0x41, 0xfc, 0x31, 0x0b, 0x5c, 0x4e, 0x85, 0x60, 0xc1, 0x39, 0xb7, 0x40, 0xc5, 0xa7, 0xb9,
0x89, 0xa6, 0x23, 0x0d, 0x4f, 0x62, 0x3b, 0xdc, 0x20, 0x59, 0x11, 0x7d, 0x0d, 0x1a, 0x2c, 0x10, 0xc2, 0x9a, 0xb6, 0x14, 0x3c, 0x8e, 0xe5, 0x70, 0x9d, 0x64, 0x49, 0xf4, 0x0d, 0xa8, 0xb3, 0x40,
0x51, 0xe8, 0x4e, 0x28, 0xe7, 0xf2, 0x1e, 0xac, 0xa9, 0xe3, 0x59, 0x57, 0xca, 0x81, 0xd6, 0x49, 0x44, 0xa1, 0x3b, 0xa6, 0x9c, 0xcb, 0x77, 0xb0, 0xaa, 0xd2, 0xb3, 0xa6, 0x98, 0x7d, 0xcd, 0x93,
0xa3, 0x70, 0x96, 0x35, 0xaa, 0x6b, 0x23, 0xa5, 0x4c, 0x8c, 0x5a, 0x50, 0xa5, 0x81, 0x17, 0xcd, 0x42, 0xe1, 0x34, 0x2b, 0x54, 0xd3, 0x42, 0x8a, 0x99, 0x08, 0x35, 0xa1, 0x42, 0x03, 0x2f, 0x9a,
0xa7, 0x82, 0xfa, 0x56, 0x43, 0x1e, 0x1a, 0xc5, 0x64, 0x16, 0x4a, 0xd9, 0xe8, 0x04, 0xb9, 0xe0, 0x4d, 0x04, 0xf5, 0xad, 0xba, 0x4c, 0x1a, 0xd5, 0xc9, 0xcc, 0x99, 0xb2, 0xd0, 0x09, 0x72, 0xce,
0xd6, 0xa6, 0xca, 0xaa, 0xfa, 0x46, 0x04, 0xb6, 0xf5, 0x31, 0xce, 0x42, 0x65, 0x4b, 0x65, 0xf6, 0xad, 0x0d, 0x15, 0x55, 0xf5, 0x8d, 0x08, 0x6c, 0xe9, 0x34, 0xce, 0x42, 0x65, 0x53, 0x45, 0xf6,
0x3b, 0x37, 0x64, 0x76, 0xa9, 0x39, 0xc4, 0xf9, 0x6d, 0x8a, 0x25, 0x35, 0xfa, 0x09, 0xdc, 0x59, 0x7b, 0xd7, 0x44, 0x76, 0xa1, 0x38, 0xc4, 0xf1, 0x6d, 0x88, 0x05, 0x36, 0xfa, 0x29, 0xdc, 0x9e,
0xf0, 0x47, 0x35, 0xca, 0xdd, 0x49, 0xcc, 0x25, 0xac, 0xa6, 0x5a, 0xaa, 0x75, 0x13, 0xe7, 0xc0, 0xf7, 0x8f, 0x6a, 0x95, 0xbb, 0xe3, 0xb8, 0x97, 0xb0, 0x1a, 0xea, 0xa8, 0xe6, 0x75, 0x3d, 0x07,
0xbb, 0x5e, 0x4e, 0xcf, 0x53, 0x2a, 0xf3, 0x0e, 0xdc, 0x22, 0x9e, 0x50, 0x25, 0xd4, 0xb8, 0x77, 0xde, 0xf1, 0x72, 0x7c, 0x9e, 0xb6, 0x32, 0xef, 0xc0, 0x4d, 0xe2, 0x09, 0x75, 0x85, 0x1a, 0xf7,
0x15, 0x61, 0xb3, 0xb6, 0x55, 0xfd, 0x90, 0x1e, 0x8b, 0x0f, 0x48, 0x57, 0xf5, 0xf0, 0x4d, 0x28, 0xae, 0x6a, 0xd8, 0xac, 0x2d, 0x75, 0x7f, 0x48, 0xaf, 0xc5, 0x09, 0xd2, 0x51, 0x35, 0x7c, 0x03,
0xf4, 0x0f, 0x2d, 0xa4, 0xdb, 0x60, 0xff, 0x70, 0xef, 0x14, 0xea, 0xd9, 0x03, 0x94, 0xed, 0xb7, 0x0a, 0xbd, 0x03, 0x0b, 0xe9, 0x32, 0xd8, 0x3b, 0x40, 0x9f, 0x40, 0x35, 0xae, 0x35, 0x07, 0xd2,
0x55, 0xdd, 0x6f, 0x1f, 0x65, 0xfb, 0x6d, 0x8e, 0x3b, 0x2e, 0xd1, 0xcf, 0x4c, 0x2b, 0xde, 0xfb, 0x22, 0x5f, 0x59, 0xf4, 0xe8, 0x1a, 0xe7, 0x8f, 0xe6, 0x3b, 0xb4, 0xdf, 0x59, 0x1d, 0xbb, 0x27,
0x11, 0xc0, 0x02, 0xdc, 0x6b, 0x9c, 0x7e, 0x2b, 0xef, 0x74, 0x77, 0x8d, 0x53, 0x39, 0x3f, 0xeb, 0x50, 0xcb, 0xe6, 0x64, 0xb6, 0x84, 0x57, 0x74, 0x09, 0x7f, 0x94, 0x2d, 0xe1, 0xb9, 0x76, 0x74,
0xf2, 0x73, 0xd8, 0x5a, 0x82, 0xf3, 0x1a, 0xbf, 0xef, 0xe6, 0xfd, 0xde, 0x5d, 0xe7, 0x57, 0x3b, 0xa1, 0xa3, 0xcd, 0x54, 0xf7, 0xdd, 0x4f, 0x00, 0xe6, 0xf9, 0xb2, 0x42, 0xe9, 0x77, 0xf2, 0x4a,
0x99, 0x67, 0x7d, 0x5f, 0xc0, 0xed, 0xb5, 0x05, 0x5d, 0xb3, 0xc2, 0xe3, 0xfc, 0x0a, 0xf6, 0xcd, 0x77, 0x56, 0x28, 0x95, 0xfb, 0xb3, 0x2a, 0x3f, 0x87, 0xcd, 0x85, 0x0c, 0x59, 0xa1, 0xf7, 0xdd,
0x17, 0x47, 0xf6, 0x8a, 0xfa, 0x59, 0x86, 0x95, 0xe6, 0x8e, 0x06, 0x3a, 0x84, 0xfb, 0x53, 0x16, 0xbc, 0xde, 0x3b, 0xab, 0xf4, 0x6a, 0x25, 0xb3, 0xac, 0xee, 0x73, 0xb8, 0xb5, 0x12, 0x23, 0x2b,
0x24, 0x20, 0x77, 0xc9, 0x78, 0x9c, 0xd6, 0x94, 0x06, 0xe4, 0x6c, 0x4c, 0xfd, 0x98, 0x29, 0xdd, 0x4e, 0x78, 0x9c, 0x3f, 0xc1, 0xbe, 0xfe, 0x2d, 0xca, 0x1e, 0xf4, 0x04, 0x1a, 0x8b, 0xf7, 0xb1,
0x9d, 0xb2, 0x20, 0x86, 0x7d, 0x67, 0x3c, 0x4e, 0x8b, 0xa7, 0x4c, 0xec, 0x7f, 0x14, 0xa0, 0x91, 0xe2, 0x8c, 0xdc, 0xab, 0x59, 0xcb, 0xbe, 0x9a, 0x3f, 0xcf, 0x34, 0xca, 0xb9, 0x6c, 0x45, 0x07,
0xcb, 0x20, 0xfa, 0x70, 0xd1, 0x4f, 0x35, 0xfd, 0xf8, 0xfa, 0x35, 0xb9, 0x7e, 0xb5, 0x46, 0x5a, 0x70, 0x7f, 0xc2, 0x82, 0x24, 0xef, 0x5c, 0x32, 0x1a, 0xa5, 0x30, 0xa3, 0x01, 0x39, 0x1d, 0x51,
0xf8, 0x6a, 0x8d, 0xd4, 0x7c, 0xc5, 0x46, 0x7a, 0x1f, 0x6a, 0x71, 0xab, 0x52, 0xaf, 0x2e, 0xcd, 0x3f, 0x6e, 0xde, 0xee, 0x4c, 0x58, 0x10, 0x67, 0x62, 0x7b, 0x34, 0x4a, 0x2f, 0x5f, 0x89, 0xd8,
0x4e, 0x92, 0xee, 0x25, 0x1f, 0x5d, 0x7b, 0x50, 0x99, 0x86, 0x9c, 0x29, 0x66, 0x2d, 0xbb, 0x73, 0xff, 0x2c, 0x40, 0x3d, 0x77, 0x03, 0xe8, 0xc9, 0xbc, 0xc4, 0xeb, 0x8e, 0xe8, 0x9b, 0x57, 0xdc,
0x09, 0xa7, 0xf2, 0xff, 0x08, 0xd3, 0xb6, 0x0f, 0xdb, 0x2b, 0x20, 0x5a, 0x0e, 0xd4, 0x58, 0x09, 0xd5, 0xeb, 0xd5, 0xf6, 0xc2, 0x57, 0xab, 0xed, 0xe6, 0x6b, 0xd6, 0xf6, 0xfb, 0x50, 0x8d, 0xab,
0x34, 0x21, 0x58, 0x85, 0x3c, 0xf3, 0x4e, 0x83, 0x37, 0xf3, 0xc1, 0xdb, 0xbf, 0x37, 0x60, 0x6b, 0xa7, 0x1a, 0x04, 0x75, 0xc3, 0x94, 0x14, 0x54, 0x39, 0x07, 0xee, 0x42, 0x79, 0x12, 0x72, 0xa6,
0xe9, 0x51, 0x26, 0x39, 0x71, 0x4c, 0x22, 0xe3, 0x05, 0x12, 0x11, 0xdd, 0x83, 0x2a, 0x67, 0x17, 0x9a, 0x7d, 0xf9, 0x60, 0x94, 0x70, 0x4a, 0xff, 0x9f, 0x72, 0xc2, 0xf6, 0x61, 0x6b, 0x09, 0x84,
0x01, 0x11, 0xb3, 0x88, 0xc6, 0x6f, 0xcf, 0x85, 0x42, 0x12, 0x36, 0xef, 0x92, 0x30, 0x4d, 0xd8, 0x8b, 0x86, 0x1a, 0x4b, 0x86, 0x26, 0x3d, 0x5f, 0x21, 0x3f, 0x0c, 0xa4, 0xc6, 0x9b, 0x79, 0xe3,
0x4c, 0x4d, 0xd8, 0x94, 0x42, 0x12, 0x8d, 0x87, 0xd0, 0x64, 0xbc, 0xc3, 0x22, 0x3f, 0x0a, 0xa7, 0xed, 0xdf, 0x1b, 0xb0, 0xb9, 0x30, 0x27, 0xca, 0x36, 0x3d, 0xee, 0x6b, 0xe3, 0x03, 0x12, 0x12,
0x31, 0xe9, 0x52, 0x79, 0xae, 0xe0, 0x15, 0xbd, 0xfd, 0x2f, 0x23, 0x83, 0x5b, 0x4c, 0x7f, 0x3e, 0xdd, 0x85, 0x0a, 0x67, 0xe7, 0x01, 0x11, 0xd3, 0x28, 0x41, 0xdb, 0x9c, 0x21, 0x7b, 0x48, 0xef,
0xa3, 0x5c, 0x8c, 0xc2, 0x4f, 0x42, 0x76, 0xdd, 0x2d, 0x1e, 0x13, 0xfc, 0xcc, 0xce, 0x25, 0xc1, 0x82, 0x30, 0xdd, 0x43, 0x9a, 0xba, 0x87, 0x54, 0x0c, 0xd9, 0xfb, 0x3c, 0x84, 0x06, 0xe3, 0x6d,
0x77, 0xe4, 0xe6, 0xaf, 0x7d, 0x11, 0x2f, 0x3f, 0xb5, 0x8b, 0xab, 0x4f, 0xed, 0x07, 0x50, 0xf7, 0x16, 0xf9, 0x51, 0x38, 0x89, 0xfb, 0x40, 0x15, 0xe7, 0x32, 0x5e, 0xe2, 0xdb, 0xff, 0x36, 0x32,
0x19, 0x9f, 0x8e, 0xc9, 0x5c, 0xbb, 0x2e, 0xc5, 0x6f, 0x2a, 0xad, 0x53, 0xee, 0x7f, 0xb0, 0xee, 0xb8, 0xc5, 0xf4, 0x17, 0x53, 0xca, 0xc5, 0x30, 0xfc, 0x38, 0x64, 0x57, 0x35, 0x16, 0xf1, 0xcc,
0xd9, 0x5b, 0xbe, 0xe1, 0xd9, 0xbb, 0xfa, 0xe4, 0xb5, 0xff, 0x68, 0xc0, 0xbd, 0x74, 0xcb, 0x3d, 0x91, 0xf1, 0x5c, 0xce, 0x1c, 0x8e, 0x74, 0xfe, 0xca, 0x21, 0x7d, 0x71, 0xfa, 0x2f, 0x2e, 0x4f,
0x9f, 0x89, 0x93, 0x4b, 0x12, 0x51, 0x7f, 0xc1, 0xc1, 0xd7, 0x6f, 0x7c, 0x79, 0x13, 0x85, 0xd5, 0xff, 0x0f, 0xa0, 0xe6, 0x33, 0x3e, 0x19, 0x91, 0x99, 0x56, 0x5d, 0x8a, 0xc7, 0x3c, 0xcd, 0x53,
0x4d, 0xac, 0x8d, 0xd0, 0xfc, 0xf2, 0x11, 0xfe, 0x39, 0x1b, 0x61, 0x97, 0x04, 0x1e, 0x1d, 0xff, 0xea, 0x7f, 0xb4, 0x6a, 0x12, 0x5f, 0xbb, 0x66, 0x12, 0x5f, 0x9e, 0xc2, 0xed, 0x3f, 0x19, 0x70,
0x5f, 0x97, 0xc6, 0xfe, 0xa2, 0x00, 0x6f, 0xae, 0x47, 0x11, 0xa6, 0x7c, 0x1a, 0x06, 0x9c, 0x5e, 0x37, 0x75, 0xb9, 0xeb, 0x33, 0x71, 0x7c, 0x41, 0x22, 0xea, 0xcf, 0xc7, 0x82, 0xd5, 0x8e, 0x2f,
0x13, 0xf2, 0xf7, 0xa1, 0x9a, 0x2e, 0xf5, 0x92, 0x0e, 0x94, 0xb9, 0x9f, 0xf1, 0x62, 0x82, 0x3c, 0x3a, 0x51, 0x58, 0x76, 0x62, 0xa5, 0x85, 0xe6, 0x97, 0xb7, 0xf0, 0x2f, 0x59, 0x0b, 0x3b, 0x24,
0x6d, 0xf2, 0x09, 0xa8, 0xa8, 0x81, 0xa9, 0x00, 0x9e, 0xca, 0x72, 0xbd, 0x8b, 0x88, 0x04, 0x22, 0xf0, 0xe8, 0xe8, 0x6b, 0x7d, 0x35, 0xf6, 0x17, 0x05, 0x78, 0x73, 0x35, 0x8a, 0x30, 0xe5, 0x93,
0xde, 0x91, 0x16, 0x56, 0xb6, 0x5b, 0x5a, 0xdd, 0xee, 0x1b, 0x00, 0x9a, 0x35, 0xb9, 0xb3, 0x88, 0x30, 0xe0, 0xf4, 0x0a, 0x93, 0x7f, 0x08, 0x95, 0xf4, 0xa8, 0x57, 0x54, 0xa0, 0xcc, 0xab, 0x89,
0xc5, 0xcf, 0xea, 0xaa, 0xd6, 0x9c, 0x46, 0x0c, 0x7d, 0x00, 0x77, 0x65, 0x7c, 0xd4, 0x13, 0xd4, 0xe7, 0x1b, 0x64, 0xb6, 0xc9, 0xa9, 0x54, 0x75, 0x2b, 0xa6, 0x02, 0x78, 0x4a, 0xcb, 0xf3, 0xce,
0x77, 0x45, 0x38, 0x65, 0x5e, 0x42, 0xe9, 0x5d, 0xd9, 0x8a, 0x36, 0x94, 0x43, 0x2b, 0x35, 0x19, 0x23, 0x12, 0x88, 0xd8, 0x23, 0x4d, 0x2c, 0xb9, 0x5b, 0x5a, 0x76, 0xf7, 0x1e, 0x80, 0x6e, 0xe4,
0x49, 0x8b, 0x98, 0xe2, 0x3f, 0xa3, 0x73, 0xf4, 0x16, 0x94, 0xd4, 0xbf, 0x51, 0xea, 0xa1, 0x54, 0xdc, 0x69, 0xc4, 0xe2, 0x49, 0xbf, 0xa2, 0x39, 0x27, 0x11, 0x43, 0x1f, 0xc0, 0x1d, 0x69, 0x1f,
0x3b, 0xd8, 0x5a, 0x6c, 0x56, 0xa2, 0xd0, 0xc7, 0x7a, 0xd4, 0xc6, 0xb0, 0xbb, 0x9a, 0xcf, 0x23, 0xf5, 0x04, 0xf5, 0x5d, 0x11, 0x4e, 0x98, 0x97, 0x4c, 0x19, 0xae, 0x2c, 0x45, 0xeb, 0x4a, 0xa1,
0x4a, 0xae, 0xe8, 0x7f, 0x8c, 0x4e, 0xfb, 0xc7, 0xf0, 0x20, 0xd3, 0x03, 0xf5, 0x35, 0xb3, 0x4c, 0x95, 0x8a, 0x0c, 0xa5, 0x44, 0xfc, 0xb0, 0x3c, 0xa7, 0x33, 0xf4, 0x16, 0x94, 0xd4, 0x1f, 0x64,
0x03, 0xaf, 0xf1, 0x9e, 0xcf, 0x49, 0x61, 0x29, 0x27, 0xf6, 0x5f, 0x0d, 0xa8, 0x3d, 0x27, 0x2f, 0x6a, 0x76, 0xab, 0xee, 0x6f, 0xce, 0x9d, 0x95, 0x28, 0xf4, 0xb1, 0x5e, 0xb5, 0x31, 0xec, 0x2c,
0x66, 0x09, 0x67, 0x6b, 0x82, 0xc9, 0xd9, 0x45, 0xfc, 0x8f, 0x9a, 0xfc, 0x94, 0xdd, 0x4c, 0xb0, 0xc7, 0xf3, 0x90, 0x92, 0x4b, 0xfa, 0x5f, 0xa3, 0xd3, 0xfe, 0x09, 0x3c, 0xc8, 0xd4, 0x40, 0xfd,
0x09, 0xe5, 0x82, 0x4c, 0xa6, 0x6a, 0x7e, 0x11, 0x2f, 0x14, 0x72, 0x51, 0x95, 0x49, 0x55, 0xc4, 0xcc, 0x2c, 0x76, 0xa6, 0x57, 0x68, 0xcf, 0xc7, 0xa4, 0xb0, 0x10, 0x13, 0xfb, 0x6f, 0x06, 0x54,
0x3a, 0xd6, 0x82, 0xfa, 0xbf, 0x80, 0xcc, 0xc7, 0x21, 0x49, 0x50, 0x99, 0x88, 0x7a, 0xc4, 0xf7, 0x5f, 0x90, 0x97, 0xd3, 0xa4, 0x8d, 0x6c, 0x80, 0xc9, 0xd9, 0x79, 0xfc, 0x27, 0x9f, 0xfc, 0x94,
0x59, 0x70, 0x11, 0x17, 0x30, 0x11, 0x65, 0x4f, 0xbe, 0x24, 0xfc, 0x52, 0x95, 0xad, 0x8e, 0xd5, 0xd5, 0x4c, 0xb0, 0x31, 0xe5, 0x82, 0x8c, 0x27, 0x6a, 0x7f, 0x11, 0xcf, 0x19, 0xf2, 0x50, 0x15,
0x37, 0xb2, 0xa1, 0x2e, 0x2e, 0x59, 0xe4, 0x1f, 0x93, 0x48, 0xe6, 0x21, 0x7e, 0xc0, 0xe6, 0x74, 0x49, 0x75, 0x89, 0x35, 0xac, 0x09, 0xf5, 0x17, 0x06, 0x99, 0x8d, 0x42, 0x92, 0xa0, 0x32, 0x21,
0xf6, 0xaf, 0x60, 0x2f, 0xb3, 0x81, 0x24, 0x2d, 0x09, 0x19, 0xb3, 0x60, 0xe3, 0x8a, 0x46, 0xf2, 0xf5, 0x8a, 0xef, 0xb3, 0xe0, 0x3c, 0xbe, 0xc0, 0x84, 0x94, 0x35, 0xf9, 0x82, 0xf0, 0x0b, 0x75,
0xce, 0x53, 0x7b, 0x6a, 0xe0, 0x44, 0x94, 0xeb, 0x9d, 0x47, 0xe1, 0x24, 0xde, 0x92, 0xfa, 0x96, 0x6d, 0x35, 0xac, 0xbe, 0x91, 0x0d, 0x35, 0x71, 0xc1, 0x22, 0xff, 0x88, 0x44, 0x32, 0x0e, 0xf1,
0x44, 0x4c, 0x84, 0xf1, 0x7f, 0x68, 0x05, 0x11, 0xca, 0xf5, 0xe5, 0x3b, 0x9f, 0x06, 0x42, 0x81, 0x4c, 0x9d, 0xe3, 0xd9, 0xbf, 0x86, 0xdd, 0x8c, 0x03, 0x49, 0x58, 0x92, 0xfe, 0xd0, 0x82, 0xf5,
0x41, 0x3d, 0x0b, 0xeb, 0x38, 0xa7, 0xb3, 0xff, 0x64, 0x00, 0x5a, 0x0d, 0xe0, 0x25, 0x0b, 0x7f, 0x4b, 0x1a, 0xc9, 0x37, 0x4f, 0xf9, 0x54, 0xc7, 0x09, 0x29, 0xcf, 0x3b, 0x8b, 0xc2, 0x71, 0xec,
0x0c, 0x95, 0x94, 0x6c, 0xea, 0x73, 0x93, 0xb9, 0xfd, 0xaf, 0xdf, 0x0a, 0x4e, 0x67, 0xa1, 0x77, 0x92, 0xfa, 0x96, 0xbd, 0xa1, 0x08, 0xe3, 0xbf, 0xf5, 0x0a, 0x22, 0x94, 0xe7, 0x7b, 0x61, 0x20,
0xa5, 0x07, 0x65, 0x93, 0xf4, 0xa8, 0xdb, 0x6b, 0x3d, 0xe0, 0xd4, 0xcc, 0xfe, 0x9b, 0x01, 0xf7, 0x68, 0x20, 0x14, 0x18, 0xd4, 0xa4, 0x5a, 0xc3, 0x39, 0x9e, 0xfd, 0x67, 0x03, 0xd0, 0xb2, 0x01,
0x57, 0x7d, 0xf7, 0x03, 0x9f, 0xfe, 0xe2, 0x15, 0x72, 0xf5, 0xd5, 0x43, 0xde, 0x81, 0x72, 0x78, 0xaf, 0x38, 0xf8, 0x23, 0x28, 0xa7, 0xfd, 0xaf, 0xce, 0x9b, 0xcc, 0xeb, 0x7f, 0xb5, 0x2b, 0x38,
0x7e, 0xce, 0xa9, 0x88, 0xb3, 0x1b, 0x4b, 0xb2, 0x0a, 0x9c, 0xfd, 0x92, 0xc6, 0x7f, 0xb8, 0xaa, 0xdd, 0x85, 0xde, 0x95, 0x1a, 0x94, 0x4c, 0x52, 0xa3, 0x6e, 0xad, 0xd4, 0x80, 0x53, 0x31, 0xfb,
0xef, 0x65, 0x8c, 0x14, 0x53, 0x8c, 0xd8, 0x5f, 0x18, 0xb0, 0x7b, 0xcd, 0x2e, 0xd0, 0x33, 0xa8, 0xef, 0x06, 0xdc, 0x5f, 0xd6, 0xdd, 0x0b, 0x7c, 0xfa, 0xcb, 0xd7, 0x88, 0xd5, 0x57, 0x37, 0x79,
0xc4, 0x4f, 0xa3, 0x84, 0x54, 0x3d, 0x7a, 0x59, 0x8c, 0x6a, 0x52, 0x3b, 0x16, 0x62, 0x7e, 0x95, 0x1b, 0xd6, 0xc2, 0xb3, 0x33, 0x4e, 0x45, 0x1c, 0xdd, 0x98, 0x92, 0xb7, 0xc0, 0xd9, 0xaf, 0x68,
0x3a, 0xd8, 0x3b, 0x87, 0x46, 0x6e, 0x68, 0x0d, 0x5d, 0xf9, 0x28, 0x4f, 0x57, 0xde, 0xbe, 0x71, 0xfc, 0x1f, 0xb0, 0xfa, 0x5e, 0xc4, 0x48, 0x31, 0xc5, 0x88, 0xfd, 0x85, 0x01, 0x3b, 0x57, 0x78,
0xb1, 0x34, 0x2b, 0x0b, 0xfa, 0xf2, 0xa4, 0xf1, 0x79, 0xad, 0xfd, 0xe8, 0xfd, 0x64, 0xe6, 0x59, 0x81, 0x9e, 0x43, 0x39, 0x9e, 0xd6, 0x92, 0xa6, 0xea, 0xd1, 0xab, 0x6c, 0x54, 0x9b, 0x5a, 0x31,
0x59, 0x7d, 0x7d, 0xfb, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0xdc, 0xcb, 0x13, 0x4d, 0x36, 0x17, 0x11, 0xf7, 0x57, 0xa9, 0x82, 0xdd, 0x33, 0xa8, 0xe7, 0x96, 0x56, 0xb4, 0x2b, 0x1f, 0xe6, 0xdb,
0x00, 0x00, 0x95, 0xb7, 0xaf, 0x3d, 0x2c, 0x8d, 0xca, 0xbc, 0x7d, 0x79, 0x5a, 0xff, 0xbc, 0xda, 0x7a, 0xf4,
0x7e, 0xb2, 0xf3, 0x74, 0x4d, 0x7d, 0x7d, 0xf7, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x53, 0x4d,
0x30, 0x03, 0xc9, 0x17, 0x00, 0x00,
} }

View File

@ -100,6 +100,9 @@ message CommunityDescription {
repeated CommunityTokenMetadata community_tokens_metadata = 16; repeated CommunityTokenMetadata community_tokens_metadata = 16;
uint64 active_members_count = 17; uint64 active_members_count = 17;
string ID = 18; string ID = 18;
// key is hash ratchet key_id + seq_no
map<string, bytes> privateData = 100;
} }
message CommunityAdminSettings { message CommunityAdminSettings {