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 }