Implement group messages logic (#86)

This commit is contained in:
Adam Babik 2019-10-13 20:16:43 +02:00 committed by GitHub
parent 17cb8c53c3
commit 1859f6c80c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1216 additions and 161 deletions

99
chat.go
View File

@ -2,10 +2,13 @@ package statusproto
import (
"crypto/ecdsa"
"crypto/sha1"
"encoding/hex"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
statusproto "github.com/status-im/status-protocol-go/types"
protocol "github.com/status-im/status-protocol-go/v1"
)
type ChatType int
@ -52,6 +55,55 @@ type Chat struct {
MembershipUpdates []ChatMembershipUpdate `json:"membershipUpdates"`
}
func (c *Chat) MembersAsPublicKeys() ([]*ecdsa.PublicKey, error) {
publicKeys := make([]string, len(c.Members))
for idx, item := range c.Members {
publicKeys[idx] = item.ID
}
return stringSliceToPublicKeys(publicKeys, true)
}
func (c *Chat) updateChatFromProtocolGroup(g *protocol.Group) {
// ID
c.ID = g.ChatID()
// Name
c.Name = g.Name()
// Members
members := g.Members()
admins := g.Admins()
joined := g.Joined()
chatMembers := make([]ChatMember, 0, len(members))
for _, m := range members {
chatMember := ChatMember{
ID: m,
}
chatMember.Admin = stringSliceContains(admins, m)
chatMember.Joined = stringSliceContains(joined, m)
chatMembers = append(chatMembers, chatMember)
}
c.Members = chatMembers
// MembershipUpdates
updates := g.Updates()
membershipUpdates := make([]ChatMembershipUpdate, 0, len(updates))
for _, update := range updates {
membershipUpdate := ChatMembershipUpdate{
Type: update.Type,
Name: update.Name,
ClockValue: uint64(update.ClockValue), // TODO: get rid of type casting
Signature: update.Signature,
From: update.From,
Member: update.Member,
Members: update.Members,
}
membershipUpdate.setID()
membershipUpdates = append(membershipUpdates, membershipUpdate)
}
c.MembershipUpdates = membershipUpdates
}
// ChatMembershipUpdate represent an event on membership of the chat
type ChatMembershipUpdate struct {
// Unique identifier for the event
@ -72,6 +124,11 @@ type ChatMembershipUpdate struct {
Members []string `json:"members,omitempty"`
}
func (u *ChatMembershipUpdate) setID() {
sum := sha1.Sum([]byte(u.Signature))
u.ID = hex.EncodeToString(sum[:])
}
// ChatMember represents a member who participates in a group chat
type ChatMember struct {
// ID is the hex encoded public key of the member
@ -113,14 +170,8 @@ func CreatePublicChat(name string) Chat {
}
}
func groupChatID(creator *ecdsa.PublicKey) string {
return uuid.New().String() + statusproto.EncodeHex(crypto.FromECDSAPub(creator))
}
func CreateGroupChat(name string, creator *ecdsa.PublicKey) Chat {
func createGroupChat() Chat {
return Chat{
ID: groupChatID(creator),
Name: name,
Active: true,
ChatType: ChatTypePrivateGroupChat,
}
@ -134,3 +185,35 @@ func findChatByID(chatID string, chats []*Chat) *Chat {
}
return nil
}
func stringSliceToPublicKeys(slice []string, prefixed bool) ([]*ecdsa.PublicKey, error) {
result := make([]*ecdsa.PublicKey, len(slice))
for idx, item := range slice {
var (
b []byte
err error
)
if prefixed {
b, err = hexutil.Decode(item)
} else {
b, err = hex.DecodeString(item)
}
if err != nil {
return nil, err
}
result[idx], err = crypto.UnmarshalPubkey(b)
if err != nil {
return nil, err
}
}
return result, nil
}
func stringSliceContains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}

28
chat_group_proxy.go Normal file
View File

@ -0,0 +1,28 @@
package statusproto
import (
protocol "github.com/status-im/status-protocol-go/v1"
)
func newProtocolGroupFromChat(chat *Chat) (*protocol.Group, error) {
return protocol.NewGroup(chat.ID, chatToFlattenMembershipUpdate(chat))
}
func chatToFlattenMembershipUpdate(chat *Chat) []protocol.MembershipUpdateFlat {
result := make([]protocol.MembershipUpdateFlat, len(chat.MembershipUpdates))
for idx, update := range chat.MembershipUpdates {
result[idx] = protocol.MembershipUpdateFlat{
ChatID: chat.ID,
From: update.From,
Signature: update.Signature,
MembershipUpdateEvent: protocol.MembershipUpdateEvent{
Name: update.Name,
Type: update.Type,
ClockValue: int64(update.ClockValue), // TODO: remove type difference
Member: update.Member,
Members: update.Members,
},
}
}
return result
}

View File

@ -78,6 +78,7 @@ func VerifySignatures(signaturePairs [][3]string) error {
}
// ExtractSignatures extract from tuples of signatures content a public key
// DEPRECATED: use ExtractSignature
func ExtractSignatures(signaturePairs [][2]string) ([]string, error) {
response := make([]string, len(signaturePairs))
for i, signaturePair := range signaturePairs {
@ -102,6 +103,12 @@ func ExtractSignatures(signaturePairs [][2]string) ([]string, error) {
return response, nil
}
// ExtractSignature returns a public key for a given data and signature.
func ExtractSignature(data, signature []byte) (*ecdsa.PublicKey, error) {
dataHash := crypto.Keccak256(data)
return crypto.SigToPub(dataHash, signature)
}
func EncryptSymmetric(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {

68
message_handler.go Normal file
View File

@ -0,0 +1,68 @@
package statusproto
import (
"github.com/pkg/errors"
protocol "github.com/status-im/status-protocol-go/v1"
)
type persistentMessageHandler struct {
persistence *sqlitePersistence
}
func newPersistentMessageHandler(persistence *sqlitePersistence) *persistentMessageHandler {
return &persistentMessageHandler{persistence: persistence}
}
// HandleMembershipUpdate updates a Chat instance according to the membership updates.
// It retrieves chat, if exists, and merges membership updates from the message.
// Finally, the Chat is updated with the new group events.
func (h *persistentMessageHandler) HandleMembershipUpdate(m protocol.MembershipUpdateMessage) error {
chat, err := h.chatID(m.ChatID)
switch err {
case errChatNotFound:
group, err := protocol.NewGroupWithMembershipUpdates(m.ChatID, m.Updates)
if err != nil {
return err
}
newChat := createGroupChat()
newChat.updateChatFromProtocolGroup(group)
chat = &newChat
case nil:
existingGroup, err := newProtocolGroupFromChat(chat)
if err != nil {
return errors.Wrap(err, "failed to create a Group from Chat")
}
updateGroup, err := protocol.NewGroupWithMembershipUpdates(m.ChatID, m.Updates)
if err != nil {
return errors.Wrap(err, "invalid membership update")
}
merged := protocol.MergeFlatMembershipUpdates(existingGroup.Updates(), updateGroup.Updates())
newGroup, err := protocol.NewGroup(chat.ID, merged)
if err != nil {
return errors.Wrap(err, "failed to create a group with new membership updates")
}
chat.updateChatFromProtocolGroup(newGroup)
default:
return err
}
return h.persistence.SaveChat(*chat)
}
func (h *persistentMessageHandler) chatID(chatID string) (*Chat, error) {
var chat *Chat
chats, err := h.persistence.Chats()
if err != nil {
return nil, err
}
for _, ch := range chats {
if chat.ID == chatID {
chat = ch
break
}
}
if chat == nil {
return nil, errChatNotFound
}
return chat, nil
}

View File

@ -28,11 +28,16 @@ const (
whisperPoWTime = 5
)
type messageHandler interface {
HandleMembershipUpdate(m protocol.MembershipUpdateMessage) error
}
type messageProcessor struct {
identity *ecdsa.PrivateKey
datasync *datasync.DataSync
protocol *encryption.Protocol
transport *transport.WhisperServiceTransport
handler messageHandler
logger *zap.Logger
featureFlags featureFlags
@ -43,6 +48,7 @@ func newMessageProcessor(
database *sql.DB,
enc *encryption.Protocol,
transport *transport.WhisperServiceTransport,
handler messageHandler,
logger *zap.Logger,
features featureFlags,
) (*messageProcessor, error) {
@ -65,6 +71,7 @@ func newMessageProcessor(
datasync: ds,
protocol: enc,
transport: transport,
handler: handler,
logger: logger,
featureFlags: features,
}
@ -87,14 +94,14 @@ func (p *messageProcessor) Stop() {
func (p *messageProcessor) SendPrivate(
ctx context.Context,
publicKey *ecdsa.PublicKey,
recipient *ecdsa.PublicKey,
chatID string,
data []byte,
clock int64,
) ([]byte, *protocol.Message, error) {
p.logger.Debug(
"sending a private message",
zap.Binary("public-key", crypto.FromECDSAPub(publicKey)),
zap.Binary("public-key", crypto.FromECDSAPub(recipient)),
)
message := protocol.CreatePrivateTextMessage(data, clock, chatID)
@ -103,7 +110,7 @@ func (p *messageProcessor) SendPrivate(
return nil, nil, errors.Wrap(err, "failed to encode message")
}
messageID, err := p.sendPrivate(ctx, publicKey, encodedMessage)
messageID, err := p.sendPrivate(ctx, recipient, encodedMessage)
if err != nil {
return nil, nil, err
}
@ -113,23 +120,25 @@ func (p *messageProcessor) SendPrivate(
// SendPrivateRaw takes encoded data, encrypts it and sends through the wire.
func (p *messageProcessor) SendPrivateRaw(
ctx context.Context,
publicKey *ecdsa.PublicKey,
recipient *ecdsa.PublicKey,
data []byte,
) ([]byte, error) {
p.logger.Debug(
"sending a private message",
zap.Binary("public-key", crypto.FromECDSAPub(publicKey)),
zap.Binary("public-key", crypto.FromECDSAPub(recipient)),
zap.String("site", "SendPrivateRaw"),
)
return p.sendPrivate(ctx, publicKey, data)
return p.sendPrivate(ctx, recipient, data)
}
// sendPrivate sends data to the recipient identifying with a given public key.
func (p *messageProcessor) sendPrivate(
ctx context.Context,
publicKey *ecdsa.PublicKey,
recipient *ecdsa.PublicKey,
data []byte,
) ([]byte, error) {
p.logger.Debug("sending private message", zap.Binary("recipient", crypto.FromECDSAPub(recipient)))
wrappedMessage, err := p.tryWrapMessageV1(data)
if err != nil {
return nil, errors.Wrap(err, "failed to wrap message")
@ -138,19 +147,19 @@ func (p *messageProcessor) sendPrivate(
messageID := protocol.MessageID(&p.identity.PublicKey, wrappedMessage)
if p.featureFlags.datasync {
if err := p.addToDataSync(publicKey, wrappedMessage); err != nil {
if err := p.addToDataSync(recipient, wrappedMessage); err != nil {
return nil, errors.Wrap(err, "failed to send message with datasync")
}
// No need to call transport tracking.
// It is done in a data sync dispatch step.
} else {
messageSpec, err := p.protocol.BuildDirectMessage(p.identity, publicKey, wrappedMessage)
messageSpec, err := p.protocol.BuildDirectMessage(p.identity, recipient, wrappedMessage)
if err != nil {
return nil, errors.Wrap(err, "failed to encrypt message")
}
hash, newMessage, err := p.sendMessageSpec(ctx, publicKey, messageSpec)
hash, newMessage, err := p.sendMessageSpec(ctx, recipient, messageSpec)
if err != nil {
return nil, errors.Wrap(err, "failed to send a message spec")
}
@ -161,6 +170,61 @@ func (p *messageProcessor) sendPrivate(
return messageID, nil
}
func (p *messageProcessor) SendGroup(
ctx context.Context,
recipients []*ecdsa.PublicKey,
chatID string,
data []byte,
clock int64,
) ([][]byte, []*protocol.Message, error) {
p.logger.Debug("sending a group message", zap.Int("membersCount", len(recipients)))
message := protocol.CreatePrivateGroupTextMessage(data, clock, chatID)
encodedMessage, err := p.encodeMessage(message)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to encode message")
}
var resultIDs [][]byte
for _, recipient := range recipients {
messageID, err := p.sendPrivate(ctx, recipient, encodedMessage)
if err != nil {
return nil, nil, err
}
resultIDs = append(resultIDs, messageID)
}
return resultIDs, nil, nil
}
func (p *messageProcessor) SendMembershipUpdate(
ctx context.Context,
recipients []*ecdsa.PublicKey,
chatID string,
updates []protocol.MembershipUpdate,
clock int64,
) ([][]byte, error) {
p.logger.Debug("sending a membership update", zap.Int("membersCount", len(recipients)))
message := protocol.MembershipUpdateMessage{
ChatID: chatID,
Updates: updates,
}
encodedMessage, err := protocol.EncodeMembershipUpdateMessage(message)
if err != nil {
return nil, errors.Wrap(err, "failed to encode membership update message")
}
var resultIDs [][]byte
for _, recipient := range recipients {
messageID, err := p.sendPrivate(ctx, recipient, encodedMessage)
if err != nil {
return nil, err
}
resultIDs = append(resultIDs, messageID)
}
return resultIDs, nil
}
func (p *messageProcessor) SendPublic(ctx context.Context, chatID string, data []byte, clock int64) ([]byte, error) {
logger := p.logger.With(zap.String("site", "SendPublic"))
logger.Debug("sending a public message", zap.String("chatID", chatID))
@ -249,6 +313,18 @@ func (p *messageProcessor) Process(shhMessage *whispertypes.Message) ([]*protoco
m.ID = statusMessage.ID
m.SigPubKey = statusMessage.SigPubKey()
decodedMessages = append(decodedMessages, &m)
case protocol.MembershipUpdateMessage:
// Handle user message that can be attached to the membership update.
userMessage := m.Message
if userMessage != nil {
userMessage.ID = statusMessage.ID
userMessage.SigPubKey = statusMessage.SigPubKey()
decodedMessages = append(decodedMessages, userMessage)
}
if err := p.processMembershipUpdate(m); err != nil {
hlogger.Error("failed to process MembershipUpdateMessage", zap.Error(err))
}
case protocol.PairMessage:
fromOurDevice := isPubKeyEqual(statusMessage.SigPubKey(), &p.identity.PublicKey)
if !fromOurDevice {
@ -267,6 +343,16 @@ func (p *messageProcessor) Process(shhMessage *whispertypes.Message) ([]*protoco
return decodedMessages, nil
}
func (p *messageProcessor) processMembershipUpdate(m protocol.MembershipUpdateMessage) error {
if err := m.Verify(); err != nil {
return err
}
if p.handler != nil {
return p.handler.HandleMembershipUpdate(m)
}
return errors.New("missing handler")
}
func (p *messageProcessor) processPairMessage(m protocol.PairMessage) error {
metadata := &multidevice.InstallationMetadata{
Name: m.Name,

View File

@ -98,6 +98,7 @@ func (s *MessageProcessorSuite) SetupTest() {
database,
encryptionProtocol,
whisperTransport,
nil,
s.logger,
featureFlags{},
)

View File

@ -9,6 +9,8 @@ import (
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pkg/errors"
"go.uber.org/zap"
@ -284,6 +286,7 @@ func NewMessenger(
database,
encryptionProtocol,
t,
newPersistentMessageHandler(&sqlitePersistence{db: database}),
logger,
c.featureFlags,
)
@ -454,12 +457,20 @@ func (m *Messenger) Mailservers() ([]string, error) {
}
func (m *Messenger) Join(chat Chat) error {
if chat.PublicKey != nil {
switch chat.ChatType {
case ChatTypeOneToOne:
return m.transport.JoinPrivate(chat.PublicKey)
} else if chat.Name != "" {
case ChatTypePrivateGroupChat:
members, err := chat.MembersAsPublicKeys()
if err != nil {
return err
}
return m.transport.JoinGroup(members)
case ChatTypePublic:
return m.transport.JoinPublic(chat.Name)
default:
return errors.New("chat is neither public nor private")
}
return errors.New("chat is neither public nor private")
}
func (m *Messenger) Leave(chat Chat) error {
@ -471,6 +482,94 @@ func (m *Messenger) Leave(chat Chat) error {
return errors.New("chat is neither public nor private")
}
// TODO: consider moving to a ChatManager ???
func (m *Messenger) CreateGroupChat(name string) (*Chat, error) {
chat := createGroupChat()
group, err := protocol.NewGroupWithCreator(name, m.identity)
if err != nil {
return nil, err
}
chat.updateChatFromProtocolGroup(group)
return &chat, nil
}
func (m *Messenger) AddMembersToChat(ctx context.Context, chat *Chat, members []*ecdsa.PublicKey) error {
group, err := newProtocolGroupFromChat(chat)
if err != nil {
return err
}
encodedMembers := make([]string, len(members))
for idx, member := range members {
encodedMembers[idx] = hexutil.Encode(crypto.FromECDSAPub(member))
}
event := protocol.NewMembersAddedEvent(encodedMembers, group.NextClockValue())
err = group.ProcessEvent(&m.identity.PublicKey, event)
if err != nil {
return err
}
if err := m.propagateMembershipUpdates(ctx, group); err != nil {
return err
}
chat.updateChatFromProtocolGroup(group)
return m.SaveChat(*chat)
}
func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chat *Chat) error {
group, err := newProtocolGroupFromChat(chat)
if err != nil {
return err
}
event := protocol.NewMemberJoinedEvent(
statusproto.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
group.NextClockValue(),
)
err = group.ProcessEvent(&m.identity.PublicKey, event)
if err != nil {
return err
}
if err := m.propagateMembershipUpdates(ctx, group); err != nil {
return err
}
chat.updateChatFromProtocolGroup(group)
return m.SaveChat(*chat)
}
func (m *Messenger) propagateMembershipUpdates(ctx context.Context, group *protocol.Group) error {
events := make([]protocol.MembershipUpdateEvent, len(group.Updates()))
for idx, event := range group.Updates() {
events[idx] = event.MembershipUpdateEvent
}
update := protocol.MembershipUpdate{
ChatID: group.ChatID(),
Events: events,
}
if err := update.Sign(m.identity); err != nil {
return err
}
recipients, err := stringSliceToPublicKeys(group.Members(), true)
if err != nil {
return err
}
// Filter out my key from the recipients
n := 0
for _, recipient := range recipients {
if !isPubKeyEqual(recipient, &m.identity.PublicKey) {
recipients[n] = recipient
n++
}
}
recipients = recipients[:n]
// Finally send membership updates to all recipients.
_, err = m.processor.SendMembershipUpdate(
ctx,
recipients,
group.ChatID(),
[]protocol.MembershipUpdate{update},
group.NextClockValue(),
)
return err
}
func (m *Messenger) SaveChat(chat Chat) error {
return m.persistence.SaveChat(chat)
}
@ -522,7 +621,7 @@ func (m *Messenger) Contacts() ([]*Contact, error) {
return m.persistence.Contacts()
}
func (m *Messenger) Send(ctx context.Context, chatID string, data []byte) ([]byte, error) {
func (m *Messenger) Send(ctx context.Context, chatID string, data []byte) ([][]byte, error) {
logger := m.logger.With(zap.String("site", "Send"), zap.String("chatID", chatID))
// A valid added chat is required.
@ -538,7 +637,8 @@ func (m *Messenger) Send(ctx context.Context, chatID string, data []byte) ([]byt
logger.Debug("last message clock received", zap.Int64("clock", clock))
if chat.PublicKey != nil {
switch chat.ChatType {
case ChatTypeOneToOne:
logger.Debug("sending private message", zap.Binary("publicKey", crypto.FromECDSAPub(chat.PublicKey)))
id, message, err := m.processor.SendPrivate(ctx, chat.PublicKey, chat.ID, data, clock)
@ -546,27 +646,64 @@ func (m *Messenger) Send(ctx context.Context, chatID string, data []byte) ([]byt
return nil, err
}
// Save our message because it won't be received from the transport layer.
message.ID = id // a Message need ID to be properly stored in the db
message.SigPubKey = &m.identity.PublicKey
message.ChatID = chatID
if err := m.cacheOwnMessage(chatID, id, message); err != nil {
return nil, err
}
if m.messagesPersistenceEnabled {
_, err = m.persistence.SaveMessages([]*protocol.Message{message})
if err != nil {
return [][]byte{id}, nil
case ChatTypePublic:
logger.Debug("sending public message", zap.String("chatName", chat.Name))
id, err := m.processor.SendPublic(ctx, chat.ID, data, clock)
if err != nil {
return nil, err
}
return [][]byte{id}, nil
case ChatTypePrivateGroupChat:
logger.Debug("sending group message", zap.String("chatName", chat.Name))
recipients, err := chat.MembersAsPublicKeys()
if err != nil {
return nil, err
}
// Filter me out of recipients.
n := 0
for _, item := range recipients {
if !isPubKeyEqual(item, &m.identity.PublicKey) {
recipients[n] = item
n++
}
}
ids, messages, err := m.processor.SendGroup(ctx, recipients[:n], chat.ID, data, clock)
if err != nil {
return nil, err
}
for idx, message := range messages {
if err := m.cacheOwnMessage(chatID, ids[idx], message); err != nil {
return nil, err
}
}
// Cache it to be returned in Retrieve().
m.ownMessages = append(m.ownMessages, message)
return id, nil
} else if chat.Name != "" {
logger.Debug("sending public message", zap.String("chatName", chat.Name))
return m.processor.SendPublic(ctx, chat.ID, data, clock)
return ids, nil
default:
return nil, errors.New("chat is neither public nor private")
}
return nil, errors.New("chat is neither public nor private")
}
func (m *Messenger) cacheOwnMessage(chatID string, id []byte, message *protocol.Message) error {
// Save our message because it won't be received from the transport layer.
message.ID = id // a Message need ID to be properly stored in the db
message.SigPubKey = &m.identity.PublicKey
message.ChatID = chatID
if m.messagesPersistenceEnabled {
_, err := m.persistence.SaveMessages([]*protocol.Message{message})
if err != nil {
return err
}
}
// Cache it to be returned in Retrieve().
m.ownMessages = append(m.ownMessages, message)
return nil
}
// SendRaw takes encoded data, encrypts it and sends through the wire.

View File

@ -309,7 +309,7 @@ func (s *MessengerSuite) TestRetrieveOwnPrivate() {
// Verify message fields.
message := messages[0]
s.Equal(messageID, message.ID)
s.Equal(messageID[0], message.ID)
s.Equal(&s.privateKey.PublicKey, message.SigPubKey) // this is OUR message
}
@ -337,7 +337,7 @@ func (s *MessengerSuite) TestRetrieveTheirPrivate() {
// Validate received message.
s.Require().Len(messages, 1)
message := messages[0]
s.Equal(messageID, message.ID)
s.Equal(messageID[0], message.ID)
s.Equal(&theirMessenger.identity.PublicKey, message.SigPubKey)
}
@ -793,6 +793,102 @@ func (s *MessengerSuite) TestSharedSecretHandler() {
s.NoError(err)
}
func (s *MessengerSuite) TestCreateGroupChat() {
chat, err := s.m.CreateGroupChat("test")
s.NoError(err)
s.Equal("test", chat.Name)
publicKeyHex := "0x" + hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey))
s.Contains(chat.ID, publicKeyHex)
s.EqualValues([]string{publicKeyHex}, []string{chat.Members[0].ID})
}
func (s *MessengerSuite) TestAddMembersToChat() {
chat, err := s.m.CreateGroupChat("test")
s.NoError(err)
key, err := crypto.GenerateKey()
s.NoError(err)
err = s.m.AddMembersToChat(context.Background(), chat, []*ecdsa.PublicKey{&key.PublicKey})
s.NoError(err)
publicKeyHex := "0x" + hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey))
keyHex := "0x" + hex.EncodeToString(crypto.FromECDSAPub(&key.PublicKey))
s.EqualValues([]string{publicKeyHex, keyHex}, []string{chat.Members[0].ID, chat.Members[1].ID})
}
// TestGroupChatAutocreate verifies that after receiving a membership update message
// for non-existing group chat, a new one is created.
func (s *MessengerSuite) TestGroupChatAutocreate() {
theirMessenger := s.newMessenger()
chat, err := theirMessenger.CreateGroupChat("test-group")
s.NoError(err)
err = theirMessenger.SaveChat(*chat)
s.NoError(err)
err = theirMessenger.AddMembersToChat(
context.Background(),
chat,
[]*ecdsa.PublicKey{&s.privateKey.PublicKey},
)
s.NoError(err)
s.Equal(2, len(chat.Members))
var chats []*Chat
err = tt.RetryWithBackOff(func() error {
_, err := s.m.RetrieveAll(context.Background(), RetrieveLatest)
if err != nil {
return err
}
chats, err = s.m.Chats()
if err != nil {
return err
}
if len(chats) == 0 {
return errors.New("expected a group chat to be created")
}
return nil
})
s.NoError(err)
s.Equal(chat.ID, chats[0].ID)
s.Equal("test-group", chats[0].Name)
s.Equal(2, len(chats[0].Members))
// Send confirmation.
err = s.m.ConfirmJoiningGroup(context.Background(), chats[0])
s.Require().NoError(err)
}
func (s *MessengerSuite) TestGroupChatMessages() {
theirMessenger := s.newMessenger()
chat, err := theirMessenger.CreateGroupChat("test-group")
s.NoError(err)
err = theirMessenger.SaveChat(*chat)
s.NoError(err)
err = theirMessenger.AddMembersToChat(
context.Background(),
chat,
[]*ecdsa.PublicKey{&s.privateKey.PublicKey},
)
s.NoError(err)
_, err = theirMessenger.Send(context.Background(), chat.ID, []byte("hello!"))
s.NoError(err)
var messages []*protocol.Message
err = tt.RetryWithBackOff(func() error {
var err error
messages, err = s.m.RetrieveAll(context.Background(), RetrieveLatest)
if err == nil && len(messages) == 0 {
err = errors.New("no messages")
}
return err
})
s.NoError(err)
// Validate received message.
s.Require().Len(messages, 1)
message := messages[0]
s.Equal(&theirMessenger.identity.PublicKey, message.SigPubKey)
}
type PostProcessorSuite struct {
suite.Suite

View File

@ -81,6 +81,9 @@ type WhisperServiceTransport struct {
}
// NewWhisperServiceTransport returns a new WhisperServiceTransport.
// TODO: leaving a chat should verify that for a given public key
// there are no other chats. It may happen that we leave a private chat
// but still have a public chat for a given public key.
func NewWhisperServiceTransport(
shh whispertypes.Whisper,
privateKey *ecdsa.PrivateKey,
@ -185,6 +188,30 @@ func (a *WhisperServiceTransport) LeavePrivate(publicKey *ecdsa.PublicKey) error
return a.filters.Remove(filters...)
}
func (a *WhisperServiceTransport) JoinGroup(publicKeys []*ecdsa.PublicKey) error {
_, err := a.filters.LoadDiscovery()
if err != nil {
return err
}
for _, pk := range publicKeys {
_, err = a.filters.LoadContactCode(pk)
if err != nil {
return err
}
}
return nil
}
func (a *WhisperServiceTransport) LeaveGroup(publicKeys []*ecdsa.PublicKey) error {
for _, publicKey := range publicKeys {
filters := a.filters.FiltersByPublicKey(publicKey)
if err := a.filters.Remove(filters...); err != nil {
return err
}
}
return nil
}
type Message struct {
Message *whispertypes.Message
Public bool

View File

@ -163,10 +163,6 @@ func membershipUpdateMessageHandler(d transit.Decoder, value interface{}) (inter
if !ok {
break
}
update.From, ok = value[transit.Keyword("from")].(string)
if !ok {
break
}
update.Signature, ok = value[transit.Keyword("signature")].(string)
if !ok {
break

View File

@ -77,7 +77,6 @@ func (messageValueEncoder) Encode(e transit.Encoder, value reflect.Value, asStri
element := map[interface{}]interface{}{
transit.Keyword("chat-id"): update.ChatID,
transit.Keyword("from"): update.From,
transit.Keyword("events"): events,
transit.Keyword("signature"): update.Signature,
}

View File

@ -3,10 +3,18 @@ package statusproto
import (
"bytes"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
gethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/status-im/status-protocol-go/crypto"
)
@ -29,11 +37,35 @@ type MembershipUpdateMessage struct {
Message *Message `json:"message"` // optional message
}
// Verify makes sure that the received update message has a valid signature.
// It also extracts public key from the signature available as From field.
// It does not verify the updates and their events. This should be done
// separately using Group struct.
func (m *MembershipUpdateMessage) Verify() error {
for idx, update := range m.Updates {
if err := update.extractFrom(); err != nil {
return errors.Wrapf(err, "failed to extract an author of %d update", idx)
}
m.Updates[idx] = update
}
return nil
}
// EncodeMembershipUpdateMessage encodes a MembershipUpdateMessage using Transit serialization.
func EncodeMembershipUpdateMessage(value MembershipUpdateMessage) ([]byte, error) {
var buf bytes.Buffer
encoder := NewMessageEncoder(&buf)
if err := encoder.Encode(value); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
type MembershipUpdate struct {
ChatID string `json:"chatId"`
From string `json:"from"`
Signature string `json:"signature"`
Signature string `json:"signature"` // hex-encoded without 0x prefix
Events []MembershipUpdateEvent `json:"events"`
From string `json:"from"` // hex-encoded with 0x prefix
}
// Sign creates a signature from MembershipUpdateEvents
@ -41,22 +73,7 @@ type MembershipUpdate struct {
// It follows the algorithm describe in the spec:
// https://github.com/status-im/specs/blob/master/status-group-chats-spec.md#signature.
func (u *MembershipUpdate) Sign(identity *ecdsa.PrivateKey) error {
sort.Slice(u.Events, func(i, j int) bool {
return u.Events[i].ClockValue < u.Events[j].ClockValue
})
tuples := make([]interface{}, len(u.Events))
for idx, event := range u.Events {
tuples[idx] = tupleMembershipUpdateEvent(event)
}
structureToSign := []interface{}{
tuples,
u.ChatID,
}
data, err := json.Marshal(structureToSign)
if err != nil {
return err
}
signature, err := crypto.SignBytesAsHex(data, identity)
signature, err := createMembershipUpdateSignature(u.ChatID, u.Events, identity)
if err != nil {
return err
}
@ -64,6 +81,38 @@ func (u *MembershipUpdate) Sign(identity *ecdsa.PrivateKey) error {
return nil
}
func (u *MembershipUpdate) extractFrom() error {
content, err := stringifyMembershipUpdateEvents(u.ChatID, u.Events)
if err != nil {
return errors.Wrap(err, "failed to stringify events")
}
signatureBytes, err := hex.DecodeString(u.Signature)
if err != nil {
return errors.Wrap(err, "failed to decode signature")
}
publicKey, err := crypto.ExtractSignature(content, signatureBytes)
if err != nil {
return errors.Wrap(err, "failed to extract signature")
}
u.From = hexutil.Encode(gethcrypto.FromECDSAPub(publicKey))
return nil
}
func (u *MembershipUpdate) Flat() []MembershipUpdateFlat {
result := make([]MembershipUpdateFlat, 0, len(u.Events))
for _, event := range u.Events {
result = append(result, MembershipUpdateFlat{
MembershipUpdateEvent: event,
ChatID: u.ChatID,
Signature: u.Signature,
From: u.From,
})
}
return result
}
// MembershipUpdateEvent contains an event information.
// Member and Members are hex-encoded values with 0x prefix.
type MembershipUpdateEvent struct {
Type string `json:"type"`
ClockValue int64 `json:"clockValue"`
@ -72,10 +121,49 @@ type MembershipUpdateEvent struct {
Name string `json:"name,omitempty"` // name of the group chat
}
func NewChatCreatedEvent(name string, clock int64) MembershipUpdateEvent {
func (u MembershipUpdateEvent) Equal(update MembershipUpdateEvent) bool {
return u.Type == update.Type &&
u.ClockValue == update.ClockValue &&
u.Member == update.Member &&
stringSliceEquals(u.Members, update.Members) &&
u.Name == update.Name
}
type MembershipUpdateFlat struct {
MembershipUpdateEvent
ChatID string `json:"chatId"`
Signature string `json:"signature"`
From string `json:"from"`
}
func (u MembershipUpdateFlat) Equal(update MembershipUpdateFlat) bool {
return u.ChatID == update.ChatID &&
u.Signature == update.Signature &&
u.From == update.From &&
u.MembershipUpdateEvent.Equal(update.MembershipUpdateEvent)
}
func MergeFlatMembershipUpdates(dest []MembershipUpdateFlat, src []MembershipUpdateFlat) []MembershipUpdateFlat {
for _, update := range src {
var exists bool
for _, existing := range dest {
if existing.Equal(update) {
exists = true
break
}
}
if !exists {
dest = append(dest, update)
}
}
return dest
}
func NewChatCreatedEvent(name string, admin string, clock int64) MembershipUpdateEvent {
return MembershipUpdateEvent{
Type: MembershipUpdateChatCreated,
Name: name,
Member: admin,
ClockValue: clock,
}
}
@ -128,14 +216,27 @@ func NewAdminRemovedEvent(admin string, clock int64) MembershipUpdateEvent {
}
}
// EncodeMembershipUpdateMessage encodes a MembershipUpdateMessage using Transit serialization.
func EncodeMembershipUpdateMessage(value MembershipUpdateMessage) ([]byte, error) {
var buf bytes.Buffer
encoder := NewMessageEncoder(&buf)
if err := encoder.Encode(value); err != nil {
return nil, err
func stringifyMembershipUpdateEvents(chatID string, events []MembershipUpdateEvent) ([]byte, error) {
sort.Slice(events, func(i, j int) bool {
return events[i].ClockValue < events[j].ClockValue
})
tuples := make([]interface{}, len(events))
for idx, event := range events {
tuples[idx] = tupleMembershipUpdateEvent(event)
}
return buf.Bytes(), nil
structureToSign := []interface{}{
tuples,
chatID,
}
return json.Marshal(structureToSign)
}
func createMembershipUpdateSignature(chatID string, events []MembershipUpdateEvent, identity *ecdsa.PrivateKey) (string, error) {
data, err := stringifyMembershipUpdateEvents(chatID, events)
if err != nil {
return "", err
}
return crypto.SignBytesAsHex(data, identity)
}
var membershipUpdateEventFieldNamesCompat = map[string]string{
@ -173,41 +274,222 @@ func tupleMembershipUpdateEvent(update MembershipUpdateEvent) [][]interface{} {
}
type Group struct {
ChatID string
Admins []string
Contacts []string
chatID string
name string
updates []MembershipUpdateFlat
admins *stringSet
members *stringSet
}
// ValidateEvent returns true if a given event is valid.
func (g *Group) ValidateEvent(from string, event MembershipUpdateEvent) bool {
func groupChatID(creator *ecdsa.PublicKey) string {
return uuid.New().String() + "-" + hexutil.Encode(gethcrypto.FromECDSAPub(creator))
}
func NewGroupWithMembershipUpdates(chatID string, updates []MembershipUpdate) (*Group, error) {
flatten := make([]MembershipUpdateFlat, 0, len(updates))
for _, update := range updates {
flatten = append(flatten, update.Flat()...)
}
return newGroup(chatID, flatten)
}
func NewGroupWithCreator(name string, creator *ecdsa.PrivateKey) (*Group, error) {
chatID := groupChatID(&creator.PublicKey)
creatorHex := publicKeyToString(&creator.PublicKey)
clock := TimestampInMsFromTime(time.Now())
chatCreated := NewChatCreatedEvent(name, creatorHex, int64(clock))
update := MembershipUpdate{
ChatID: chatID,
From: creatorHex,
Events: []MembershipUpdateEvent{chatCreated},
}
if err := update.Sign(creator); err != nil {
return nil, err
}
return newGroup(chatID, update.Flat())
}
func NewGroup(chatID string, updates []MembershipUpdateFlat) (*Group, error) {
return newGroup(chatID, updates)
}
func newGroup(chatID string, updates []MembershipUpdateFlat) (*Group, error) {
g := Group{
chatID: chatID,
updates: updates,
admins: newStringSet(),
members: newStringSet(),
}
if err := g.init(); err != nil {
return nil, err
}
return &g, nil
}
func (g *Group) init() error {
g.sortEvents()
var chatID string
for _, update := range g.updates {
if chatID == "" {
chatID = update.ChatID
} else if update.ChatID != chatID {
return errors.New("updates contain different chat IDs")
}
valid := g.validateEvent(update.From, update.MembershipUpdateEvent)
if !valid {
return fmt.Errorf("invalid event %#+v from %s", update.MembershipUpdateEvent, update.From)
}
g.processEvent(update.From, update.MembershipUpdateEvent)
}
valid := g.validateChatID(g.chatID)
if !valid {
return fmt.Errorf("invalid chat ID: %s", g.chatID)
}
if chatID != g.chatID {
return fmt.Errorf("expected chat ID equal %s, got %s", g.chatID, chatID)
}
return nil
}
func (g Group) ChatID() string {
return g.chatID
}
func (g Group) Updates() []MembershipUpdateFlat {
return g.updates
}
func (g Group) Name() string {
return g.name
}
func (g Group) Members() []string {
return g.members.List()
}
func (g Group) Admins() []string {
return g.admins.List()
}
func (g Group) Joined() []string {
var result []string
for _, update := range g.updates {
if update.Type == MembershipUpdateMemberJoined {
result = append(result, update.Member)
}
}
return result
}
func (g *Group) ProcessEvents(from *ecdsa.PublicKey, events []MembershipUpdateEvent) error {
for _, event := range events {
err := g.ProcessEvent(from, event)
if err != nil {
return err
}
}
return nil
}
func (g *Group) ProcessEvent(from *ecdsa.PublicKey, event MembershipUpdateEvent) error {
fromHex := hexutil.Encode(gethcrypto.FromECDSAPub(from))
if !g.validateEvent(fromHex, event) {
return fmt.Errorf("invalid event %#+v from %s", event, from)
}
update := MembershipUpdate{
ChatID: g.chatID,
From: fromHex,
Events: []MembershipUpdateEvent{event},
}
g.updates = append(g.updates, update.Flat()...)
g.processEvent(fromHex, event)
return nil
}
func (g Group) LastClockValue() int64 {
if len(g.updates) == 0 {
return 0
}
return g.updates[len(g.updates)-1].ClockValue
}
func (g Group) NextClockValue() int64 {
return g.LastClockValue() + 1
}
func (g Group) creator() (string, error) {
if len(g.updates) == 0 {
return "", errors.New("no events in the group")
}
first := g.updates[0]
if first.Type != MembershipUpdateChatCreated {
return "", fmt.Errorf("expected first event to be 'chat-created', got %s", first.Type)
}
return first.From, nil
}
func (g Group) validateChatID(chatID string) bool {
creator, err := g.creator()
if err != nil || creator == "" {
return false
}
// TODO: It does not verify that the prefix is a valid UUID.
// Improve it so that the prefix follows UUIDv4 spec.
return strings.HasSuffix(chatID, creator) && chatID != creator
}
// validateEvent returns true if a given event is valid.
func (g Group) validateEvent(from string, event MembershipUpdateEvent) bool {
switch event.Type {
case MembershipUpdateChatCreated:
return len(g.Admins) == 0 && len(g.Contacts) == 0
return g.admins.Empty() && g.members.Empty()
case MembershipUpdateNameChanged:
return stringSliceContains(g.Admins, from) && len(event.Name) > 0
return g.admins.Has(from) && len(event.Name) > 0
case MembershipUpdateMembersAdded:
return stringSliceContains(g.Admins, from)
return g.admins.Has(from)
case MembershipUpdateMemberJoined:
return stringSliceContains(g.Contacts, from) && from == event.Member
return g.members.Has(from) && from == event.Member
case MembershipUpdateMemberRemoved:
// Member can remove themselves or admin can remove a member.
return from == event.Member || (stringSliceContains(g.Admins, from) && !stringSliceContains(g.Admins, event.Member))
return from == event.Member || (g.admins.Has(from) && !g.admins.Has(event.Member))
case MembershipUpdateAdminsAdded:
return stringSliceContains(g.Admins, from) && stringSliceSubset(event.Members, g.Contacts)
return g.admins.Has(from) && stringSliceSubset(event.Members, g.members.List())
case MembershipUpdateAdminRemoved:
return stringSliceContains(g.Admins, from) && from == event.Member
return g.admins.Has(from) && from == event.Member
default:
return false
}
}
func stringSliceContains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
func (g *Group) processEvent(from string, event MembershipUpdateEvent) {
switch event.Type {
case MembershipUpdateChatCreated:
g.name = event.Name
g.members.Add(event.Member)
g.admins.Add(event.Member)
case MembershipUpdateNameChanged:
g.name = event.Name
case MembershipUpdateAdminsAdded:
g.admins.Add(event.Members...)
case MembershipUpdateAdminRemoved:
g.admins.Remove(event.Member)
case MembershipUpdateMembersAdded:
g.members.Add(event.Members...)
case MembershipUpdateMemberRemoved:
g.members.Remove(event.Member)
case MembershipUpdateMemberJoined:
g.members.Add(event.Member)
}
return false
}
func (g *Group) sortEvents() {
sort.Slice(g.updates, func(i, j int) bool {
return g.updates[i].ClockValue < g.updates[j].ClockValue
})
}
func stringSliceSubset(subset []string, set []string) bool {
@ -225,3 +507,82 @@ func stringSliceSubset(subset []string, set []string) bool {
}
return false
}
func stringSliceEquals(slice1, slice2 []string) bool {
set := map[string]struct{}{}
for _, s := range slice1 {
set[s] = struct{}{}
}
for _, s := range slice2 {
_, ok := set[s]
if !ok {
return false
}
}
return true
}
func publicKeyToString(publicKey *ecdsa.PublicKey) string {
return hexutil.Encode(gethcrypto.FromECDSAPub(publicKey))
}
type stringSet struct {
m map[string]struct{}
items []string
}
func newStringSet() *stringSet {
return &stringSet{
m: make(map[string]struct{}),
}
}
func newStringSetFromSlice(s []string) *stringSet {
set := newStringSet()
if len(s) > 0 {
set.Add(s...)
}
return set
}
func (s *stringSet) Add(items ...string) {
for _, item := range items {
if _, ok := s.m[item]; !ok {
s.m[item] = struct{}{}
s.items = append(s.items, item)
}
}
}
func (s *stringSet) Remove(items ...string) {
for _, item := range items {
if _, ok := s.m[item]; ok {
delete(s.m, item)
s.removeFromItems(item)
}
}
}
func (s *stringSet) Has(item string) bool {
_, ok := s.m[item]
return ok
}
func (s *stringSet) Empty() bool {
return len(s.items) == 0
}
func (s *stringSet) List() []string {
return s.items
}
func (s *stringSet) removeFromItems(dropped string) {
n := 0
for _, item := range s.items {
if item != dropped {
s.items[n] = item
n++
}
}
s.items = s.items[:n]
}

View File

@ -1,6 +1,7 @@
package statusproto
import (
"encoding/hex"
"strings"
"testing"
"unicode"
@ -12,22 +13,21 @@ import (
)
var (
testMembershipUpdateMessageBytes = []byte(`["~#g5",["072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",["~#list",[["^ ","~:chat-id","072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1","~:from","0x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1","~:events",[["^ ","~:type","chat-created","~:name","thathata","~:clock-value",156897373998501],["^ ","^5","members-added","^7",156897373998502,"~:members",["~#set",["0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"]]]],"~:signature","7fca3d614cf55bc6cdf9c17fd1e65d1688673322bf1f004c58c78e0927edefea3d1053bf6a9d2e058ae88079f588105dccf2a2f9f330f6035cd47c715ee5950601"]]],null]]`)
testMembershipUpdateMessageBytes = []byte(`["~#g5",["072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",["~#list",[["^ ","~:chat-id","072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1","~:events",[["^ ","~:type","chat-created","~:name","thathata","~:clock-value",156897373998501],["^ ","^4","members-added","^6",156897373998502,"~:members",["~#set",["0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"]]]],"~:signature","7fca3d614cf55bc6cdf9c17fd1e65d1688673322bf1f004c58c78e0927edefea3d1053bf6a9d2e058ae88079f588105dccf2a2f9f330f6035cd47c715ee5950601"]]],null]]`)
testMembershipUpdateMessageStruct = MembershipUpdateMessage{
ChatID: "072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
Updates: []MembershipUpdate{
{
ChatID: "072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
Signature: "7fca3d614cf55bc6cdf9c17fd1e65d1688673322bf1f004c58c78e0927edefea3d1053bf6a9d2e058ae88079f588105dccf2a2f9f330f6035cd47c715ee5950601",
From: "0x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
Events: []MembershipUpdateEvent{
{
Type: "chat-created",
Type: MembershipUpdateChatCreated,
Name: "thathata",
ClockValue: 156897373998501,
},
{
Type: "members-added",
Type: MembershipUpdateMembersAdded,
Members: []string{"0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"},
ClockValue: 156897373998502,
},
@ -107,7 +107,100 @@ func TestSignMembershipUpdate(t *testing.T) {
require.Equal(t, expected, update.Signature)
}
func TestGroupCreator(t *testing.T) {
key, err := crypto.GenerateKey()
require.NoError(t, err)
g, err := NewGroupWithCreator("abc", key)
require.NoError(t, err)
creator, err := g.creator()
require.NoError(t, err)
require.Equal(t, publicKeyToString(&key.PublicKey), creator)
}
func TestGroupProcessEvent(t *testing.T) {
createGroup := func(admins, members []string, name string) Group {
return Group{
name: name,
admins: newStringSetFromSlice(admins),
members: newStringSetFromSlice(members),
}
}
testCases := []struct {
Name string
Group Group
Result Group
From string
Event MembershipUpdateEvent
}{
{
Name: "chat-created event",
Group: createGroup(nil, nil, ""),
Result: createGroup([]string{"0xabc"}, []string{"0xabc"}, "some-name"),
From: "0xabc",
Event: NewChatCreatedEvent("some-name", "0xabc", 0),
},
{
Name: "name-changed event",
Group: createGroup(nil, nil, ""),
Result: createGroup(nil, nil, "some-name"),
From: "0xabc",
Event: NewNameChangedEvent("some-name", 0),
},
{
Name: "admins-added event",
Group: createGroup(nil, nil, ""),
Result: createGroup([]string{"0xabc", "0x123"}, nil, ""),
From: "0xabc",
Event: NewAdminsAddedEvent([]string{"0xabc", "0x123"}, 0),
},
{
Name: "admin-removed event",
Group: createGroup([]string{"0xabc", "0xdef"}, nil, ""),
Result: createGroup([]string{"0xdef"}, nil, ""),
From: "0xabc",
Event: NewAdminRemovedEvent("0xabc", 0),
},
{
Name: "members-added event",
Group: createGroup(nil, nil, ""),
Result: createGroup(nil, []string{"0xabc", "0xdef"}, ""),
From: "0xabc",
Event: NewMembersAddedEvent([]string{"0xabc", "0xdef"}, 0),
},
{
Name: "member-removed event",
Group: createGroup(nil, []string{"0xabc", "0xdef"}, ""),
Result: createGroup(nil, []string{"0xdef"}, ""),
From: "0xabc",
Event: NewMemberRemovedEvent("0xabc", 0),
},
{
Name: "member-joined event",
Group: createGroup(nil, []string{"0xabc"}, ""),
Result: createGroup(nil, []string{"0xabc", "0xdef"}, ""),
From: "0xabc",
Event: NewMemberJoinedEvent("0xdef", 0),
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
g := tc.Group
g.processEvent(tc.From, tc.Event)
require.EqualValues(t, tc.Result, g)
})
}
}
func TestGroupValidateEvent(t *testing.T) {
createGroup := func(admins, members []string) Group {
return Group{
admins: newStringSetFromSlice(admins),
members: newStringSetFromSlice(members),
}
}
testCases := []struct {
Name string
From string
@ -116,152 +209,132 @@ func TestGroupValidateEvent(t *testing.T) {
Result bool
}{
{
Name: "chat-created with empty admins and contacts",
Event: NewChatCreatedEvent("test", 0),
Name: "chat-created with empty admins and members",
Group: createGroup(nil, nil),
Event: NewChatCreatedEvent("test", "0xabc", 0),
Result: true,
},
{
Name: "chat-created with existing admins",
Group: Group{
Admins: []string{"0xabc"},
},
Event: NewChatCreatedEvent("test", 0),
Name: "chat-created with existing admins",
Group: createGroup([]string{"0xabc"}, nil),
Event: NewChatCreatedEvent("test", "0xabc", 0),
Result: false,
},
{
Name: "chat-created with existing contacts",
Group: Group{
Contacts: []string{"0xabc"},
},
Event: NewChatCreatedEvent("test", 0),
Name: "chat-created with existing members",
Group: createGroup(nil, []string{"0xabc"}),
Event: NewChatCreatedEvent("test", "0xabc", 0),
Result: false,
},
{
Name: "name-changed allowed because from is admin",
From: "0xabc",
Group: Group{
Admins: []string{"0xabc"},
},
Name: "name-changed allowed because from is admin",
From: "0xabc",
Group: createGroup([]string{"0xabc"}, nil),
Event: NewNameChangedEvent("new-name", 0),
Result: true,
},
{
Name: "name-changed not allowed for non-admins",
From: "0xabc",
Group: createGroup(nil, nil),
Event: NewNameChangedEvent("new-name", 0),
Result: false,
},
{
Name: "members-added allowed because from is admin",
From: "0xabc",
Group: Group{
Admins: []string{"0xabc"},
},
Name: "members-added allowed because from is admin",
From: "0xabc",
Group: createGroup([]string{"0xabc"}, nil),
Event: NewMembersAddedEvent([]string{"0x123"}, 0),
Result: true,
},
{
Name: "members-added not allowed for non-admins",
From: "0xabc",
Group: createGroup(nil, nil),
Event: NewMembersAddedEvent([]string{"0x123"}, 0),
Result: false,
},
{
Name: "member-removed allowed because removing themselves",
From: "0xabc",
Group: createGroup(nil, nil),
Event: NewMemberRemovedEvent("0xabc", 0),
Result: true,
},
{
Name: "member-removed allowed because from is admin",
From: "0xabc",
Group: Group{
Admins: []string{"0xabc"},
},
Name: "member-removed allowed because from is admin",
From: "0xabc",
Group: createGroup([]string{"0xabc"}, nil),
Event: NewMemberRemovedEvent("0x123", 0),
Result: true,
},
{
Name: "member-removed not allowed for non-admins",
From: "0xabc",
Group: createGroup(nil, nil),
Event: NewMemberRemovedEvent("0x123", 0),
Result: false,
},
{
Name: "member-joined must be in contacts",
From: "0xabc",
Group: Group{
Contacts: []string{"0xabc"},
},
Name: "member-joined must be in members",
From: "0xabc",
Group: createGroup(nil, []string{"0xabc"}),
Event: NewMemberJoinedEvent("0xabc", 0),
Result: true,
},
{
Name: "member-joined not valid because not in contacts",
Name: "member-joined not valid because not in members",
From: "0xabc",
Group: createGroup(nil, nil),
Event: NewMemberJoinedEvent("0xabc", 0),
Result: false,
},
{
Name: "member-joined not valid because from differs from the event",
From: "0xdef",
Group: createGroup(nil, nil),
Event: NewMemberJoinedEvent("0xabc", 0),
Result: false,
},
{
Name: "admins-added allowed because originating from other admin",
From: "0xabc",
Group: Group{
Admins: []string{"0xabc", "0x123"},
Contacts: []string{"0xdef", "0xghi"},
},
Name: "admins-added allowed because originating from other admin",
From: "0xabc",
Group: createGroup([]string{"0xabc", "0x123"}, []string{"0xdef", "0xghi"}),
Event: NewAdminsAddedEvent([]string{"0xdef"}, 0),
Result: true,
},
{
Name: "admins-added not allowed because not from admin",
From: "0xabc",
Group: Group{
Admins: []string{"0x123"},
Contacts: []string{"0xdef", "0xghi"},
},
Name: "admins-added not allowed because not from admin",
From: "0xabc",
Group: createGroup([]string{"0x123"}, []string{"0xdef", "0xghi"}),
Event: NewAdminsAddedEvent([]string{"0xdef"}, 0),
Result: false,
},
{
Name: "admins-added not allowed because not in contacts",
From: "0xabc",
Group: Group{
Admins: []string{"0xabc", "0x123"},
Contacts: []string{"0xghi"},
},
Name: "admins-added not allowed because not in members",
From: "0xabc",
Group: createGroup([]string{"0xabc", "0x123"}, []string{"0xghi"}),
Event: NewAdminsAddedEvent([]string{"0xdef"}, 0),
Result: false,
},
{
Name: "admin-removed allowed because is admin and removes themselves",
From: "0xabc",
Group: Group{
Admins: []string{"0xabc"},
},
Name: "admin-removed allowed because is admin and removes themselves",
From: "0xabc",
Group: createGroup([]string{"0xabc"}, nil),
Event: NewAdminRemovedEvent("0xabc", 0),
Result: true,
},
{
Name: "admin-removed not allowed because not themselves",
From: "0xabc",
Group: Group{
Admins: []string{"0xabc", "0xdef"},
},
Name: "admin-removed not allowed because not themselves",
From: "0xabc",
Group: createGroup([]string{"0xabc", "0xdef"}, nil),
Event: NewAdminRemovedEvent("0xdef", 0),
Result: false,
},
{
Name: "admin-removed not allowed because not admin",
From: "0xdef",
Group: Group{
Admins: []string{"0xabc"},
},
Name: "admin-removed not allowed because not admin",
From: "0xdef",
Group: createGroup([]string{"0xabc"}, nil),
Event: NewAdminRemovedEvent("0xabc", 0),
Result: false,
},
@ -269,8 +342,101 @@ func TestGroupValidateEvent(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result := tc.Group.ValidateEvent(tc.From, tc.Event)
result := tc.Group.validateEvent(tc.From, tc.Event)
assert.Equal(t, tc.Result, result)
})
}
}
func TestMembershipUpdateMessageProcess(t *testing.T) {
key, err := crypto.GenerateKey()
require.NoError(t, err)
updates := []MembershipUpdate{
{
ChatID: "some-chat",
Events: []MembershipUpdateEvent{
NewChatCreatedEvent("some-name", "0xabc", 0),
},
},
}
err = updates[0].Sign(key)
require.NoError(t, err)
require.NotEmpty(t, updates[0].Signature)
message := MembershipUpdateMessage{
ChatID: "some-chat",
Updates: updates,
}
err = message.Verify()
require.NoError(t, err)
require.EqualValues(t, "0x"+hex.EncodeToString(crypto.FromECDSAPub(&key.PublicKey)), updates[0].From)
}
func TestMembershipUpdateEventEqual(t *testing.T) {
u1 := MembershipUpdateEvent{
Type: MembershipUpdateChatCreated,
ClockValue: 1,
Member: "0xabc",
Members: []string{"0xabc"},
Name: "abc",
}
require.True(t, u1.Equal(u1))
// Verify equality breaking.
u2 := u1
u2.Members = append(u2.Members, "0xdef")
require.False(t, u1.Equal(u2))
u2 = u1
u2.Type = MembershipUpdateMembersAdded
require.False(t, u1.Equal(u2))
u2 = u1
u2.ClockValue = 2
require.False(t, u1.Equal(u2))
u2 = u1
u2.Member = "0xdef"
require.False(t, u1.Equal(u2))
u2 = u1
u2.Name = "def"
require.False(t, u1.Equal(u2))
}
func TestMembershipUpdateFlatEqual(t *testing.T) {
u1 := MembershipUpdateFlat{
ChatID: "abc",
Signature: "abc",
From: "0xabc",
}
require.True(t, u1.Equal(u1))
// Verify equality breaking.
u2 := u1
u2.ChatID = "def"
require.False(t, u1.Equal(u2))
u2 = u1
u2.Signature = "def"
require.False(t, u1.Equal(u2))
u2 = u1
u2.From = "0xdef"
require.False(t, u1.Equal(u2))
}
func TestMergeFlatMembershipUpdates(t *testing.T) {
u1 := []MembershipUpdateFlat{
{
ChatID: "abc",
Signature: "abc",
From: "0xabc",
},
}
u2 := []MembershipUpdateFlat{
{
ChatID: "abc",
Signature: "def",
From: "0xdef",
},
}
result := MergeFlatMembershipUpdates(u1, u1)
require.EqualValues(t, u1, result)
result = MergeFlatMembershipUpdates(u1, u2)
require.EqualValues(t, append(u1, u2...), result)
}