mirror of
https://github.com/status-im/status-protocol-go.git
synced 2025-03-03 12:00:33 +00:00
Implement group messages logic (#86)
This commit is contained in:
parent
17cb8c53c3
commit
1859f6c80c
99
chat.go
99
chat.go
@ -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
28
chat_group_proxy.go
Normal 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
|
||||
}
|
@ -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
68
message_handler.go
Normal 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
|
||||
}
|
@ -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,
|
||||
|
@ -98,6 +98,7 @@ func (s *MessageProcessorSuite) SetupTest() {
|
||||
database,
|
||||
encryptionProtocol,
|
||||
whisperTransport,
|
||||
nil,
|
||||
s.logger,
|
||||
featureFlags{},
|
||||
)
|
||||
|
163
messenger.go
163
messenger.go
@ -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 != "" {
|
||||
return m.transport.JoinPublic(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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := m.cacheOwnMessage(chatID, id, message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
default:
|
||||
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})
|
||||
_, err := m.persistence.SaveMessages([]*protocol.Message{message})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 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 nil, errors.New("chat is neither public nor private")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendRaw takes encoded data, encrypts it and sends through the wire.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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),
|
||||
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"},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
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",
|
||||
Name: "member-joined must be in members",
|
||||
From: "0xabc",
|
||||
Group: Group{
|
||||
Contacts: []string{"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"},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
Group: createGroup([]string{"0x123"}, []string{"0xdef", "0xghi"}),
|
||||
Event: NewAdminsAddedEvent([]string{"0xdef"}, 0),
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
Name: "admins-added not allowed because not in contacts",
|
||||
Name: "admins-added not allowed because not in members",
|
||||
From: "0xabc",
|
||||
Group: Group{
|
||||
Admins: []string{"0xabc", "0x123"},
|
||||
Contacts: []string{"0xghi"},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user