status-go/protocol/messenger_profile_showcase.go

594 lines
21 KiB
Go
Raw Normal View History

package protocol
import (
"bytes"
"context"
"crypto/ecdsa"
crand "crypto/rand"
"errors"
"reflect"
"sort"
"github.com/golang/protobuf/proto"
"go.uber.org/zap"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/identity"
"github.com/status-im/status-go/protocol/protobuf"
)
var errorDecryptingPayloadEncryptionKey = errors.New("decrypting the payload encryption key resulted in no error and a nil key")
func (m *Messenger) toProfileShowcaseCommunityProto(preferences []*identity.ProfileShowcaseCommunityPreference, visibility identity.ProfileShowcaseVisibility) []*protobuf.ProfileShowcaseCommunity {
entries := []*protobuf.ProfileShowcaseCommunity{}
for _, preference := range preferences {
if preference.ShowcaseVisibility != visibility {
continue
}
entry := &protobuf.ProfileShowcaseCommunity{
CommunityId: preference.CommunityID,
Order: uint32(preference.Order),
}
community, err := m.communitiesManager.GetByIDString(preference.CommunityID)
if err != nil {
m.logger.Warn("failed to get community for profile entry ", zap.Error(err))
}
if community != nil && community.Encrypted() {
grant, _, err := m.communitiesManager.GetCommunityGrant(preference.CommunityID)
if err != nil {
m.logger.Warn("failed to get community for profile entry ", zap.Error(err))
}
entry.Grant = grant
}
entries = append(entries, entry)
}
return entries
}
func (m *Messenger) toProfileShowcaseAccountProto(preferences []*identity.ProfileShowcaseAccountPreference, visibility identity.ProfileShowcaseVisibility) []*protobuf.ProfileShowcaseAccount {
entries := []*protobuf.ProfileShowcaseAccount{}
for _, preference := range preferences {
if preference.ShowcaseVisibility != visibility {
continue
}
entries = append(entries, &protobuf.ProfileShowcaseAccount{
Address: preference.Address,
Name: preference.Name,
ColorId: preference.ColorID,
Emoji: preference.Emoji,
Order: uint32(preference.Order),
})
}
return entries
}
func (m *Messenger) toProfileShowcaseCollectibleProto(preferences []*identity.ProfileShowcaseCollectiblePreference, visibility identity.ProfileShowcaseVisibility) []*protobuf.ProfileShowcaseCollectible {
entries := []*protobuf.ProfileShowcaseCollectible{}
for _, preference := range preferences {
if preference.ShowcaseVisibility != visibility {
continue
}
entries = append(entries, &protobuf.ProfileShowcaseCollectible{
ContractAddress: preference.ContractAddress,
ChainId: preference.ChainID,
TokenId: preference.TokenID,
CommunityId: preference.CommunityID,
AccountAddress: preference.AccountAddress,
Order: uint32(preference.Order),
})
}
return entries
}
func (m *Messenger) toProfileShowcaseVerifiedTokensProto(preferences []*identity.ProfileShowcaseVerifiedTokenPreference, visibility identity.ProfileShowcaseVisibility) []*protobuf.ProfileShowcaseVerifiedToken {
entries := []*protobuf.ProfileShowcaseVerifiedToken{}
for _, preference := range preferences {
if preference.ShowcaseVisibility != visibility {
continue
}
entries = append(entries, &protobuf.ProfileShowcaseVerifiedToken{
Symbol: preference.Symbol,
Order: uint32(preference.Order),
})
}
return entries
}
func (m *Messenger) toProfileShowcaseUnverifiedTokensProto(preferences []*identity.ProfileShowcaseUnverifiedTokenPreference, visibility identity.ProfileShowcaseVisibility) []*protobuf.ProfileShowcaseUnverifiedToken {
entries := []*protobuf.ProfileShowcaseUnverifiedToken{}
for _, preference := range preferences {
if preference.ShowcaseVisibility != visibility {
continue
}
entries = append(entries, &protobuf.ProfileShowcaseUnverifiedToken{
ContractAddress: preference.ContractAddress,
ChainId: preference.ChainID,
Order: uint32(preference.Order),
})
}
return entries
}
func (m *Messenger) fromProfileShowcaseCommunityProto(senderPubKey *ecdsa.PublicKey, messages []*protobuf.ProfileShowcaseCommunity) []*identity.ProfileShowcaseCommunity {
entries := []*identity.ProfileShowcaseCommunity{}
for _, message := range messages {
entry := &identity.ProfileShowcaseCommunity{
CommunityID: message.CommunityId,
Order: int(message.Order),
MembershipStatus: identity.ProfileShowcaseMembershipStatusUnproven,
}
community, err := m.FetchCommunity(&FetchCommunityRequest{
CommunityKey: message.CommunityId,
Shard: nil,
TryDatabase: true,
WaitForResponse: true,
})
if err != nil {
m.logger.Warn("failed to fetch community for profile entry ", zap.Error(err))
}
if community != nil && community.Encrypted() {
grant, err := community.VerifyGrantSignature(message.Grant)
if err != nil {
m.logger.Warn("failed to verify grant signature ", zap.Error(err))
entry.MembershipStatus = identity.ProfileShowcaseMembershipStatusNotAMember
} else {
if grant != nil && bytes.Equal(grant.MemberId, crypto.CompressPubkey(senderPubKey)) {
entry.MembershipStatus = identity.ProfileShowcaseMembershipStatusProvenMember
} else { // Show as not a member if membership can't be proven
entry.MembershipStatus = identity.ProfileShowcaseMembershipStatusNotAMember
}
}
} else if community != nil {
// Use member list as a proof for unecrypted communities
if community.HasMember(senderPubKey) {
entry.MembershipStatus = identity.ProfileShowcaseMembershipStatusProvenMember
} else {
entry.MembershipStatus = identity.ProfileShowcaseMembershipStatusNotAMember
}
}
entries = append(entries, entry)
}
return entries
}
func (m *Messenger) fromProfileShowcaseAccountProto(messages []*protobuf.ProfileShowcaseAccount) []*identity.ProfileShowcaseAccount {
entries := []*identity.ProfileShowcaseAccount{}
for _, entry := range messages {
entries = append(entries, &identity.ProfileShowcaseAccount{
Address: entry.Address,
Name: entry.Name,
ColorID: entry.ColorId,
Emoji: entry.Emoji,
Order: int(entry.Order),
})
}
return entries
}
func (m *Messenger) fromProfileShowcaseCollectibleProto(messages []*protobuf.ProfileShowcaseCollectible) []*identity.ProfileShowcaseCollectible {
entries := []*identity.ProfileShowcaseCollectible{}
for _, entry := range messages {
entries = append(entries, &identity.ProfileShowcaseCollectible{
ContractAddress: entry.ContractAddress,
ChainID: entry.ChainId,
TokenID: entry.TokenId,
CommunityID: entry.CommunityId,
AccountAddress: entry.AccountAddress,
Order: int(entry.Order),
})
}
return entries
}
func (m *Messenger) fromProfileShowcaseVerifiedTokenProto(messages []*protobuf.ProfileShowcaseVerifiedToken) []*identity.ProfileShowcaseVerifiedToken {
entries := []*identity.ProfileShowcaseVerifiedToken{}
for _, entry := range messages {
entries = append(entries, &identity.ProfileShowcaseVerifiedToken{
Symbol: entry.Symbol,
Order: int(entry.Order),
})
}
return entries
}
func (m *Messenger) fromProfileShowcaseUnverifiedTokenProto(messages []*protobuf.ProfileShowcaseUnverifiedToken) []*identity.ProfileShowcaseUnverifiedToken {
entries := []*identity.ProfileShowcaseUnverifiedToken{}
for _, entry := range messages {
entries = append(entries, &identity.ProfileShowcaseUnverifiedToken{
ContractAddress: entry.ContractAddress,
ChainID: entry.ChainId,
Order: int(entry.Order),
})
}
return entries
}
func (m *Messenger) SetProfileShowcasePreferences(preferences *identity.ProfileShowcasePreferences, sync bool) error {
clock, _ := m.getLastClockWithRelatedChat()
preferences.Clock = clock
return m.setProfileShowcasePreferences(preferences, sync)
}
func (m *Messenger) setProfileShowcasePreferences(preferences *identity.ProfileShowcasePreferences, sync bool) error {
err := identity.Validate(preferences)
if err != nil {
return err
}
err = m.persistence.SaveProfileShowcasePreferences(preferences)
if err != nil {
return err
}
if sync {
err = m.syncProfileShowcasePreferences(context.Background(), m.dispatchMessage)
if err != nil {
return err
}
}
return m.DispatchProfileShowcase()
}
func (m *Messenger) DispatchProfileShowcase() error {
err := m.publishContactCode()
if err != nil {
return err
}
return nil
}
func (m *Messenger) GetProfileShowcasePreferences() (*identity.ProfileShowcasePreferences, error) {
return m.persistence.GetProfileShowcasePreferences()
}
func (m *Messenger) GetProfileShowcaseForContact(contactID string) (*identity.ProfileShowcase, error) {
return m.persistence.GetProfileShowcaseForContact(contactID)
}
func (m *Messenger) GetProfileShowcaseAccountsByAddress(address string) ([]*identity.ProfileShowcaseAccount, error) {
return m.persistence.GetProfileShowcaseAccountsByAddress(address)
}
func (m *Messenger) EncryptProfileShowcaseEntriesWithContactPubKeys(entries *protobuf.ProfileShowcaseEntries, contacts []*Contact) (*protobuf.ProfileShowcaseEntriesEncrypted, error) {
// Make AES key
AESKey := make([]byte, 32)
_, err := crand.Read(AESKey)
if err != nil {
return nil, err
}
// Encrypt showcase entries with the AES key
data, err := proto.Marshal(entries)
if err != nil {
return nil, err
}
encrypted, err := common.Encrypt(data, AESKey, crand.Reader)
if err != nil {
return nil, err
}
eAESKeys := [][]byte{}
// Sign for each contact
for _, contact := range contacts {
var pubK *ecdsa.PublicKey
var sharedKey []byte
var eAESKey []byte
pubK, err = contact.PublicKey()
if err != nil {
return nil, err
}
// Generate a Diffie-Helman (DH) between the sender private key and the recipient's public key
sharedKey, err = common.MakeECDHSharedKey(m.identity, pubK)
if err != nil {
return nil, err
}
// Encrypt the main AES key with AES encryption using the DH key
eAESKey, err = common.Encrypt(AESKey, sharedKey, crand.Reader)
if err != nil {
return nil, err
}
eAESKeys = append(eAESKeys, eAESKey)
}
return &protobuf.ProfileShowcaseEntriesEncrypted{
EncryptedEntries: encrypted,
EncryptionKeys: eAESKeys,
}, nil
}
func (m *Messenger) DecryptProfileShowcaseEntriesWithPubKey(senderPubKey *ecdsa.PublicKey, encrypted *protobuf.ProfileShowcaseEntriesEncrypted) (*protobuf.ProfileShowcaseEntries, error) {
for _, eAESKey := range encrypted.EncryptionKeys {
// Generate a Diffie-Helman (DH) between the recipient's private key and the sender's public key
sharedKey, err := common.MakeECDHSharedKey(m.identity, senderPubKey)
if err != nil {
return nil, err
}
// Decrypt the main encryption AES key with AES encryption using the DH key
dAESKey, err := common.Decrypt(eAESKey, sharedKey)
if err != nil {
if err.Error() == ErrCipherMessageAutentificationFailed {
continue
}
return nil, err
}
if dAESKey == nil {
return nil, errorDecryptingPayloadEncryptionKey
}
// Decrypt profile entries with the newly decrypted main encryption AES key
entriesData, err := common.Decrypt(encrypted.EncryptedEntries, dAESKey)
if err != nil {
return nil, err
}
entries := &protobuf.ProfileShowcaseEntries{}
err = proto.Unmarshal(entriesData, entries)
if err != nil {
return nil, err
}
return entries, nil
}
// Return empty if no matching key found
return &protobuf.ProfileShowcaseEntries{}, nil
}
func (m *Messenger) GetProfileShowcaseForSelfIdentity() (*protobuf.ProfileShowcase, error) {
preferences, err := m.GetProfileShowcasePreferences()
if err != nil {
return nil, err
}
forEveryone := &protobuf.ProfileShowcaseEntries{
Communities: m.toProfileShowcaseCommunityProto(preferences.Communities, identity.ProfileShowcaseVisibilityEveryone),
Accounts: m.toProfileShowcaseAccountProto(preferences.Accounts, identity.ProfileShowcaseVisibilityEveryone),
Collectibles: m.toProfileShowcaseCollectibleProto(preferences.Collectibles, identity.ProfileShowcaseVisibilityEveryone),
VerifiedTokens: m.toProfileShowcaseVerifiedTokensProto(preferences.VerifiedTokens, identity.ProfileShowcaseVisibilityEveryone),
UnverifiedTokens: m.toProfileShowcaseUnverifiedTokensProto(preferences.UnverifiedTokens, identity.ProfileShowcaseVisibilityEveryone),
}
forContacts := &protobuf.ProfileShowcaseEntries{
Communities: m.toProfileShowcaseCommunityProto(preferences.Communities, identity.ProfileShowcaseVisibilityContacts),
Accounts: m.toProfileShowcaseAccountProto(preferences.Accounts, identity.ProfileShowcaseVisibilityContacts),
Collectibles: m.toProfileShowcaseCollectibleProto(preferences.Collectibles, identity.ProfileShowcaseVisibilityContacts),
VerifiedTokens: m.toProfileShowcaseVerifiedTokensProto(preferences.VerifiedTokens, identity.ProfileShowcaseVisibilityContacts),
UnverifiedTokens: m.toProfileShowcaseUnverifiedTokensProto(preferences.UnverifiedTokens, identity.ProfileShowcaseVisibilityContacts),
}
forIDVerifiedContacts := &protobuf.ProfileShowcaseEntries{
Communities: m.toProfileShowcaseCommunityProto(preferences.Communities, identity.ProfileShowcaseVisibilityIDVerifiedContacts),
Accounts: m.toProfileShowcaseAccountProto(preferences.Accounts, identity.ProfileShowcaseVisibilityIDVerifiedContacts),
Collectibles: m.toProfileShowcaseCollectibleProto(preferences.Collectibles, identity.ProfileShowcaseVisibilityIDVerifiedContacts),
VerifiedTokens: m.toProfileShowcaseVerifiedTokensProto(preferences.VerifiedTokens, identity.ProfileShowcaseVisibilityIDVerifiedContacts),
UnverifiedTokens: m.toProfileShowcaseUnverifiedTokensProto(preferences.UnverifiedTokens, identity.ProfileShowcaseVisibilityIDVerifiedContacts),
}
mutualContacts := []*Contact{}
iDVerifiedContacts := []*Contact{}
m.allContacts.Range(func(_ string, contact *Contact) (shouldContinue bool) {
if contact.mutual() {
mutualContacts = append(mutualContacts, contact)
if contact.IsVerified() {
iDVerifiedContacts = append(iDVerifiedContacts, contact)
}
}
return true
})
forContactsEncrypted, err := m.EncryptProfileShowcaseEntriesWithContactPubKeys(forContacts, mutualContacts)
if err != nil {
return nil, err
}
forIDVerifiedContactsEncrypted, err := m.EncryptProfileShowcaseEntriesWithContactPubKeys(forIDVerifiedContacts, iDVerifiedContacts)
if err != nil {
return nil, err
}
return &protobuf.ProfileShowcase{
ForEveryone: forEveryone,
ForContacts: forContactsEncrypted,
ForIdVerifiedContacts: forIDVerifiedContactsEncrypted,
}, nil
}
func (m *Messenger) buildProfileShowcaseFromEntries(
contactID string,
communities []*identity.ProfileShowcaseCommunity,
accounts []*identity.ProfileShowcaseAccount,
collectibles []*identity.ProfileShowcaseCollectible,
verifiedTokens []*identity.ProfileShowcaseVerifiedToken,
unverifiedTokens []*identity.ProfileShowcaseUnverifiedToken) *identity.ProfileShowcase {
sort.Slice(communities, func(i, j int) bool {
return communities[j].Order > communities[i].Order
})
sort.Slice(accounts, func(i, j int) bool {
return accounts[j].Order > accounts[i].Order
})
sort.Slice(collectibles, func(i, j int) bool {
return collectibles[j].Order > collectibles[i].Order
})
sort.Slice(verifiedTokens, func(i, j int) bool {
return verifiedTokens[j].Order > verifiedTokens[i].Order
})
sort.Slice(unverifiedTokens, func(i, j int) bool {
return unverifiedTokens[j].Order > unverifiedTokens[i].Order
})
return &identity.ProfileShowcase{
ContactID: contactID,
Communities: communities,
Accounts: accounts,
Collectibles: collectibles,
VerifiedTokens: verifiedTokens,
UnverifiedTokens: unverifiedTokens,
}
}
func (m *Messenger) BuildProfileShowcaseFromIdentity(state *ReceivedMessageState, message *protobuf.ProfileShowcase) error {
senderPubKey := state.CurrentMessageState.PublicKey
contactID := state.CurrentMessageState.Contact.ID
communities := []*identity.ProfileShowcaseCommunity{}
accounts := []*identity.ProfileShowcaseAccount{}
collectibles := []*identity.ProfileShowcaseCollectible{}
verifiedTokens := []*identity.ProfileShowcaseVerifiedToken{}
unverifiedTokens := []*identity.ProfileShowcaseUnverifiedToken{}
communities = append(communities, m.fromProfileShowcaseCommunityProto(senderPubKey, message.ForEveryone.Communities)...)
accounts = append(accounts, m.fromProfileShowcaseAccountProto(message.ForEveryone.Accounts)...)
collectibles = append(collectibles, m.fromProfileShowcaseCollectibleProto(message.ForEveryone.Collectibles)...)
verifiedTokens = append(verifiedTokens, m.fromProfileShowcaseVerifiedTokenProto(message.ForEveryone.VerifiedTokens)...)
unverifiedTokens = append(unverifiedTokens, m.fromProfileShowcaseUnverifiedTokenProto(message.ForEveryone.UnverifiedTokens)...)
forContacts, err := m.DecryptProfileShowcaseEntriesWithPubKey(senderPubKey, message.ForContacts)
if err != nil {
return err
}
if forContacts != nil {
communities = append(communities, m.fromProfileShowcaseCommunityProto(senderPubKey, forContacts.Communities)...)
accounts = append(accounts, m.fromProfileShowcaseAccountProto(forContacts.Accounts)...)
collectibles = append(collectibles, m.fromProfileShowcaseCollectibleProto(forContacts.Collectibles)...)
verifiedTokens = append(verifiedTokens, m.fromProfileShowcaseVerifiedTokenProto(forContacts.VerifiedTokens)...)
unverifiedTokens = append(unverifiedTokens, m.fromProfileShowcaseUnverifiedTokenProto(forContacts.UnverifiedTokens)...)
}
forIDVerifiedContacts, err := m.DecryptProfileShowcaseEntriesWithPubKey(senderPubKey, message.ForIdVerifiedContacts)
if err != nil {
return err
}
if forIDVerifiedContacts != nil {
communities = append(communities, m.fromProfileShowcaseCommunityProto(senderPubKey, forIDVerifiedContacts.Communities)...)
accounts = append(accounts, m.fromProfileShowcaseAccountProto(forIDVerifiedContacts.Accounts)...)
collectibles = append(collectibles, m.fromProfileShowcaseCollectibleProto(forIDVerifiedContacts.Collectibles)...)
verifiedTokens = append(verifiedTokens, m.fromProfileShowcaseVerifiedTokenProto(forIDVerifiedContacts.VerifiedTokens)...)
unverifiedTokens = append(unverifiedTokens, m.fromProfileShowcaseUnverifiedTokenProto(forIDVerifiedContacts.UnverifiedTokens)...)
}
newShowcase := m.buildProfileShowcaseFromEntries(
contactID, communities, accounts, collectibles, verifiedTokens, unverifiedTokens)
oldShowcase, err := m.persistence.GetProfileShowcaseForContact(contactID)
if err != nil {
return err
}
if reflect.DeepEqual(newShowcase, oldShowcase) {
return nil
}
err = m.persistence.ClearProfileShowcaseForContact(contactID)
if err != nil {
return err
}
err = m.persistence.SaveProfileShowcaseForContact(newShowcase)
if err != nil {
return err
}
state.Response.AddProfileShowcase(newShowcase)
return nil
}
func (m *Messenger) UpdateProfileShowcaseWalletAccount(account *accounts.Account) error {
profileAccount, err := m.persistence.GetProfileShowcaseAccountPreference(account.Address.Hex())
if err != nil {
return err
}
if profileAccount == nil {
// No corresponding profile entry, exit
return nil
}
profileAccount.Name = account.Name
profileAccount.ColorID = string(account.ColorID)
profileAccount.Emoji = account.Emoji
err = m.persistence.SaveProfileShowcaseAccountPreference(profileAccount)
if err != nil {
return err
}
return m.DispatchProfileShowcase()
}
func (m *Messenger) DeleteProfileShowcaseWalletAccount(account *accounts.Account) error {
deleted, err := m.persistence.DeleteProfileShowcaseAccountPreference(account.Address.Hex())
if err != nil {
return err
}
if deleted {
return m.DispatchProfileShowcase()
}
return nil
}
func (m *Messenger) DeleteProfileShowcaseCommunity(community *communities.Community) error {
deleted, err := m.persistence.DeleteProfileShowcaseCommunityPreference(community.IDString())
if err != nil {
return err
}
if deleted {
return m.DispatchProfileShowcase()
}
return nil
}
func (m *Messenger) saveProfileShowcasePreferencesProto(p *protobuf.SyncProfileShowcasePreferences, shouldSync bool) error {
preferences := FromProfileShowcasePreferencesProto(p)
return m.setProfileShowcasePreferences(preferences, shouldSync)
}
func (m *Messenger) syncProfileShowcasePreferences(ctx context.Context, rawMessageHandler RawMessageHandler) error {
preferences, err := m.GetProfileShowcasePreferences()
if err != nil {
return err
}
syncMessage := ToProfileShowcasePreferencesProto(preferences)
encodedMessage, err := proto.Marshal(syncMessage)
if err != nil {
return err
}
_, chat := m.getLastClockWithRelatedChat()
rawMessage := common.RawMessage{
LocalChatID: chat.ID,
Payload: encodedMessage,
MessageType: protobuf.ApplicationMetadataMessage_SYNC_PROFILE_SHOWCASE_PREFERENCES,
ResendAutomatically: true,
}
_, err = rawMessageHandler(ctx, rawMessage)
return err
}