github.com/status-im/status-go@v1.1.0/protocol/contact.go (about)

     1  package protocol
     2  
     3  import (
     4  	"crypto/ecdsa"
     5  	"encoding/json"
     6  	"fmt"
     7  
     8  	accountJson "github.com/status-im/status-go/account/json"
     9  	"github.com/status-im/status-go/api/multiformat"
    10  	"github.com/status-im/status-go/eth-node/crypto"
    11  	"github.com/status-im/status-go/eth-node/types"
    12  	"github.com/status-im/status-go/images"
    13  	"github.com/status-im/status-go/multiaccounts"
    14  	"github.com/status-im/status-go/multiaccounts/accounts"
    15  	multiaccountscommon "github.com/status-im/status-go/multiaccounts/common"
    16  	"github.com/status-im/status-go/multiaccounts/settings"
    17  	"github.com/status-im/status-go/protocol/common"
    18  	"github.com/status-im/status-go/protocol/protobuf"
    19  	"github.com/status-im/status-go/protocol/verification"
    20  )
    21  
    22  type ContactRequestState int
    23  
    24  const (
    25  	ContactRequestStateNone ContactRequestState = iota
    26  	ContactRequestStateMutual
    27  	ContactRequestStateSent
    28  	// Received is a confusing state, we should use
    29  	// sent for both, since they are now stored in different
    30  	// states
    31  	ContactRequestStateReceived
    32  	ContactRequestStateDismissed
    33  )
    34  
    35  type MutualStateUpdateType int
    36  
    37  const (
    38  	MutualStateUpdateTypeSent MutualStateUpdateType = iota + 1
    39  	MutualStateUpdateTypeAdded
    40  	MutualStateUpdateTypeRemoved
    41  )
    42  
    43  // ContactDeviceInfo is a struct containing information about a particular device owned by a contact
    44  type ContactDeviceInfo struct {
    45  	// The installation id of the device
    46  	InstallationID string `json:"id"`
    47  	// Timestamp represents the last time we received this info
    48  	Timestamp int64 `json:"timestamp"`
    49  	// FCMToken is to be used for push notifications
    50  	FCMToken string `json:"fcmToken"`
    51  }
    52  
    53  func (c *Contact) CanonicalImage(profilePicturesVisibility settings.ProfilePicturesVisibilityType) string {
    54  	if profilePicturesVisibility == settings.ProfilePicturesVisibilityNone || (profilePicturesVisibility == settings.ProfilePicturesVisibilityContactsOnly && !c.added()) {
    55  		return c.Identicon
    56  	}
    57  
    58  	if largeImage, ok := c.Images[images.LargeDimName]; ok {
    59  		imageBase64, err := largeImage.GetDataURI()
    60  		if err == nil {
    61  			return imageBase64
    62  		}
    63  	}
    64  
    65  	if thumbImage, ok := c.Images[images.SmallDimName]; ok {
    66  		imageBase64, err := thumbImage.GetDataURI()
    67  		if err == nil {
    68  			return imageBase64
    69  		}
    70  	}
    71  
    72  	return c.Identicon
    73  }
    74  
    75  type VerificationStatus int
    76  
    77  const (
    78  	VerificationStatusUNVERIFIED VerificationStatus = iota
    79  	VerificationStatusVERIFYING
    80  	VerificationStatusVERIFIED
    81  )
    82  
    83  // Contact has information about a "Contact"
    84  type Contact struct {
    85  	// ID of the contact. It's a hex-encoded public key (prefixed with 0x).
    86  	ID string `json:"id"`
    87  	// Ethereum address of the contact
    88  	Address string `json:"address,omitempty"`
    89  	// ENS name of contact
    90  	EnsName string `json:"name,omitempty"`
    91  	// EnsVerified whether we verified the name of the contact
    92  	ENSVerified bool `json:"ensVerified"`
    93  	// Generated username name of the contact
    94  	Alias string `json:"alias,omitempty"`
    95  	// Identicon generated from public key
    96  	Identicon string `json:"identicon"`
    97  	// LastUpdated is the last time we received an update from the contact
    98  	// updates should be discarded if last updated is less than the one stored
    99  	LastUpdated uint64 `json:"lastUpdated"`
   100  
   101  	// LastUpdatedLocally is the last time we updated the contact locally
   102  	LastUpdatedLocally uint64 `json:"lastUpdatedLocally"`
   103  
   104  	LocalNickname string `json:"localNickname,omitempty"`
   105  
   106  	// Display name of the contact
   107  	DisplayName string `json:"displayName"`
   108  
   109  	// Customization color of the contact
   110  	CustomizationColor multiaccountscommon.CustomizationColor `json:"customizationColor,omitempty"`
   111  
   112  	// Bio - description of the contact (tell us about yourself)
   113  	Bio string `json:"bio"`
   114  
   115  	Images map[string]images.IdentityImage `json:"images"`
   116  
   117  	Blocked bool `json:"blocked"`
   118  
   119  	// ContactRequestRemoteState is the state of the contact request
   120  	// on the contact's end
   121  	ContactRequestRemoteState ContactRequestState `json:"contactRequestRemoteState"`
   122  	// ContactRequestRemoteClock is the clock for incoming contact requests
   123  	ContactRequestRemoteClock uint64 `json:"contactRequestRemoteClock"`
   124  
   125  	// ContactRequestLocalState is the state of the contact request
   126  	// on our end
   127  	ContactRequestLocalState ContactRequestState `json:"contactRequestLocalState"`
   128  	// ContactRequestLocalClock is the clock for outgoing contact requests
   129  	ContactRequestLocalClock uint64 `json:"contactRequestLocalClock"`
   130  
   131  	IsSyncing bool
   132  	Removed   bool
   133  
   134  	VerificationStatus VerificationStatus       `json:"verificationStatus"`
   135  	TrustStatus        verification.TrustStatus `json:"trustStatus"`
   136  }
   137  
   138  func (c Contact) IsVerified() bool {
   139  	return c.VerificationStatus == VerificationStatusVERIFIED
   140  }
   141  
   142  func (c Contact) IsVerifying() bool {
   143  	return c.VerificationStatus == VerificationStatusVERIFYING
   144  }
   145  
   146  func (c Contact) IsUnverified() bool {
   147  	return c.VerificationStatus == VerificationStatusUNVERIFIED
   148  }
   149  
   150  func (c Contact) IsUntrustworthy() bool {
   151  	return c.TrustStatus == verification.TrustStatusUNTRUSTWORTHY
   152  }
   153  
   154  func (c Contact) IsTrusted() bool {
   155  	return c.TrustStatus == verification.TrustStatusTRUSTED
   156  }
   157  
   158  func (c Contact) PublicKey() (*ecdsa.PublicKey, error) {
   159  	b, err := types.DecodeHex(c.ID)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	return crypto.UnmarshalPubkey(b)
   164  }
   165  
   166  func (c *Contact) Block(clock uint64) {
   167  	c.Blocked = true
   168  	c.DismissContactRequest(clock)
   169  	c.Removed = true
   170  }
   171  
   172  func (c *Contact) BlockDesktop() {
   173  	c.Blocked = true
   174  }
   175  
   176  func (c *Contact) Unblock(clock uint64) {
   177  	c.Blocked = false
   178  	// Reset the contact request flow
   179  	c.RetractContactRequest(clock)
   180  }
   181  
   182  func (c *Contact) added() bool {
   183  	return c.ContactRequestLocalState == ContactRequestStateSent
   184  }
   185  
   186  func (c *Contact) hasAddedUs() bool {
   187  	return c.ContactRequestRemoteState == ContactRequestStateReceived
   188  }
   189  
   190  func (c *Contact) mutual() bool {
   191  	return c.added() && c.hasAddedUs()
   192  }
   193  
   194  func (c *Contact) active() bool {
   195  	return c.mutual() && !c.Blocked
   196  }
   197  
   198  func (c *Contact) dismissed() bool {
   199  	return c.ContactRequestLocalState == ContactRequestStateDismissed
   200  }
   201  
   202  func (c *Contact) names() []string {
   203  	var names []string
   204  
   205  	if c.LocalNickname != "" {
   206  		names = append(names, c.LocalNickname)
   207  	}
   208  
   209  	if c.ENSVerified && len(c.EnsName) != 0 {
   210  		names = append(names, c.EnsName)
   211  	}
   212  
   213  	if c.DisplayName != "" {
   214  		names = append(names, c.DisplayName)
   215  	}
   216  
   217  	return append(names, c.Alias)
   218  
   219  }
   220  
   221  func (c *Contact) PrimaryName() string {
   222  	return c.names()[0]
   223  }
   224  
   225  func (c *Contact) SecondaryName() string {
   226  	// Only shown if the user has a nickname
   227  	if c.LocalNickname == "" {
   228  		return ""
   229  	}
   230  	names := c.names()
   231  	if len(names) > 1 {
   232  		return names[1]
   233  	}
   234  	return ""
   235  }
   236  
   237  type ContactRequestProcessingResponse struct {
   238  	processed                 bool
   239  	newContactRequestReceived bool
   240  	sendBackState             bool
   241  }
   242  
   243  func (c *Contact) ContactRequestSent(clock uint64) ContactRequestProcessingResponse {
   244  	if clock <= c.ContactRequestLocalClock {
   245  		return ContactRequestProcessingResponse{}
   246  	}
   247  
   248  	c.ContactRequestLocalClock = clock
   249  	c.ContactRequestLocalState = ContactRequestStateSent
   250  
   251  	c.Removed = false
   252  
   253  	return ContactRequestProcessingResponse{processed: true}
   254  }
   255  
   256  func (c *Contact) AcceptContactRequest(clock uint64) ContactRequestProcessingResponse {
   257  	// We treat accept the same as sent, that's because accepting a contact
   258  	// request that does not exist is possible if the instruction is coming from
   259  	// a different device, we'd rather assume that a contact requested existed
   260  	// and didn't reach our device than being in an inconsistent state
   261  	return c.ContactRequestSent(clock)
   262  }
   263  
   264  func (c *Contact) RetractContactRequest(clock uint64) ContactRequestProcessingResponse {
   265  	if clock <= c.ContactRequestLocalClock {
   266  		return ContactRequestProcessingResponse{}
   267  	}
   268  
   269  	// This is a symmetric action, we set both local & remote clock
   270  	// since we want everything before this point discarded, regardless
   271  	// the side it was sent from
   272  	c.ContactRequestLocalClock = clock
   273  	c.ContactRequestLocalState = ContactRequestStateNone
   274  	c.ContactRequestRemoteState = ContactRequestStateNone
   275  	c.ContactRequestRemoteClock = clock
   276  	c.Removed = true
   277  
   278  	return ContactRequestProcessingResponse{processed: true}
   279  }
   280  
   281  func (c *Contact) DismissContactRequest(clock uint64) ContactRequestProcessingResponse {
   282  	if clock <= c.ContactRequestLocalClock {
   283  		return ContactRequestProcessingResponse{}
   284  	}
   285  
   286  	c.ContactRequestLocalClock = clock
   287  	c.ContactRequestLocalState = ContactRequestStateDismissed
   288  
   289  	return ContactRequestProcessingResponse{processed: true}
   290  }
   291  
   292  // Remote actions
   293  
   294  func (c *Contact) contactRequestRetracted(clock uint64, fromSyncing bool, r ContactRequestProcessingResponse) ContactRequestProcessingResponse {
   295  	if clock <= c.ContactRequestRemoteClock {
   296  		return r
   297  	}
   298  
   299  	// This is a symmetric action, we set both local & remote clock
   300  	// since we want everything before this point discarded, regardless
   301  	// the side it was sent from. The only exception is when the contact
   302  	// request has been explicitly dismissed, in which case we don't
   303  	// change state
   304  	if c.ContactRequestLocalState != ContactRequestStateDismissed && !fromSyncing {
   305  		c.ContactRequestLocalClock = clock
   306  		c.ContactRequestLocalState = ContactRequestStateNone
   307  	}
   308  	c.ContactRequestRemoteClock = clock
   309  	c.ContactRequestRemoteState = ContactRequestStateNone
   310  	r.processed = true
   311  	return r
   312  }
   313  
   314  func (c *Contact) ContactRequestRetracted(clock uint64, fromSyncing bool) ContactRequestProcessingResponse {
   315  	return c.contactRequestRetracted(clock, fromSyncing, ContactRequestProcessingResponse{})
   316  }
   317  
   318  func (c *Contact) contactRequestReceived(clock uint64, r ContactRequestProcessingResponse) ContactRequestProcessingResponse {
   319  	if clock <= c.ContactRequestRemoteClock {
   320  		return r
   321  	}
   322  	r.processed = true
   323  	c.ContactRequestRemoteClock = clock
   324  	switch c.ContactRequestRemoteState {
   325  	case ContactRequestStateNone:
   326  		r.newContactRequestReceived = true
   327  	}
   328  	c.ContactRequestRemoteState = ContactRequestStateReceived
   329  
   330  	return r
   331  }
   332  
   333  func (c *Contact) ContactRequestReceived(clock uint64) ContactRequestProcessingResponse {
   334  	return c.contactRequestReceived(clock, ContactRequestProcessingResponse{})
   335  }
   336  
   337  func (c *Contact) ContactRequestAccepted(clock uint64) ContactRequestProcessingResponse {
   338  	if clock <= c.ContactRequestRemoteClock {
   339  		return ContactRequestProcessingResponse{}
   340  	}
   341  	// We treat received and accepted in the same way
   342  	// since the intention is clear on the other side
   343  	// and there's no difference
   344  	return c.ContactRequestReceived(clock)
   345  }
   346  
   347  func buildContactFromPkString(pkString string) (*Contact, error) {
   348  	publicKeyBytes, err := types.DecodeHex(pkString)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes)
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  
   358  	return buildContact(pkString, publicKey)
   359  }
   360  
   361  func BuildContactFromPublicKey(publicKey *ecdsa.PublicKey) (*Contact, error) {
   362  	id := common.PubkeyToHex(publicKey)
   363  	return buildContact(id, publicKey)
   364  }
   365  
   366  func getShortenedCompressedKey(publicKey string) string {
   367  	if len(publicKey) > 9 {
   368  		firstPart := publicKey[0:3]
   369  		ellipsis := "..."
   370  		publicKeySize := len(publicKey)
   371  		lastPart := publicKey[publicKeySize-6 : publicKeySize]
   372  		abbreviatedKey := fmt.Sprintf("%s%s%s", firstPart, ellipsis, lastPart)
   373  		return abbreviatedKey
   374  	}
   375  	return ""
   376  }
   377  
   378  func buildContact(publicKeyString string, publicKey *ecdsa.PublicKey) (*Contact, error) {
   379  	compressedKey, err := multiformat.SerializeLegacyKey(common.PubkeyToHex(publicKey))
   380  	if err != nil {
   381  		return nil, err
   382  	}
   383  
   384  	address := crypto.PubkeyToAddress(*publicKey)
   385  
   386  	contact := &Contact{
   387  		ID:                 publicKeyString,
   388  		Alias:              getShortenedCompressedKey(compressedKey),
   389  		Address:            types.EncodeHex(address[:]),
   390  		CustomizationColor: multiaccountscommon.CustomizationColorBlue,
   391  	}
   392  
   393  	return contact, nil
   394  }
   395  
   396  func buildSelfContact(identity *ecdsa.PrivateKey, settings *accounts.Database, multiAccounts *multiaccounts.Database, account *multiaccounts.Account) (*Contact, error) {
   397  	myPublicKeyString := types.EncodeHex(crypto.FromECDSAPub(&identity.PublicKey))
   398  
   399  	c, err := buildContact(myPublicKeyString, &identity.PublicKey)
   400  	if err != nil {
   401  		return nil, fmt.Errorf("failed to build contact: %w", err)
   402  	}
   403  
   404  	if settings != nil {
   405  		if s, err := settings.GetSettings(); err == nil {
   406  			c.DisplayName = s.DisplayName
   407  			c.Bio = s.Bio
   408  			if s.PreferredName != nil {
   409  				c.EnsName = *s.PreferredName
   410  			}
   411  		}
   412  	}
   413  
   414  	if multiAccounts != nil && account != nil {
   415  		if identityImages, err := multiAccounts.GetIdentityImages(account.KeyUID); err != nil {
   416  			imagesMap := make(map[string]images.IdentityImage)
   417  			for _, img := range identityImages {
   418  				imagesMap[img.Name] = *img
   419  			}
   420  
   421  			c.Images = imagesMap
   422  		}
   423  		if len(account.CustomizationColor) != 0 {
   424  			c.CustomizationColor = account.CustomizationColor
   425  		}
   426  	}
   427  
   428  	return c, nil
   429  }
   430  
   431  func contactIDFromPublicKey(key *ecdsa.PublicKey) string {
   432  	return types.EncodeHex(crypto.FromECDSAPub(key))
   433  }
   434  
   435  func contactIDFromPublicKeyString(key string) (string, error) {
   436  	pubKey, err := common.HexToPubkey(key)
   437  	if err != nil {
   438  		return "", err
   439  	}
   440  
   441  	return contactIDFromPublicKey(pubKey), nil
   442  }
   443  
   444  func (c *Contact) ProcessSyncContactRequestState(remoteState ContactRequestState, remoteClock uint64, localState ContactRequestState, localClock uint64) {
   445  	// We process the two separately, first local state
   446  	switch localState {
   447  	case ContactRequestStateDismissed:
   448  		c.DismissContactRequest(localClock)
   449  	case ContactRequestStateNone:
   450  		c.RetractContactRequest(localClock)
   451  	case ContactRequestStateSent:
   452  		c.ContactRequestSent(localClock)
   453  	}
   454  
   455  	// and later remote state
   456  	switch remoteState {
   457  	case ContactRequestStateReceived:
   458  		c.ContactRequestReceived(remoteClock)
   459  	case ContactRequestStateNone:
   460  		c.ContactRequestRetracted(remoteClock, true)
   461  	}
   462  }
   463  
   464  func (c *Contact) MarshalJSON() ([]byte, error) {
   465  	type Alias Contact
   466  	type ContactType struct {
   467  		*Alias
   468  		Added               bool                `json:"added"`
   469  		ContactRequestState ContactRequestState `json:"contactRequestState"`
   470  		HasAddedUs          bool                `json:"hasAddedUs"`
   471  		Mutual              bool                `json:"mutual"`
   472  		Active              bool                `json:"active"`
   473  		PrimaryName         string              `json:"primaryName"`
   474  		SecondaryName       string              `json:"secondaryName,omitempty"`
   475  	}
   476  
   477  	item := ContactType{
   478  		Alias: (*Alias)(c),
   479  	}
   480  
   481  	item.Added = c.added()
   482  	item.HasAddedUs = c.hasAddedUs()
   483  	item.Mutual = c.mutual()
   484  	item.Active = c.active()
   485  	item.PrimaryName = c.PrimaryName()
   486  	item.SecondaryName = c.SecondaryName()
   487  
   488  	if c.mutual() {
   489  		item.ContactRequestState = ContactRequestStateMutual
   490  	} else if c.dismissed() {
   491  		item.ContactRequestState = ContactRequestStateDismissed
   492  	} else if c.added() {
   493  		item.ContactRequestState = ContactRequestStateSent
   494  	} else if c.hasAddedUs() {
   495  		item.ContactRequestState = ContactRequestStateReceived
   496  	}
   497  	ext, err := accountJson.ExtendStructWithPubKeyData(item.ID, item)
   498  	if err != nil {
   499  		return nil, err
   500  	}
   501  
   502  	return json.Marshal(ext)
   503  }
   504  
   505  // ContactRequestPropagatedStateReceived handles the propagation of state from
   506  // the other end.
   507  func (c *Contact) ContactRequestPropagatedStateReceived(state *protobuf.ContactRequestPropagatedState) ContactRequestProcessingResponse {
   508  
   509  	// It's inverted, as their local states is our remote state
   510  	expectedLocalState := ContactRequestState(state.RemoteState)
   511  	expectedLocalClock := state.RemoteClock
   512  
   513  	remoteState := ContactRequestState(state.LocalState)
   514  	remoteClock := state.LocalClock
   515  
   516  	response := ContactRequestProcessingResponse{}
   517  
   518  	// If we notice that the state is not consistent, and their clock is
   519  	// outdated, we send back the state so they can catch up.
   520  	if expectedLocalClock < c.ContactRequestLocalClock && expectedLocalState != c.ContactRequestLocalState {
   521  		response.processed = true
   522  		response.sendBackState = true
   523  	}
   524  
   525  	// If they expect our state to be more up-to-date, we only
   526  	// trust it if the state is set to None, in this case we can trust
   527  	// it, since a retraction can be initiated by both parties
   528  	if expectedLocalClock > c.ContactRequestLocalClock && c.ContactRequestLocalState != ContactRequestStateDismissed && expectedLocalState == ContactRequestStateNone {
   529  		response.processed = true
   530  		c.ContactRequestLocalClock = expectedLocalClock
   531  		c.ContactRequestLocalState = ContactRequestStateNone
   532  		// We set the remote state, as this was an implicit retraction
   533  		// potentially, for example this could happen if they
   534  		// sent a retraction earier, but we never received it,
   535  		// or one of our paired devices has retracted the contact request
   536  		// but we never synced with them.
   537  		c.ContactRequestRemoteState = ContactRequestStateNone
   538  	}
   539  
   540  	// We always trust this
   541  	if remoteClock > c.ContactRequestRemoteClock {
   542  		if remoteState == ContactRequestStateSent {
   543  			response = c.contactRequestReceived(remoteClock, response)
   544  		} else if remoteState == ContactRequestStateNone {
   545  			response = c.contactRequestRetracted(remoteClock, false, response)
   546  		}
   547  	}
   548  
   549  	return response
   550  }
   551  
   552  func (c *Contact) ContactRequestPropagatedState() *protobuf.ContactRequestPropagatedState {
   553  	return &protobuf.ContactRequestPropagatedState{
   554  		LocalClock:  c.ContactRequestLocalClock,
   555  		LocalState:  uint64(c.ContactRequestLocalState),
   556  		RemoteClock: c.ContactRequestRemoteClock,
   557  		RemoteState: uint64(c.ContactRequestRemoteState),
   558  	}
   559  }