github.com/status-im/status-go@v1.1.0/protocol/v1/membership_update_message.go (about) 1 package protocol 2 3 import ( 4 "bytes" 5 "crypto/ecdsa" 6 "fmt" 7 "sort" 8 "strings" 9 10 "github.com/golang/protobuf/proto" 11 "github.com/google/uuid" 12 "github.com/pkg/errors" 13 14 "github.com/status-im/status-go/eth-node/crypto" 15 "github.com/status-im/status-go/eth-node/types" 16 "github.com/status-im/status-go/protocol/protobuf" 17 ) 18 19 // MembershipUpdateMessage is a message used to propagate information 20 // about group membership changes. 21 // For more information, see https://github.com/status-im/specs/blob/master/status-group-chats-spec.md. 22 type MembershipUpdateMessage struct { 23 ChatID string `json:"chatId"` // UUID concatenated with hex-encoded public key of the creator for the chat 24 Events []MembershipUpdateEvent `json:"events"` 25 Message *protobuf.ChatMessage `json:"-"` 26 EmojiReaction *protobuf.EmojiReaction `json:"-"` 27 } 28 29 const signatureLength = 65 30 31 func MembershipUpdateEventFromProtobuf(chatID string, raw []byte) (*MembershipUpdateEvent, error) { 32 if len(raw) <= signatureLength { 33 return nil, errors.New("invalid payload length") 34 } 35 decodedEvent := protobuf.MembershipUpdateEvent{} 36 signature := raw[:signatureLength] 37 encodedEvent := raw[signatureLength:] 38 39 signatureMaterial := append([]byte(chatID), encodedEvent...) 40 publicKey, err := crypto.ExtractSignature(signatureMaterial, signature) 41 if err != nil { 42 return nil, errors.Wrap(err, "failed to extract signature") 43 } 44 45 from := publicKeyToString(publicKey) 46 47 err = proto.Unmarshal(encodedEvent, &decodedEvent) 48 if err != nil { 49 return nil, err 50 } 51 return &MembershipUpdateEvent{ 52 ClockValue: decodedEvent.Clock, 53 ChatID: chatID, 54 Members: decodedEvent.Members, 55 Name: decodedEvent.Name, 56 Type: decodedEvent.Type, 57 Color: decodedEvent.Color, 58 Image: decodedEvent.Image, 59 Signature: signature, 60 RawPayload: encodedEvent, 61 From: from, 62 }, nil 63 } 64 65 func (m *MembershipUpdateMessage) ToProtobuf() (*protobuf.MembershipUpdateMessage, error) { 66 var rawEvents [][]byte 67 for _, e := range m.Events { 68 var encodedEvent []byte 69 encodedEvent = append(encodedEvent, e.Signature...) 70 encodedEvent = append(encodedEvent, e.RawPayload...) 71 rawEvents = append(rawEvents, encodedEvent) 72 } 73 74 mUM := &protobuf.MembershipUpdateMessage{ 75 ChatId: m.ChatID, 76 Events: rawEvents, 77 } 78 79 // If message is not piggybacking anything, that's a valid case and we just return 80 switch { 81 case m.Message != nil: 82 mUM.ChatEntity = &protobuf.MembershipUpdateMessage_Message{Message: m.Message} 83 case m.EmojiReaction != nil: 84 mUM.ChatEntity = &protobuf.MembershipUpdateMessage_EmojiReaction{EmojiReaction: m.EmojiReaction} 85 } 86 87 return mUM, nil 88 } 89 90 func MembershipUpdateMessageFromProtobuf(raw *protobuf.MembershipUpdateMessage) (*MembershipUpdateMessage, error) { 91 var events []MembershipUpdateEvent 92 for _, e := range raw.Events { 93 verifiedEvent, err := MembershipUpdateEventFromProtobuf(raw.ChatId, e) 94 if err != nil { 95 return nil, err 96 } 97 events = append(events, *verifiedEvent) 98 } 99 return &MembershipUpdateMessage{ 100 ChatID: raw.ChatId, 101 Events: events, 102 Message: raw.GetMessage(), 103 EmojiReaction: raw.GetEmojiReaction(), 104 }, nil 105 } 106 107 // EncodeMembershipUpdateMessage encodes a MembershipUpdateMessage using protobuf serialization. 108 func EncodeMembershipUpdateMessage(value MembershipUpdateMessage) ([]byte, error) { 109 pb, err := value.ToProtobuf() 110 if err != nil { 111 return nil, err 112 } 113 114 return proto.Marshal(pb) 115 } 116 117 // MembershipUpdateEvent contains an event information. 118 // Member and Members are hex-encoded values with 0x prefix. 119 type MembershipUpdateEvent struct { 120 Type protobuf.MembershipUpdateEvent_EventType `json:"type"` 121 ClockValue uint64 `json:"clockValue"` 122 Members []string `json:"members,omitempty"` // in "members-added" and "admins-added" events 123 Name string `json:"name,omitempty"` // name of the group chat 124 Color string `json:"color,omitempty"` // color of the group chat 125 Image []byte `json:"image,omitempty"` // image of the group chat 126 From string `json:"from,omitempty"` 127 Signature []byte `json:"signature,omitempty"` 128 ChatID string `json:"chatId"` 129 RawPayload []byte `json:"rawPayload"` 130 } 131 132 func (u *MembershipUpdateEvent) Equal(update MembershipUpdateEvent) bool { 133 return bytes.Equal(u.Signature, update.Signature) 134 } 135 136 func (u *MembershipUpdateEvent) Sign(key *ecdsa.PrivateKey) error { 137 if len(u.ChatID) == 0 { 138 return errors.New("can't sign with empty chatID") 139 } 140 encodedEvent, err := proto.Marshal(u.ToProtobuf()) 141 if err != nil { 142 return err 143 } 144 u.RawPayload = encodedEvent 145 var signatureMaterial []byte 146 signatureMaterial = append(signatureMaterial, []byte(u.ChatID)...) 147 signatureMaterial = crypto.Keccak256(append(signatureMaterial, u.RawPayload...)) 148 signature, err := crypto.Sign(signatureMaterial, key) 149 150 if err != nil { 151 return err 152 } 153 u.Signature = signature 154 u.From = publicKeyToString(&key.PublicKey) 155 return nil 156 } 157 158 func (u *MembershipUpdateEvent) ToProtobuf() *protobuf.MembershipUpdateEvent { 159 return &protobuf.MembershipUpdateEvent{ 160 Clock: u.ClockValue, 161 Name: u.Name, 162 Color: u.Color, 163 Image: u.Image, 164 Members: u.Members, 165 Type: u.Type, 166 } 167 } 168 169 func MergeMembershipUpdateEvents(dest []MembershipUpdateEvent, src []MembershipUpdateEvent) []MembershipUpdateEvent { 170 for _, update := range src { 171 var exists bool 172 for _, existing := range dest { 173 if existing.Equal(update) { 174 exists = true 175 break 176 } 177 } 178 if !exists { 179 dest = append(dest, update) 180 } 181 } 182 return dest 183 } 184 185 func NewChatCreatedEvent(name string, color string, clock uint64) MembershipUpdateEvent { 186 return MembershipUpdateEvent{ 187 Type: protobuf.MembershipUpdateEvent_CHAT_CREATED, 188 Name: name, 189 ClockValue: clock, 190 Color: color, 191 } 192 } 193 194 func NewNameChangedEvent(name string, clock uint64) MembershipUpdateEvent { 195 return MembershipUpdateEvent{ 196 Type: protobuf.MembershipUpdateEvent_NAME_CHANGED, 197 Name: name, 198 ClockValue: clock, 199 } 200 } 201 202 func NewColorChangedEvent(color string, clock uint64) MembershipUpdateEvent { 203 return MembershipUpdateEvent{ 204 Type: protobuf.MembershipUpdateEvent_COLOR_CHANGED, 205 Color: color, 206 ClockValue: clock, 207 } 208 } 209 210 func NewImageChangedEvent(image []byte, clock uint64) MembershipUpdateEvent { 211 return MembershipUpdateEvent{ 212 Type: protobuf.MembershipUpdateEvent_IMAGE_CHANGED, 213 Image: image, 214 ClockValue: clock, 215 } 216 } 217 218 func NewMembersAddedEvent(members []string, clock uint64) MembershipUpdateEvent { 219 return MembershipUpdateEvent{ 220 Type: protobuf.MembershipUpdateEvent_MEMBERS_ADDED, 221 Members: members, 222 ClockValue: clock, 223 } 224 } 225 226 func NewMemberJoinedEvent(clock uint64) MembershipUpdateEvent { 227 return MembershipUpdateEvent{ 228 Type: protobuf.MembershipUpdateEvent_MEMBER_JOINED, 229 ClockValue: clock, 230 } 231 } 232 233 func NewAdminsAddedEvent(admins []string, clock uint64) MembershipUpdateEvent { 234 return MembershipUpdateEvent{ 235 Type: protobuf.MembershipUpdateEvent_ADMINS_ADDED, 236 Members: admins, 237 ClockValue: clock, 238 } 239 } 240 241 func NewMemberRemovedEvent(member string, clock uint64) MembershipUpdateEvent { 242 return MembershipUpdateEvent{ 243 Type: protobuf.MembershipUpdateEvent_MEMBER_REMOVED, 244 Members: []string{member}, 245 ClockValue: clock, 246 } 247 } 248 249 func NewAdminRemovedEvent(admin string, clock uint64) MembershipUpdateEvent { 250 return MembershipUpdateEvent{ 251 Type: protobuf.MembershipUpdateEvent_ADMIN_REMOVED, 252 Members: []string{admin}, 253 ClockValue: clock, 254 } 255 } 256 257 type Group struct { 258 chatID string 259 name string 260 color string 261 image []byte 262 events []MembershipUpdateEvent 263 admins *stringSet 264 members *stringSet 265 } 266 267 func groupChatID(creator *ecdsa.PublicKey) string { 268 return uuid.New().String() + "-" + publicKeyToString(creator) 269 } 270 271 func NewGroupWithEvents(chatID string, events []MembershipUpdateEvent) (*Group, error) { 272 return newGroup(chatID, events) 273 } 274 275 func NewGroupWithCreator(name string, color string, clock uint64, creator *ecdsa.PrivateKey) (*Group, error) { 276 chatID := groupChatID(&creator.PublicKey) 277 chatCreated := NewChatCreatedEvent(name, color, clock) 278 chatCreated.ChatID = chatID 279 err := chatCreated.Sign(creator) 280 if err != nil { 281 return nil, err 282 } 283 return newGroup(chatID, []MembershipUpdateEvent{chatCreated}) 284 } 285 286 func newGroup(chatID string, events []MembershipUpdateEvent) (*Group, error) { 287 g := Group{ 288 chatID: chatID, 289 events: events, 290 admins: newStringSet(), 291 members: newStringSet(), 292 } 293 if err := g.init(); err != nil { 294 return nil, err 295 } 296 return &g, nil 297 } 298 299 func (g *Group) init() error { 300 g.sortEvents() 301 302 var chatID string 303 304 for _, event := range g.events { 305 if chatID == "" { 306 chatID = event.ChatID 307 } else if event.ChatID != chatID { 308 return errors.New("updates contain different chat IDs") 309 } 310 valid := g.validateEvent(event) 311 if !valid { 312 return fmt.Errorf("invalid event %#+v from %s", event, event.From) 313 } 314 g.processEvent(event) 315 } 316 317 valid := g.validateChatID(g.chatID) 318 if !valid { 319 return fmt.Errorf("invalid chat ID: %s", g.chatID) 320 } 321 if chatID != g.chatID { 322 return fmt.Errorf("expected chat ID equal %s, got %s", g.chatID, chatID) 323 } 324 325 return nil 326 } 327 328 func (g Group) ChatID() string { 329 return g.chatID 330 } 331 332 func (g Group) Name() string { 333 return g.name 334 } 335 336 func (g Group) Color() string { 337 return g.color 338 } 339 340 func (g Group) Image() []byte { 341 return g.image 342 } 343 344 func (g Group) Events() []MembershipUpdateEvent { 345 return g.events 346 } 347 348 // AbridgedEvents returns the minimum set of events for a user to publish a post 349 // The events we want to keep: 350 // 1) Chat created 351 // 2) Latest color changed 352 // 3) Latest image changed 353 // 4) For each admin, the latest admins added event that contains them 354 // 5) For each member, the latest members added event that contains them 355 // 4 & 5, might bring removed admins or removed members, for those, we also need to 356 // keep the event that removes them 357 func (g Group) AbridgedEvents() []MembershipUpdateEvent { 358 var events []MembershipUpdateEvent 359 var nameChangedEventFound bool 360 var colorChangedEventFound bool 361 var imageChangedEventFound bool 362 removedMembers := make(map[string]*MembershipUpdateEvent) 363 addedMembers := make(map[string]bool) 364 extraMembers := make(map[string]bool) 365 admins := make(map[string]bool) 366 // Iterate in reverse 367 for i := len(g.events) - 1; i >= 0; i-- { 368 event := g.events[i] 369 switch event.Type { 370 case protobuf.MembershipUpdateEvent_CHAT_CREATED: 371 events = append(events, event) 372 case protobuf.MembershipUpdateEvent_NAME_CHANGED: 373 if nameChangedEventFound { 374 continue 375 } 376 events = append(events, event) 377 nameChangedEventFound = true 378 case protobuf.MembershipUpdateEvent_COLOR_CHANGED: 379 if colorChangedEventFound { 380 continue 381 } 382 events = append(events, event) 383 colorChangedEventFound = true 384 case protobuf.MembershipUpdateEvent_IMAGE_CHANGED: 385 if imageChangedEventFound { 386 continue 387 } 388 events = append(events, event) 389 imageChangedEventFound = true 390 391 case protobuf.MembershipUpdateEvent_MEMBERS_ADDED: 392 var shouldAddEvent bool 393 for _, m := range event.Members { 394 // If it's adding a current user, and we don't have a more 395 // recent event 396 // if it's an admin, we track it 397 if admins[m] || (g.members.Has(m) && !addedMembers[m]) { 398 addedMembers[m] = true 399 shouldAddEvent = true 400 } 401 } 402 if shouldAddEvent { 403 // Append the event and check the not current members that are also 404 // added 405 for _, m := range event.Members { 406 if !g.members.Has(m) && !admins[m] { 407 extraMembers[m] = true 408 } 409 } 410 events = append(events, event) 411 } 412 case protobuf.MembershipUpdateEvent_ADMIN_REMOVED: 413 // We add it always for now 414 events = append(events, event) 415 case protobuf.MembershipUpdateEvent_ADMINS_ADDED: 416 // We track admins in full 417 admins[event.Members[0]] = true 418 events = append(events, event) 419 case protobuf.MembershipUpdateEvent_MEMBER_REMOVED: 420 // Save member removed events, as we might need it 421 // to remove members who have been added but subsequently left 422 if removedMembers[event.Members[0]] == nil || removedMembers[event.Members[0]].ClockValue < event.ClockValue { 423 removedMembers[event.Members[0]] = &event 424 } 425 426 case protobuf.MembershipUpdateEvent_MEMBER_JOINED: 427 if g.members.Has(event.From) { 428 events = append(events, event) 429 } 430 431 } 432 } 433 434 for m := range extraMembers { 435 if removedMembers[m] != nil { 436 events = append(events, *removedMembers[m]) 437 } 438 } 439 440 sort.Slice(events, func(i, j int) bool { 441 return events[i].ClockValue < events[j].ClockValue 442 }) 443 444 return events 445 } 446 447 func (g Group) Members() []string { 448 return g.members.List() 449 } 450 451 func (g Group) MemberPublicKeys() ([]*ecdsa.PublicKey, error) { 452 var publicKeys = make([]*ecdsa.PublicKey, 0, len(g.Members())) 453 for _, memberPublicKey := range g.Members() { 454 publicKey, err := hexToPubkey(memberPublicKey) 455 if err != nil { 456 return nil, err 457 } 458 publicKeys = append(publicKeys, publicKey) 459 } 460 return publicKeys, nil 461 } 462 463 func hexToPubkey(pk string) (*ecdsa.PublicKey, error) { 464 bytes, err := types.DecodeHex(pk) 465 if err != nil { 466 return nil, err 467 } 468 return crypto.UnmarshalPubkey(bytes) 469 } 470 471 func (g Group) Admins() []string { 472 return g.admins.List() 473 } 474 475 func (g *Group) ProcessEvents(events []MembershipUpdateEvent) error { 476 for _, event := range events { 477 err := g.ProcessEvent(event) 478 if err != nil { 479 return err 480 } 481 } 482 return nil 483 } 484 485 func (g *Group) ProcessEvent(event MembershipUpdateEvent) error { 486 if !g.validateEvent(event) { 487 return fmt.Errorf("invalid event %#+v", event) 488 } 489 // Check if exists 490 g.events = append(g.events, event) 491 g.processEvent(event) 492 return nil 493 } 494 495 func (g Group) LastClockValue() uint64 { 496 if len(g.events) == 0 { 497 return 0 498 } 499 return g.events[len(g.events)-1].ClockValue 500 } 501 502 func (g Group) Creator() (string, error) { 503 if len(g.events) == 0 { 504 return "", errors.New("no events in the group") 505 } 506 first := g.events[0] 507 if first.Type != protobuf.MembershipUpdateEvent_CHAT_CREATED { 508 return "", fmt.Errorf("expected first event to be 'chat-created', got %s", first.Type) 509 } 510 return first.From, nil 511 } 512 513 func (g Group) isCreator(id string) (bool, error) { 514 c, err := g.Creator() 515 if err != nil { 516 return false, err 517 } 518 519 return id == c, nil 520 } 521 522 func (g Group) validateChatID(chatID string) bool { 523 creator, err := g.Creator() 524 if err != nil || creator == "" { 525 return false 526 } 527 // TODO: It does not verify that the prefix is a valid UUID. 528 // Improve it so that the prefix follows UUIDv4 spec. 529 return strings.HasSuffix(chatID, creator) && chatID != creator 530 } 531 532 func (g Group) IsMember(id string) bool { 533 return g.members.Has(id) 534 } 535 536 func (g Group) WasEverMember(id string) (bool, error) { 537 isCreator, err := g.isCreator(id) 538 if err != nil { 539 return false, err 540 } 541 542 if isCreator { 543 return true, nil 544 } 545 546 for _, event := range g.events { 547 if event.Type == protobuf.MembershipUpdateEvent_MEMBERS_ADDED { 548 for _, member := range event.Members { 549 if member == id { 550 return true, nil 551 } 552 } 553 } 554 } 555 return false, nil 556 } 557 558 // validateEvent returns true if a given event is valid. 559 func (g Group) validateEvent(event MembershipUpdateEvent) bool { 560 if len(event.From) == 0 { 561 return false 562 } 563 switch event.Type { 564 case protobuf.MembershipUpdateEvent_CHAT_CREATED: 565 return g.admins.Empty() && g.members.Empty() 566 case protobuf.MembershipUpdateEvent_NAME_CHANGED: 567 return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Name) > 0 568 case protobuf.MembershipUpdateEvent_COLOR_CHANGED: 569 return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Color) > 0 570 case protobuf.MembershipUpdateEvent_IMAGE_CHANGED: 571 return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Image) > 0 572 case protobuf.MembershipUpdateEvent_MEMBERS_ADDED: 573 return g.admins.Has(event.From) || g.members.Has(event.From) 574 case protobuf.MembershipUpdateEvent_MEMBER_JOINED: 575 return g.members.Has(event.From) 576 case protobuf.MembershipUpdateEvent_MEMBER_REMOVED: 577 // Member can remove themselves or admin can remove a member. 578 return len(event.Members) == 1 && (event.From == event.Members[0] || (g.admins.Has(event.From) && !g.admins.Has(event.Members[0]))) 579 case protobuf.MembershipUpdateEvent_ADMINS_ADDED: 580 return g.admins.Has(event.From) && stringSliceSubset(event.Members, g.members.List()) 581 case protobuf.MembershipUpdateEvent_ADMIN_REMOVED: 582 return len(event.Members) == 1 && g.admins.Has(event.From) && event.From == event.Members[0] 583 default: 584 return false 585 } 586 } 587 588 func (g *Group) processEvent(event MembershipUpdateEvent) { 589 switch event.Type { 590 case protobuf.MembershipUpdateEvent_CHAT_CREATED: 591 g.name = event.Name 592 g.color = event.Color 593 g.members.Add(event.From) 594 g.admins.Add(event.From) 595 case protobuf.MembershipUpdateEvent_NAME_CHANGED: 596 g.name = event.Name 597 case protobuf.MembershipUpdateEvent_COLOR_CHANGED: 598 g.color = event.Color 599 case protobuf.MembershipUpdateEvent_IMAGE_CHANGED: 600 g.image = event.Image 601 case protobuf.MembershipUpdateEvent_ADMINS_ADDED: 602 g.admins.Add(event.Members...) 603 case protobuf.MembershipUpdateEvent_ADMIN_REMOVED: 604 g.admins.Remove(event.Members[0]) 605 case protobuf.MembershipUpdateEvent_MEMBERS_ADDED: 606 g.members.Add(event.Members...) 607 case protobuf.MembershipUpdateEvent_MEMBER_REMOVED: 608 g.admins.Remove(event.Members[0]) 609 g.members.Remove(event.Members[0]) 610 } 611 } 612 613 func (g *Group) sortEvents() { 614 sort.Slice(g.events, func(i, j int) bool { 615 return g.events[i].ClockValue < g.events[j].ClockValue 616 }) 617 } 618 619 func stringSliceSubset(subset []string, set []string) bool { 620 for _, item1 := range set { 621 var found bool 622 for _, item2 := range subset { 623 if item1 == item2 { 624 found = true 625 break 626 } 627 } 628 if found { 629 return true 630 } 631 } 632 return false 633 } 634 635 func publicKeyToString(publicKey *ecdsa.PublicKey) string { 636 return types.EncodeHex(crypto.FromECDSAPub(publicKey)) 637 } 638 639 type stringSet struct { 640 m map[string]struct{} 641 items []string 642 } 643 644 func newStringSet() *stringSet { 645 return &stringSet{ 646 m: make(map[string]struct{}), 647 } 648 } 649 650 func newStringSetFromSlice(s []string) *stringSet { 651 set := newStringSet() 652 if len(s) > 0 { 653 set.Add(s...) 654 } 655 return set 656 } 657 658 func (s *stringSet) Add(items ...string) { 659 for _, item := range items { 660 if _, ok := s.m[item]; !ok { 661 s.m[item] = struct{}{} 662 s.items = append(s.items, item) 663 } 664 } 665 } 666 667 func (s *stringSet) Remove(items ...string) { 668 for _, item := range items { 669 if _, ok := s.m[item]; ok { 670 delete(s.m, item) 671 s.removeFromItems(item) 672 } 673 } 674 } 675 676 func (s *stringSet) Has(item string) bool { 677 _, ok := s.m[item] 678 return ok 679 } 680 681 func (s *stringSet) Empty() bool { 682 return len(s.items) == 0 683 } 684 685 func (s *stringSet) List() []string { 686 return s.items 687 } 688 689 func (s *stringSet) removeFromItems(dropped string) { 690 n := 0 691 for _, item := range s.items { 692 if item != dropped { 693 s.items[n] = item 694 n++ 695 } 696 } 697 s.items = s.items[:n] 698 }