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:
parent
9cbfda69da
commit
1d3c618fb4
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue