status-go/protocol/communities/community.go

708 lines
18 KiB
Go

package communities
import (
"bytes"
"crypto/ecdsa"
"encoding/json"
"sync"
"github.com/golang/protobuf/proto"
"go.uber.org/zap"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/v1"
)
const signatureLength = 65
type Config struct {
PrivateKey *ecdsa.PrivateKey
CommunityDescription *protobuf.CommunityDescription
MarshaledCommunityDescription []byte
ID *ecdsa.PublicKey
Joined bool
Verified bool
Logger *zap.Logger
}
type Community struct {
config *Config
mutex sync.Mutex
}
func New(config Config) (*Community, error) {
if config.Logger == nil {
logger, err := zap.NewDevelopment()
if err != nil {
return nil, err
}
config.Logger = logger
}
community := &Community{config: &config}
community.initialize()
return community, nil
}
func (o *Community) MarshalJSON() ([]byte, error) {
item := struct {
*protobuf.CommunityDescription `json:"description"`
ID string `json:"id"`
Admin bool `json:"admin"`
Verified bool `json:"verified"`
Joined bool `json:"joined"`
}{
ID: o.IDString(),
CommunityDescription: o.config.CommunityDescription,
Admin: o.IsAdmin(),
Verified: o.config.Verified,
Joined: o.config.Joined,
}
return json.Marshal(item)
}
func (o *Community) initialize() {
if o.config.CommunityDescription == nil {
o.config.CommunityDescription = &protobuf.CommunityDescription{}
}
}
type CommunityChatChanges struct {
MembersAdded map[string]*protobuf.CommunityMember
MembersRemoved map[string]*protobuf.CommunityMember
}
type CommunityChanges struct {
MembersAdded map[string]*protobuf.CommunityMember
MembersRemoved map[string]*protobuf.CommunityMember
ChatsRemoved map[string]*protobuf.CommunityChat
ChatsAdded map[string]*protobuf.CommunityChat
ChatsModified map[string]*CommunityChatChanges
}
func emptyCommunityChanges() *CommunityChanges {
return &CommunityChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
ChatsRemoved: make(map[string]*protobuf.CommunityChat),
ChatsAdded: make(map[string]*protobuf.CommunityChat),
ChatsModified: make(map[string]*CommunityChatChanges),
}
}
func (o *Community) CreateChat(chatID string, chat *protobuf.CommunityChat) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if o.config.PrivateKey == nil {
return nil, ErrNotAdmin
}
err := validateCommunityChat(o.config.CommunityDescription, chat)
if err != nil {
return nil, err
}
if o.config.CommunityDescription.Chats == nil {
o.config.CommunityDescription.Chats = make(map[string]*protobuf.CommunityChat)
}
if _, ok := o.config.CommunityDescription.Chats[chatID]; ok {
return nil, ErrChatAlreadyExists
}
o.config.CommunityDescription.Chats[chatID] = chat
o.increaseClock()
changes := emptyCommunityChanges()
changes.ChatsAdded[chatID] = chat
return changes, nil
}
func (o *Community) DeleteChat(chatID string) (*protobuf.CommunityDescription, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if o.config.PrivateKey == nil {
return nil, ErrNotAdmin
}
if o.config.CommunityDescription.Chats == nil {
o.config.CommunityDescription.Chats = make(map[string]*protobuf.CommunityChat)
}
delete(o.config.CommunityDescription.Chats, chatID)
o.increaseClock()
return o.config.CommunityDescription, nil
}
func (o *Community) InviteUserToOrg(pk *ecdsa.PublicKey) (*protobuf.CommunityInvitation, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if o.config.PrivateKey == nil {
return nil, ErrNotAdmin
}
memberKey := common.PubkeyToHex(pk)
if o.config.CommunityDescription.Members == nil {
o.config.CommunityDescription.Members = make(map[string]*protobuf.CommunityMember)
}
if _, ok := o.config.CommunityDescription.Members[memberKey]; !ok {
o.config.CommunityDescription.Members[memberKey] = &protobuf.CommunityMember{}
}
o.increaseClock()
response := &protobuf.CommunityInvitation{}
marshaledCommunity, err := o.toBytes()
if err != nil {
return nil, err
}
response.CommunityDescription = marshaledCommunity
grant, err := o.buildGrant(pk, "")
if err != nil {
return nil, err
}
response.Grant = grant
response.PublicKey = crypto.CompressPubkey(pk)
return response, nil
}
func (o *Community) InviteUserToChat(pk *ecdsa.PublicKey, chatID string) (*protobuf.CommunityInvitation, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if o.config.PrivateKey == nil {
return nil, ErrNotAdmin
}
memberKey := common.PubkeyToHex(pk)
if _, ok := o.config.CommunityDescription.Members[memberKey]; !ok {
o.config.CommunityDescription.Members[memberKey] = &protobuf.CommunityMember{}
}
chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok {
return nil, ErrChatNotFound
}
if chat.Members == nil {
chat.Members = make(map[string]*protobuf.CommunityMember)
}
chat.Members[memberKey] = &protobuf.CommunityMember{}
o.increaseClock()
response := &protobuf.CommunityInvitation{}
marshaledCommunity, err := o.toBytes()
if err != nil {
return nil, err
}
response.CommunityDescription = marshaledCommunity
grant, err := o.buildGrant(pk, chatID)
if err != nil {
return nil, err
}
response.Grant = grant
response.ChatId = chatID
return response, nil
}
func (o *Community) hasMember(pk *ecdsa.PublicKey) bool {
key := common.PubkeyToHex(pk)
_, ok := o.config.CommunityDescription.Members[key]
return ok
}
func (o *Community) HasMember(pk *ecdsa.PublicKey) bool {
o.mutex.Lock()
defer o.mutex.Unlock()
return o.hasMember(pk)
}
func (o *Community) IsMemberInChat(pk *ecdsa.PublicKey, chatID string) bool {
o.mutex.Lock()
defer o.mutex.Unlock()
if !o.hasMember(pk) {
return false
}
chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok {
return false
}
key := common.PubkeyToHex(pk)
_, ok = chat.Members[key]
return ok
}
func (o *Community) RemoveUserFromChat(pk *ecdsa.PublicKey, chatID string) (*protobuf.CommunityDescription, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if o.config.PrivateKey == nil {
return nil, ErrNotAdmin
}
if !o.hasMember(pk) {
return o.config.CommunityDescription, nil
}
chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok {
return o.config.CommunityDescription, nil
}
key := common.PubkeyToHex(pk)
delete(chat.Members, key)
return o.config.CommunityDescription, nil
}
func (o *Community) RemoveUserFromOrg(pk *ecdsa.PublicKey) (*protobuf.CommunityDescription, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if o.config.PrivateKey == nil {
return nil, ErrNotAdmin
}
if !o.hasMember(pk) {
return o.config.CommunityDescription, nil
}
key := common.PubkeyToHex(pk)
// Remove from org
delete(o.config.CommunityDescription.Members, key)
// Remove from chats
for _, chat := range o.config.CommunityDescription.Chats {
delete(chat.Members, key)
}
return o.config.CommunityDescription, nil
}
// TODO: this should accept a request from a user to join and perform any validation
func (o *Community) AcceptRequestToJoin(pk *ecdsa.PublicKey) (*protobuf.CommunityRequestJoinResponse, error) {
return nil, nil
}
// TODO: this should decline a request from a user to join
func (o *Community) DeclineRequestToJoin(pk *ecdsa.PublicKey) (*protobuf.CommunityRequestJoinResponse, error) {
return nil, nil
}
func (o *Community) Join() {
o.config.Joined = true
}
func (o *Community) Leave() {
o.config.Joined = false
}
func (o *Community) Joined() bool {
return o.config.Joined
}
func (o *Community) HandleCommunityDescription(signer *ecdsa.PublicKey, description *protobuf.CommunityDescription, rawMessage []byte) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
if !common.IsPubKeyEqual(o.config.ID, signer) {
return nil, ErrNotAuthorized
}
err := ValidateCommunityDescription(description)
if err != nil {
return nil, err
}
response := emptyCommunityChanges()
if description.Clock <= o.config.CommunityDescription.Clock {
return response, nil
}
// We only calculate changes if we joined the org, otherwise not interested
if o.config.Joined {
// Check for new members at the org level
for pk, member := range description.Members {
if _, ok := o.config.CommunityDescription.Members[pk]; !ok {
if response.MembersAdded == nil {
response.MembersAdded = make(map[string]*protobuf.CommunityMember)
}
response.MembersAdded[pk] = member
}
}
// Check for removed members at the org level
for pk, member := range o.config.CommunityDescription.Members {
if _, ok := description.Members[pk]; !ok {
if response.MembersRemoved == nil {
response.MembersRemoved = make(map[string]*protobuf.CommunityMember)
}
response.MembersRemoved[pk] = member
}
}
// check for removed chats
for chatID, chat := range o.config.CommunityDescription.Chats {
if description.Chats == nil {
description.Chats = make(map[string]*protobuf.CommunityChat)
}
if _, ok := description.Chats[chatID]; !ok {
if response.ChatsRemoved == nil {
response.ChatsRemoved = make(map[string]*protobuf.CommunityChat)
}
response.ChatsRemoved[chatID] = chat
}
}
for chatID, chat := range description.Chats {
if o.config.CommunityDescription.Chats == nil {
o.config.CommunityDescription.Chats = make(map[string]*protobuf.CommunityChat)
}
if _, ok := o.config.CommunityDescription.Chats[chatID]; !ok {
if response.ChatsAdded == nil {
response.ChatsAdded = make(map[string]*protobuf.CommunityChat)
}
response.ChatsAdded[chatID] = chat
} else {
// Check for members added
for pk, member := range description.Chats[chatID].Members {
if _, ok := o.config.CommunityDescription.Chats[chatID].Members[pk]; !ok {
if response.ChatsModified[chatID] == nil {
response.ChatsModified[chatID] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
}
}
response.ChatsModified[chatID].MembersAdded[pk] = member
}
}
// check for members removed
for pk, member := range o.config.CommunityDescription.Chats[chatID].Members {
if _, ok := description.Chats[chatID].Members[pk]; !ok {
if response.ChatsModified[chatID] == nil {
response.ChatsModified[chatID] = &CommunityChatChanges{
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
}
}
response.ChatsModified[chatID].MembersRemoved[pk] = member
}
}
}
}
}
o.config.CommunityDescription = description
o.config.MarshaledCommunityDescription = rawMessage
return response, nil
}
// HandleRequestJoin handles a request, checks that the right permissions are applied and returns an CommunityRequestJoinResponse
func (o *Community) HandleRequestJoin(signer *ecdsa.PublicKey, request *protobuf.CommunityRequestJoin) error {
o.mutex.Lock()
defer o.mutex.Unlock()
// If we are not admin, fuggetaboutit
if !o.IsAdmin() {
return ErrNotAdmin
}
// If the org is ens name only, then reject if not present
if o.config.CommunityDescription.Permissions.EnsOnly && len(request.EnsName) == 0 {
return ErrCantRequestAccess
}
if len(request.ChatId) != 0 {
return o.handleRequestJoinWithChatID(request)
}
err := o.handleRequestJoinWithoutChatID(request)
if err != nil {
return err
}
// Store request to join
return nil
}
func (o *Community) IsAdmin() bool {
return o.config.PrivateKey != nil
}
func (o *Community) handleRequestJoinWithChatID(request *protobuf.CommunityRequestJoin) error {
chat, ok := o.config.CommunityDescription.Chats[request.ChatId]
if !ok {
return ErrChatNotFound
}
// If chat is no permissions, access should not have been requested
if chat.Permissions.Access != protobuf.CommunityPermissions_ON_REQUEST {
return ErrCantRequestAccess
}
if chat.Permissions.EnsOnly && len(request.EnsName) == 0 {
return ErrCantRequestAccess
}
return nil
}
func (o *Community) handleRequestJoinWithoutChatID(request *protobuf.CommunityRequestJoin) error {
// If they want access to the org only, check that the org is ON_REQUEST
if o.config.CommunityDescription.Permissions.Access != protobuf.CommunityPermissions_ON_REQUEST {
return ErrCantRequestAccess
}
return nil
}
func (o *Community) ID() []byte {
return crypto.CompressPubkey(o.config.ID)
}
func (o *Community) IDString() string {
return types.EncodeHex(o.ID())
}
func (o *Community) PrivateKey() *ecdsa.PrivateKey {
return o.config.PrivateKey
}
func (o *Community) marshaledDescription() ([]byte, error) {
return proto.Marshal(o.config.CommunityDescription)
}
func (o *Community) MarshaledDescription() ([]byte, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
return o.marshaledDescription()
}
func (o *Community) toBytes() ([]byte, error) {
// This should not happen, as we can only serialize on our side if we
// created the community
if o.config.PrivateKey == nil && len(o.config.MarshaledCommunityDescription) == 0 {
return nil, ErrNotAdmin
}
// We are not admin, use the received serialized version
if o.config.PrivateKey == nil {
return o.config.MarshaledCommunityDescription, nil
}
// serialize and sign
payload, err := o.marshaledDescription()
if err != nil {
return nil, err
}
return protocol.WrapMessageV1(payload, protobuf.ApplicationMetadataMessage_COMMUNITY_DESCRIPTION, o.config.PrivateKey)
}
// ToBytes returns the community in a wrapped & signed protocol message
func (o *Community) ToBytes() ([]byte, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
return o.toBytes()
}
func (o *Community) Chats() map[string]*protobuf.CommunityChat {
o.mutex.Lock()
defer o.mutex.Unlock()
response := make(map[string]*protobuf.CommunityChat)
for k, v := range o.config.CommunityDescription.Chats {
response[k] = v
}
return response
}
func (o *Community) VerifyGrantSignature(data []byte) (*protobuf.Grant, error) {
if len(data) <= signatureLength {
return nil, ErrInvalidGrant
}
signature := data[:signatureLength]
payload := data[signatureLength:]
grant := &protobuf.Grant{}
err := proto.Unmarshal(payload, grant)
if err != nil {
return nil, err
}
if grant.Clock == 0 {
return nil, ErrInvalidGrant
}
if grant.MemberId == nil {
return nil, ErrInvalidGrant
}
if !bytes.Equal(grant.CommunityId, o.ID()) {
return nil, ErrInvalidGrant
}
extractedPublicKey, err := crypto.SigToPub(crypto.Keccak256(payload), signature)
if err != nil {
return nil, err
}
if !common.IsPubKeyEqual(o.config.ID, extractedPublicKey) {
return nil, ErrInvalidGrant
}
return grant, nil
}
func (o *Community) CanPost(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) {
if o.config.CommunityDescription.Chats == nil {
o.config.Logger.Debug("canPost, no-chats")
return false, nil
}
chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok {
o.config.Logger.Debug("canPost, no chat with id", zap.String("chat-id", chatID))
return false, nil
}
// creator can always post
if common.IsPubKeyEqual(pk, o.config.ID) {
return true, nil
}
// If both the chat & the org have no permissions, the user is allowed to post
if o.config.CommunityDescription.Permissions.Access == protobuf.CommunityPermissions_NO_MEMBERSHIP && chat.Permissions.Access == protobuf.CommunityPermissions_NO_MEMBERSHIP {
return true, nil
}
if chat.Permissions.Access != protobuf.CommunityPermissions_NO_MEMBERSHIP {
if chat.Members == nil {
o.config.Logger.Debug("canPost, no members in chat", zap.String("chat-id", chatID))
return false, nil
}
_, ok := chat.Members[common.PubkeyToHex(pk)]
// If member, we stop here
if ok {
return true, nil
}
// If not a member, and not grant, we return
if !ok && grantBytes == nil {
o.config.Logger.Debug("canPost, not a member in chat", zap.String("chat-id", chatID))
return false, nil
}
// Otherwise we verify the grant
return o.canPostWithGrant(pk, chatID, grantBytes)
}
// Chat has no membership, check org permissions
if o.config.CommunityDescription.Members == nil {
o.config.Logger.Debug("canPost, no members in org", zap.String("chat-id", chatID))
return false, nil
}
// If member, they can post
_, ok = o.config.CommunityDescription.Members[common.PubkeyToHex(pk)]
if ok {
return true, nil
}
// Not a member and no grant, can't post
if !ok && grantBytes == nil {
o.config.Logger.Debug("canPost, not a member in org", zap.String("chat-id", chatID), zap.String("pubkey", common.PubkeyToHex(pk)))
return false, nil
}
return o.canPostWithGrant(pk, chatID, grantBytes)
}
func (o *Community) canPostWithGrant(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) {
grant, err := o.VerifyGrantSignature(grantBytes)
if err != nil {
return false, err
}
// If the clock is lower or equal is invalid
if grant.Clock <= o.config.CommunityDescription.Clock {
return false, nil
}
if grant.MemberId == nil {
return false, nil
}
grantPk, err := crypto.DecompressPubkey(grant.MemberId)
if err != nil {
return false, nil
}
if !common.IsPubKeyEqual(grantPk, pk) {
return false, nil
}
if chatID != grant.ChatId {
return false, nil
}
return true, nil
}
func (o *Community) buildGrant(key *ecdsa.PublicKey, chatID string) ([]byte, error) {
grant := &protobuf.Grant{
CommunityId: o.ID(),
MemberId: crypto.CompressPubkey(key),
ChatId: chatID,
Clock: o.config.CommunityDescription.Clock,
}
marshaledGrant, err := proto.Marshal(grant)
if err != nil {
return nil, err
}
signatureMaterial := crypto.Keccak256(marshaledGrant)
signature, err := crypto.Sign(signatureMaterial, o.config.PrivateKey)
if err != nil {
return nil, err
}
return append(signature, marshaledGrant...), nil
}
func (o *Community) increaseClock() {
o.config.CommunityDescription.Clock = o.nextClock()
}
func (o *Community) nextClock() uint64 {
return o.config.CommunityDescription.Clock + 1
}