package protocol import ( "crypto/ecdsa" "encoding/json" "fmt" accountJson "github.com/status-im/status-go/account/json" "github.com/status-im/status-go/api/multiformat" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/images" "github.com/status-im/status-go/multiaccounts" "github.com/status-im/status-go/multiaccounts/accounts" multiaccountscommon "github.com/status-im/status-go/multiaccounts/common" "github.com/status-im/status-go/multiaccounts/settings" "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/identity" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/verification" ) type ContactRequestState int const ( ContactRequestStateNone ContactRequestState = iota ContactRequestStateMutual ContactRequestStateSent // Received is a confusing state, we should use // sent for both, since they are now stored in different // states ContactRequestStateReceived ContactRequestStateDismissed ) type MutualStateUpdateType int const ( MutualStateUpdateTypeSent MutualStateUpdateType = iota + 1 MutualStateUpdateTypeAdded MutualStateUpdateTypeRemoved ) // ContactDeviceInfo is a struct containing information about a particular device owned by a contact type ContactDeviceInfo struct { // The installation id of the device InstallationID string `json:"id"` // Timestamp represents the last time we received this info Timestamp int64 `json:"timestamp"` // FCMToken is to be used for push notifications FCMToken string `json:"fcmToken"` } func (c *Contact) CanonicalImage(profilePicturesVisibility settings.ProfilePicturesVisibilityType) string { if profilePicturesVisibility == settings.ProfilePicturesVisibilityNone || (profilePicturesVisibility == settings.ProfilePicturesVisibilityContactsOnly && !c.added()) { return c.Identicon } if largeImage, ok := c.Images[images.LargeDimName]; ok { imageBase64, err := largeImage.GetDataURI() if err == nil { return imageBase64 } } if thumbImage, ok := c.Images[images.SmallDimName]; ok { imageBase64, err := thumbImage.GetDataURI() if err == nil { return imageBase64 } } return c.Identicon } type VerificationStatus int const ( VerificationStatusUNVERIFIED VerificationStatus = iota VerificationStatusVERIFYING VerificationStatusVERIFIED ) // Contact has information about a "Contact" type Contact struct { // ID of the contact. It's a hex-encoded public key (prefixed with 0x). ID string `json:"id"` // Ethereum address of the contact Address string `json:"address,omitempty"` // ENS name of contact EnsName string `json:"name,omitempty"` // EnsVerified whether we verified the name of the contact ENSVerified bool `json:"ensVerified"` // Generated username name of the contact Alias string `json:"alias,omitempty"` // Identicon generated from public key Identicon string `json:"identicon"` // LastUpdated is the last time we received an update from the contact // updates should be discarded if last updated is less than the one stored LastUpdated uint64 `json:"lastUpdated"` // LastUpdatedLocally is the last time we updated the contact locally LastUpdatedLocally uint64 `json:"lastUpdatedLocally"` LocalNickname string `json:"localNickname,omitempty"` // Display name of the contact DisplayName string `json:"displayName"` // Customization color of the contact CustomizationColor multiaccountscommon.CustomizationColor `json:"customizationColor,omitempty"` // Bio - description of the contact (tell us about yourself) Bio string `json:"bio"` // Deprecated: use social links from ProfileShowcasePreferences SocialLinks identity.SocialLinks `json:"socialLinks"` Images map[string]images.IdentityImage `json:"images"` Blocked bool `json:"blocked"` // ContactRequestRemoteState is the state of the contact request // on the contact's end ContactRequestRemoteState ContactRequestState `json:"contactRequestRemoteState"` // ContactRequestRemoteClock is the clock for incoming contact requests ContactRequestRemoteClock uint64 `json:"contactRequestRemoteClock"` // ContactRequestLocalState is the state of the contact request // on our end ContactRequestLocalState ContactRequestState `json:"contactRequestLocalState"` // ContactRequestLocalClock is the clock for outgoing contact requests ContactRequestLocalClock uint64 `json:"contactRequestLocalClock"` IsSyncing bool Removed bool VerificationStatus VerificationStatus `json:"verificationStatus"` TrustStatus verification.TrustStatus `json:"trustStatus"` } func (c Contact) IsVerified() bool { return c.VerificationStatus == VerificationStatusVERIFIED } func (c Contact) IsVerifying() bool { return c.VerificationStatus == VerificationStatusVERIFYING } func (c Contact) IsUnverified() bool { return c.VerificationStatus == VerificationStatusUNVERIFIED } func (c Contact) IsUntrustworthy() bool { return c.TrustStatus == verification.TrustStatusUNTRUSTWORTHY } func (c Contact) IsTrusted() bool { return c.TrustStatus == verification.TrustStatusTRUSTED } func (c Contact) PublicKey() (*ecdsa.PublicKey, error) { b, err := types.DecodeHex(c.ID) if err != nil { return nil, err } return crypto.UnmarshalPubkey(b) } func (c *Contact) Block(clock uint64) { c.Blocked = true c.DismissContactRequest(clock) c.Removed = true } func (c *Contact) BlockDesktop() { c.Blocked = true } func (c *Contact) Unblock(clock uint64) { c.Blocked = false // Reset the contact request flow c.RetractContactRequest(clock) } func (c *Contact) added() bool { return c.ContactRequestLocalState == ContactRequestStateSent } func (c *Contact) hasAddedUs() bool { return c.ContactRequestRemoteState == ContactRequestStateReceived } func (c *Contact) mutual() bool { return c.added() && c.hasAddedUs() } func (c *Contact) active() bool { return c.mutual() && !c.Blocked } func (c *Contact) dismissed() bool { return c.ContactRequestLocalState == ContactRequestStateDismissed } func (c *Contact) names() []string { var names []string if c.LocalNickname != "" { names = append(names, c.LocalNickname) } if c.ENSVerified && len(c.EnsName) != 0 { names = append(names, c.EnsName) } if c.DisplayName != "" { names = append(names, c.DisplayName) } return append(names, c.Alias) } func (c *Contact) PrimaryName() string { return c.names()[0] } func (c *Contact) SecondaryName() string { // Only shown if the user has a nickname if c.LocalNickname == "" { return "" } names := c.names() if len(names) > 1 { return names[1] } return "" } type ContactRequestProcessingResponse struct { processed bool newContactRequestReceived bool sendBackState bool } func (c *Contact) ContactRequestSent(clock uint64) ContactRequestProcessingResponse { if clock <= c.ContactRequestLocalClock { return ContactRequestProcessingResponse{} } c.ContactRequestLocalClock = clock c.ContactRequestLocalState = ContactRequestStateSent c.Removed = false return ContactRequestProcessingResponse{processed: true} } func (c *Contact) AcceptContactRequest(clock uint64) ContactRequestProcessingResponse { // We treat accept the same as sent, that's because accepting a contact // request that does not exist is possible if the instruction is coming from // a different device, we'd rather assume that a contact requested existed // and didn't reach our device than being in an inconsistent state return c.ContactRequestSent(clock) } func (c *Contact) RetractContactRequest(clock uint64) ContactRequestProcessingResponse { if clock <= c.ContactRequestLocalClock { return ContactRequestProcessingResponse{} } // This is a symmetric action, we set both local & remote clock // since we want everything before this point discarded, regardless // the side it was sent from c.ContactRequestLocalClock = clock c.ContactRequestLocalState = ContactRequestStateNone c.ContactRequestRemoteState = ContactRequestStateNone c.ContactRequestRemoteClock = clock c.Removed = true return ContactRequestProcessingResponse{processed: true} } func (c *Contact) DismissContactRequest(clock uint64) ContactRequestProcessingResponse { if clock <= c.ContactRequestLocalClock { return ContactRequestProcessingResponse{} } c.ContactRequestLocalClock = clock c.ContactRequestLocalState = ContactRequestStateDismissed return ContactRequestProcessingResponse{processed: true} } // Remote actions func (c *Contact) contactRequestRetracted(clock uint64, fromSyncing bool, r ContactRequestProcessingResponse) ContactRequestProcessingResponse { if clock <= c.ContactRequestRemoteClock { return r } // This is a symmetric action, we set both local & remote clock // since we want everything before this point discarded, regardless // the side it was sent from. The only exception is when the contact // request has been explicitly dismissed, in which case we don't // change state if c.ContactRequestLocalState != ContactRequestStateDismissed && !fromSyncing { c.ContactRequestLocalClock = clock c.ContactRequestLocalState = ContactRequestStateNone } c.ContactRequestRemoteClock = clock c.ContactRequestRemoteState = ContactRequestStateNone r.processed = true return r } func (c *Contact) ContactRequestRetracted(clock uint64, fromSyncing bool) ContactRequestProcessingResponse { return c.contactRequestRetracted(clock, fromSyncing, ContactRequestProcessingResponse{}) } func (c *Contact) contactRequestReceived(clock uint64, r ContactRequestProcessingResponse) ContactRequestProcessingResponse { if clock <= c.ContactRequestRemoteClock { return r } r.processed = true c.ContactRequestRemoteClock = clock switch c.ContactRequestRemoteState { case ContactRequestStateNone: r.newContactRequestReceived = true } c.ContactRequestRemoteState = ContactRequestStateReceived return r } func (c *Contact) ContactRequestReceived(clock uint64) ContactRequestProcessingResponse { return c.contactRequestReceived(clock, ContactRequestProcessingResponse{}) } func (c *Contact) ContactRequestAccepted(clock uint64) ContactRequestProcessingResponse { if clock <= c.ContactRequestRemoteClock { return ContactRequestProcessingResponse{} } // We treat received and accepted in the same way // since the intention is clear on the other side // and there's no difference return c.ContactRequestReceived(clock) } func buildContactFromPkString(pkString string) (*Contact, error) { publicKeyBytes, err := types.DecodeHex(pkString) if err != nil { return nil, err } publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) if err != nil { return nil, err } return buildContact(pkString, publicKey) } func BuildContactFromPublicKey(publicKey *ecdsa.PublicKey) (*Contact, error) { id := common.PubkeyToHex(publicKey) return buildContact(id, publicKey) } func getShortenedCompressedKey(publicKey string) string { if len(publicKey) > 9 { firstPart := publicKey[0:3] ellipsis := "..." publicKeySize := len(publicKey) lastPart := publicKey[publicKeySize-6 : publicKeySize] abbreviatedKey := fmt.Sprintf("%s%s%s", firstPart, ellipsis, lastPart) return abbreviatedKey } return "" } func buildContact(publicKeyString string, publicKey *ecdsa.PublicKey) (*Contact, error) { compressedKey, err := multiformat.SerializeLegacyKey(common.PubkeyToHex(publicKey)) if err != nil { return nil, err } address := crypto.PubkeyToAddress(*publicKey) contact := &Contact{ ID: publicKeyString, Alias: getShortenedCompressedKey(compressedKey), Address: types.EncodeHex(address[:]), CustomizationColor: multiaccountscommon.CustomizationColorBlue, } return contact, nil } func buildSelfContact(identity *ecdsa.PrivateKey, settings *accounts.Database, multiAccounts *multiaccounts.Database, account *multiaccounts.Account) (*Contact, error) { myPublicKeyString := types.EncodeHex(crypto.FromECDSAPub(&identity.PublicKey)) c, err := buildContact(myPublicKeyString, &identity.PublicKey) if err != nil { return nil, fmt.Errorf("failed to build contact: %w", err) } if settings != nil { if s, err := settings.GetSettings(); err == nil { c.DisplayName = s.DisplayName c.Bio = s.Bio if s.PreferredName != nil { c.EnsName = *s.PreferredName } } if socialLinks, err := settings.GetSocialLinks(); err != nil { c.SocialLinks = socialLinks } } if multiAccounts != nil && account != nil { if identityImages, err := multiAccounts.GetIdentityImages(account.KeyUID); err != nil { imagesMap := make(map[string]images.IdentityImage) for _, img := range identityImages { imagesMap[img.Name] = *img } c.Images = imagesMap } if len(account.CustomizationColor) != 0 { c.CustomizationColor = account.CustomizationColor } } return c, nil } func contactIDFromPublicKey(key *ecdsa.PublicKey) string { return types.EncodeHex(crypto.FromECDSAPub(key)) } func contactIDFromPublicKeyString(key string) (string, error) { pubKey, err := common.HexToPubkey(key) if err != nil { return "", err } return contactIDFromPublicKey(pubKey), nil } func (c *Contact) ProcessSyncContactRequestState(remoteState ContactRequestState, remoteClock uint64, localState ContactRequestState, localClock uint64) { // We process the two separately, first local state switch localState { case ContactRequestStateDismissed: c.DismissContactRequest(localClock) case ContactRequestStateNone: c.RetractContactRequest(localClock) case ContactRequestStateSent: c.ContactRequestSent(localClock) } // and later remote state switch remoteState { case ContactRequestStateReceived: c.ContactRequestReceived(remoteClock) case ContactRequestStateNone: c.ContactRequestRetracted(remoteClock, true) } } func (c *Contact) MarshalJSON() ([]byte, error) { type Alias Contact type ContactType struct { *Alias Added bool `json:"added"` ContactRequestState ContactRequestState `json:"contactRequestState"` HasAddedUs bool `json:"hasAddedUs"` Mutual bool `json:"mutual"` Active bool `json:"active"` PrimaryName string `json:"primaryName"` SecondaryName string `json:"secondaryName,omitempty"` } item := ContactType{ Alias: (*Alias)(c), } item.Added = c.added() item.HasAddedUs = c.hasAddedUs() item.Mutual = c.mutual() item.Active = c.active() item.PrimaryName = c.PrimaryName() item.SecondaryName = c.SecondaryName() if c.mutual() { item.ContactRequestState = ContactRequestStateMutual } else if c.dismissed() { item.ContactRequestState = ContactRequestStateDismissed } else if c.added() { item.ContactRequestState = ContactRequestStateSent } else if c.hasAddedUs() { item.ContactRequestState = ContactRequestStateReceived } ext, err := accountJson.ExtendStructWithPubKeyData(item.ID, item) if err != nil { return nil, err } return json.Marshal(ext) } // ContactRequestPropagatedStateReceived handles the propagation of state from // the other end. func (c *Contact) ContactRequestPropagatedStateReceived(state *protobuf.ContactRequestPropagatedState) ContactRequestProcessingResponse { // It's inverted, as their local states is our remote state expectedLocalState := ContactRequestState(state.RemoteState) expectedLocalClock := state.RemoteClock remoteState := ContactRequestState(state.LocalState) remoteClock := state.LocalClock response := ContactRequestProcessingResponse{} // If we notice that the state is not consistent, and their clock is // outdated, we send back the state so they can catch up. if expectedLocalClock < c.ContactRequestLocalClock && expectedLocalState != c.ContactRequestLocalState { response.processed = true response.sendBackState = true } // If they expect our state to be more up-to-date, we only // trust it if the state is set to None, in this case we can trust // it, since a retraction can be initiated by both parties if expectedLocalClock > c.ContactRequestLocalClock && c.ContactRequestLocalState != ContactRequestStateDismissed && expectedLocalState == ContactRequestStateNone { response.processed = true c.ContactRequestLocalClock = expectedLocalClock c.ContactRequestLocalState = ContactRequestStateNone // We set the remote state, as this was an implicit retraction // potentially, for example this could happen if they // sent a retraction earier, but we never received it, // or one of our paired devices has retracted the contact request // but we never synced with them. c.ContactRequestRemoteState = ContactRequestStateNone } // We always trust this if remoteClock > c.ContactRequestRemoteClock { if remoteState == ContactRequestStateSent { response = c.contactRequestReceived(remoteClock, response) } else if remoteState == ContactRequestStateNone { response = c.contactRequestRetracted(remoteClock, false, response) } } return response } func (c *Contact) ContactRequestPropagatedState() *protobuf.ContactRequestPropagatedState { return &protobuf.ContactRequestPropagatedState{ LocalClock: c.ContactRequestLocalClock, LocalState: uint64(c.ContactRequestLocalState), RemoteClock: c.ContactRequestRemoteClock, RemoteState: uint64(c.ContactRequestRemoteState), } }