package model import ( "crypto/rand" "cwtch.im/cwtch/protocol" "errors" "fmt" "git.openprivacy.ca/openprivacy/libricochet-go/log" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "github.com/golang/protobuf/proto" "golang.org/x/crypto/nacl/secretbox" "io" "sync" "time" ) // Group defines and encapsulates Cwtch's conception of group chat. Which are sessions // tied to a server under a given group key. Each group has a set of Messages. type Group struct { GroupID string SignedGroupID []byte GroupKey [32]byte GroupServer string Timeline Timeline `json:"-"` Accepted bool Owner string IsCompromised bool InitialMessage []byte Attributes map[string]string lock sync.Mutex LocalID string State string `json:"-"` unacknowledgedMessages []Message } // NewGroup initializes a new group associated with a given CwtchServer func NewGroup(server string) (*Group, error) { group := new(Group) group.LocalID = GenerateRandomID() if utils.IsValidHostname(server) == false { return nil, errors.New("Server is not a valid v3 onion") } group.GroupServer = server var groupID [16]byte if _, err := io.ReadFull(rand.Reader, groupID[:]); err != nil { log.Errorf("Cannot read from random: %v\n", err) return nil, err } group.GroupID = fmt.Sprintf("%x", groupID) var groupKey [32]byte if _, err := io.ReadFull(rand.Reader, groupKey[:]); err != nil { log.Errorf("Error: Cannot read from random: %v\n", err) return nil, err } copy(group.GroupKey[:], groupKey[:]) group.Owner = "self" group.Attributes = make(map[string]string) return group, nil } // SignGroup adds a signature to the group. func (g *Group) SignGroup(signature []byte) { g.SignedGroupID = signature copy(g.Timeline.SignedGroupID[:], g.SignedGroupID) } // Compromised should be called if we detect a a groupkey leak. func (g *Group) Compromised() { g.IsCompromised = true } // GetInitialMessage returns the first message of the group, if one was sent with the invite. func (g *Group) GetInitialMessage() []byte { g.lock.Lock() defer g.lock.Unlock() return g.InitialMessage } // Invite generates a invitation that can be sent to a cwtch peer func (g *Group) Invite(initialMessage []byte) ([]byte, error) { if g.SignedGroupID == nil { return nil, errors.New("group isn't signed") } g.InitialMessage = initialMessage[:] gci := &protocol.GroupChatInvite{ GroupName: g.GroupID, GroupSharedKey: g.GroupKey[:], ServerHost: g.GroupServer, SignedGroupId: g.SignedGroupID[:], InitialMessage: initialMessage[:], } invite, err := proto.Marshal(gci) return invite, err } // AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline func (g *Group) AddSentMessage(message *protocol.DecryptedGroupMessage, sig []byte) Message { g.lock.Lock() defer g.lock.Unlock() timelineMessage := Message{ Message: message.GetText(), Timestamp: time.Unix(int64(message.GetTimestamp()), 0), Received: time.Unix(0, 0), Signature: sig, PeerID: message.GetOnion(), PreviousMessageSig: message.GetPreviousMessageSig(), ReceivedByServer: false, } g.unacknowledgedMessages = append(g.unacknowledgedMessages, timelineMessage) return timelineMessage } // ErrorSentMessage removes a sent message from the unacknowledged list and sets its error flag if found, otherwise returns false func (g *Group) ErrorSentMessage(sig []byte, error string) bool { g.lock.Lock() defer g.lock.Unlock() var message *Message // Delete the message from the unack'd buffer if it exists for i, unAckedMessage := range g.unacknowledgedMessages { if compareSignatures(unAckedMessage.Signature, sig) { message = &unAckedMessage g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...) message.Error = error g.Timeline.Insert(message) return true } } return false } // AddMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline func (g *Group) AddMessage(message *protocol.DecryptedGroupMessage, sig []byte) (*Message, bool) { g.lock.Lock() defer g.lock.Unlock() // Delete the message from the unack'd buffer if it exists for i, unAckedMessage := range g.unacknowledgedMessages { if compareSignatures(unAckedMessage.Signature, sig) { g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...) break } } timelineMessage := &Message{ Message: message.GetText(), Timestamp: time.Unix(int64(message.GetTimestamp()), 0), Received: time.Now(), Signature: sig, PeerID: message.GetOnion(), PreviousMessageSig: message.GetPreviousMessageSig(), ReceivedByServer: true, Error: "", } seen := g.Timeline.Insert(timelineMessage) return timelineMessage, seen } // GetTimeline provides a safe copy of the timeline func (g *Group) GetTimeline() (timeline []Message) { g.lock.Lock() defer g.lock.Unlock() return append(g.Timeline.GetMessages(), g.unacknowledgedMessages...) } //EncryptMessage takes a message and encrypts the message under the group key. func (g *Group) EncryptMessage(message *protocol.DecryptedGroupMessage) ([]byte, error) { var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { log.Errorf("Cannot read from random: %v\n", err) return nil, err } wire, err := proto.Marshal(message) if err != nil { return nil, err } encrypted := secretbox.Seal(nonce[:], []byte(wire), &nonce, &g.GroupKey) return encrypted, nil } // DecryptMessage takes a ciphertext and returns true and the decrypted message if the // cipher text can be successfully decrypted,else false. func (g *Group) DecryptMessage(ciphertext []byte) (bool, *protocol.DecryptedGroupMessage) { if len(ciphertext) > 24 { var decryptNonce [24]byte copy(decryptNonce[:], ciphertext[:24]) decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &g.GroupKey) if ok { dm := &protocol.DecryptedGroupMessage{} err := proto.Unmarshal(decrypted, dm) if err == nil { return true, dm } } } return false, nil } // SetAttribute allows applications to store arbitrary configuration info at the group level. func (g *Group) SetAttribute(name string, value string) { g.lock.Lock() defer g.lock.Unlock() g.Attributes[name] = value } // GetAttribute returns the value of a value set with SetAttribute. If no such value has been set exists is set to false. func (g *Group) GetAttribute(name string) (value string, exists bool) { g.lock.Lock() defer g.lock.Unlock() value, exists = g.Attributes[name] return }