2018-03-09 20:44:13 +00:00
|
|
|
package model
|
|
|
|
|
|
|
|
import (
|
2021-05-14 18:26:04 +00:00
|
|
|
"crypto/ed25519"
|
2018-03-09 20:44:13 +00:00
|
|
|
"crypto/rand"
|
2021-05-14 18:26:04 +00:00
|
|
|
"crypto/sha512"
|
2020-12-17 01:40:03 +00:00
|
|
|
"cwtch.im/cwtch/model/attr"
|
2020-07-14 00:46:05 +00:00
|
|
|
"cwtch.im/cwtch/protocol/groups"
|
2021-05-14 18:26:04 +00:00
|
|
|
"encoding/base32"
|
2021-05-03 23:32:48 +00:00
|
|
|
"encoding/base64"
|
2021-05-14 18:26:04 +00:00
|
|
|
"encoding/hex"
|
2020-07-14 00:46:05 +00:00
|
|
|
"encoding/json"
|
2018-05-16 20:20:46 +00:00
|
|
|
"errors"
|
2018-03-09 20:44:13 +00:00
|
|
|
"fmt"
|
2020-02-10 22:09:24 +00:00
|
|
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
|
|
|
"git.openprivacy.ca/openprivacy/log"
|
2018-03-30 21:16:51 +00:00
|
|
|
"golang.org/x/crypto/nacl/secretbox"
|
2021-05-14 18:26:04 +00:00
|
|
|
"golang.org/x/crypto/pbkdf2"
|
2018-03-30 21:16:51 +00:00
|
|
|
"io"
|
2021-05-14 18:26:04 +00:00
|
|
|
"strings"
|
2018-05-06 04:18:00 +00:00
|
|
|
"sync"
|
2018-05-09 19:09:00 +00:00
|
|
|
"time"
|
2018-03-09 20:44:13 +00:00
|
|
|
)
|
|
|
|
|
2020-09-28 18:18:18 +00:00
|
|
|
// CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date
|
2021-05-14 18:26:04 +00:00
|
|
|
const CurrentGroupVersion = 3
|
|
|
|
|
|
|
|
// GroupInvitePrefix identifies a particular string as being a serialized group invite.
|
|
|
|
const GroupInvitePrefix = "torv3"
|
2020-09-28 18:18:18 +00:00
|
|
|
|
2018-10-05 03:18:34 +00:00
|
|
|
// Group defines and encapsulates Cwtch's conception of group chat. Which are sessions
|
2019-02-03 01:18:33 +00:00
|
|
|
// tied to a server under a given group key. Each group has a set of Messages.
|
2018-03-09 20:44:13 +00:00
|
|
|
type Group struct {
|
2021-05-14 18:26:04 +00:00
|
|
|
// GroupID is now derived from the GroupKey and the GroupServer
|
2019-02-20 20:03:04 +00:00
|
|
|
GroupID string
|
|
|
|
GroupKey [32]byte
|
|
|
|
GroupServer string
|
2019-07-19 17:27:50 +00:00
|
|
|
Timeline Timeline `json:"-"`
|
2019-02-20 20:03:04 +00:00
|
|
|
Accepted bool
|
|
|
|
IsCompromised bool
|
|
|
|
Attributes map[string]string
|
|
|
|
lock sync.Mutex
|
|
|
|
LocalID string
|
2019-05-15 20:12:11 +00:00
|
|
|
State string `json:"-"`
|
2020-10-07 20:53:22 +00:00
|
|
|
UnacknowledgedMessages []Message
|
2020-09-28 18:18:18 +00:00
|
|
|
Version int
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
|
|
|
|
2018-03-15 16:33:26 +00:00
|
|
|
// NewGroup initializes a new group associated with a given CwtchServer
|
2018-09-27 00:08:54 +00:00
|
|
|
func NewGroup(server string) (*Group, error) {
|
2018-03-09 20:44:13 +00:00
|
|
|
group := new(Group)
|
2020-09-28 18:18:18 +00:00
|
|
|
group.Version = CurrentGroupVersion
|
2019-12-12 20:21:14 +00:00
|
|
|
group.LocalID = GenerateRandomID()
|
2021-05-28 08:45:59 +00:00
|
|
|
group.Accepted = true // we are starting a group, so we assume we want to connect to it...
|
2021-06-02 18:34:57 +00:00
|
|
|
if !tor.IsValidHostname(server) {
|
|
|
|
return nil, errors.New("server is not a valid v3 onion")
|
2018-11-21 22:15:43 +00:00
|
|
|
}
|
|
|
|
|
2018-03-09 20:44:13 +00:00
|
|
|
group.GroupServer = server
|
|
|
|
|
|
|
|
var groupID [16]byte
|
|
|
|
if _, err := io.ReadFull(rand.Reader, groupID[:]); err != nil {
|
2018-12-04 02:52:11 +00:00
|
|
|
log.Errorf("Cannot read from random: %v\n", err)
|
2018-09-27 00:08:54 +00:00
|
|
|
return nil, err
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
|
|
|
group.GroupID = fmt.Sprintf("%x", groupID)
|
|
|
|
|
|
|
|
var groupKey [32]byte
|
|
|
|
if _, err := io.ReadFull(rand.Reader, groupKey[:]); err != nil {
|
2018-12-04 02:52:11 +00:00
|
|
|
log.Errorf("Error: Cannot read from random: %v\n", err)
|
2018-09-27 00:08:54 +00:00
|
|
|
return nil, err
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
|
|
|
copy(group.GroupKey[:], groupKey[:])
|
2021-05-14 18:26:04 +00:00
|
|
|
|
|
|
|
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
|
|
|
|
// and key.
|
|
|
|
group.GroupID = deriveGroupID(groupKey[:], server)
|
|
|
|
|
2018-11-02 23:43:40 +00:00
|
|
|
group.Attributes = make(map[string]string)
|
2020-12-17 01:40:03 +00:00
|
|
|
// By default we set the "name" of the group to a random string, we can override this later, but to simplify the
|
|
|
|
// codes around invite, we assume that this is always set.
|
|
|
|
group.Attributes[attr.GetLocalScope("name")] = group.GroupID
|
2018-09-27 00:08:54 +00:00
|
|
|
return group, nil
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
|
|
|
|
2021-05-18 19:09:11 +00:00
|
|
|
// CheckGroup returns true only if the ID of the group is cryptographically valid.
|
|
|
|
func (g *Group) CheckGroup() bool {
|
|
|
|
return g.GroupID == deriveGroupID(g.GroupKey[:], g.GroupServer)
|
|
|
|
}
|
|
|
|
|
2021-05-18 19:11:00 +00:00
|
|
|
// deriveGroupID hashes together the key and the hostname to create a bound identifier that can later
|
|
|
|
// be referenced and checked by profiles when they receive invites and messages.
|
2021-05-14 18:26:04 +00:00
|
|
|
func deriveGroupID(groupKey []byte, serverHostname string) string {
|
|
|
|
data, _ := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
|
|
|
|
pubkey := data[0:ed25519.PublicKeySize]
|
2021-05-18 19:09:11 +00:00
|
|
|
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New))
|
2018-03-15 20:53:22 +00:00
|
|
|
}
|
|
|
|
|
2021-05-18 19:11:00 +00:00
|
|
|
// Compromised should be called if we detect a groupkey leak
|
2018-05-20 18:38:56 +00:00
|
|
|
func (g *Group) Compromised() {
|
|
|
|
g.IsCompromised = true
|
|
|
|
}
|
|
|
|
|
2018-05-16 20:18:47 +00:00
|
|
|
// Invite generates a invitation that can be sent to a cwtch peer
|
2021-05-03 23:32:48 +00:00
|
|
|
func (g *Group) Invite() (string, error) {
|
2018-05-16 20:18:47 +00:00
|
|
|
|
2020-07-14 00:46:05 +00:00
|
|
|
gci := &groups.GroupInvite{
|
2021-05-14 18:26:04 +00:00
|
|
|
GroupID: g.GroupID,
|
|
|
|
GroupName: g.Attributes[attr.GetLocalScope("name")],
|
|
|
|
SharedKey: g.GroupKey[:],
|
|
|
|
ServerHost: g.GroupServer,
|
2018-03-15 20:53:22 +00:00
|
|
|
}
|
2018-04-28 17:52:59 +00:00
|
|
|
|
2020-07-14 00:46:05 +00:00
|
|
|
invite, err := json.Marshal(gci)
|
2021-05-14 18:26:04 +00:00
|
|
|
serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite))
|
2021-05-03 23:32:48 +00:00
|
|
|
return serializedInvite, err
|
2018-03-15 20:53:22 +00:00
|
|
|
}
|
|
|
|
|
2019-02-20 20:03:04 +00:00
|
|
|
// AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
|
2020-07-14 00:46:05 +00:00
|
|
|
func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte) Message {
|
2019-02-20 20:03:04 +00:00
|
|
|
g.lock.Lock()
|
|
|
|
defer g.lock.Unlock()
|
|
|
|
timelineMessage := Message{
|
2020-07-14 00:46:05 +00:00
|
|
|
Message: message.Text,
|
|
|
|
Timestamp: time.Unix(int64(message.Timestamp), 0),
|
2019-02-20 20:03:04 +00:00
|
|
|
Received: time.Unix(0, 0),
|
|
|
|
Signature: sig,
|
2020-07-14 00:46:05 +00:00
|
|
|
PeerID: message.Onion,
|
|
|
|
PreviousMessageSig: message.PreviousMessageSig,
|
2019-10-18 23:56:10 +00:00
|
|
|
ReceivedByServer: false,
|
2019-02-20 20:03:04 +00:00
|
|
|
}
|
2020-10-07 20:53:22 +00:00
|
|
|
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages, timelineMessage)
|
2019-02-20 20:03:04 +00:00
|
|
|
return timelineMessage
|
|
|
|
}
|
|
|
|
|
2019-10-18 23:56:10 +00:00
|
|
|
// 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
|
2020-10-07 20:53:22 +00:00
|
|
|
for i, unAckedMessage := range g.UnacknowledgedMessages {
|
2019-10-18 23:56:10 +00:00
|
|
|
if compareSignatures(unAckedMessage.Signature, sig) {
|
|
|
|
message = &unAckedMessage
|
2020-10-07 20:53:22 +00:00
|
|
|
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
|
2019-10-18 23:56:10 +00:00
|
|
|
|
|
|
|
message.Error = error
|
|
|
|
g.Timeline.Insert(message)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-05-16 20:18:47 +00:00
|
|
|
// AddMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
|
2020-07-14 00:46:05 +00:00
|
|
|
func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*Message, bool) {
|
2019-02-20 20:03:04 +00:00
|
|
|
|
|
|
|
g.lock.Lock()
|
|
|
|
defer g.lock.Unlock()
|
|
|
|
|
|
|
|
// Delete the message from the unack'd buffer if it exists
|
2020-10-07 20:53:22 +00:00
|
|
|
for i, unAckedMessage := range g.UnacknowledgedMessages {
|
2019-02-20 20:03:04 +00:00
|
|
|
if compareSignatures(unAckedMessage.Signature, sig) {
|
2020-10-07 20:53:22 +00:00
|
|
|
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
|
2019-02-20 20:03:04 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-31 19:33:32 +00:00
|
|
|
timelineMessage := &Message{
|
2020-07-14 00:46:05 +00:00
|
|
|
Message: message.Text,
|
|
|
|
Timestamp: time.Unix(int64(message.Timestamp), 0),
|
2018-05-09 19:09:00 +00:00
|
|
|
Received: time.Now(),
|
2018-06-22 18:11:23 +00:00
|
|
|
Signature: sig,
|
2020-07-14 00:46:05 +00:00
|
|
|
PeerID: message.Onion,
|
|
|
|
PreviousMessageSig: message.PreviousMessageSig,
|
2019-10-18 23:56:10 +00:00
|
|
|
ReceivedByServer: true,
|
|
|
|
Error: "",
|
2020-12-19 01:03:35 +00:00
|
|
|
Acknowledged: true,
|
2018-03-15 20:53:22 +00:00
|
|
|
}
|
2019-02-03 03:24:42 +00:00
|
|
|
seen := g.Timeline.Insert(timelineMessage)
|
2019-02-20 20:03:04 +00:00
|
|
|
|
2019-02-03 03:24:42 +00:00
|
|
|
return timelineMessage, seen
|
2018-03-15 20:53:22 +00:00
|
|
|
}
|
|
|
|
|
2018-12-03 19:12:34 +00:00
|
|
|
// GetTimeline provides a safe copy of the timeline
|
|
|
|
func (g *Group) GetTimeline() (timeline []Message) {
|
2018-05-06 04:18:00 +00:00
|
|
|
g.lock.Lock()
|
2018-12-03 19:12:34 +00:00
|
|
|
defer g.lock.Unlock()
|
2020-10-07 20:53:22 +00:00
|
|
|
return append(g.Timeline.GetMessages(), g.UnacknowledgedMessages...)
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
|
|
|
|
2018-03-15 16:33:26 +00:00
|
|
|
//EncryptMessage takes a message and encrypts the message under the group key.
|
2020-07-14 00:46:05 +00:00
|
|
|
func (g *Group) EncryptMessage(message *groups.DecryptedGroupMessage) ([]byte, error) {
|
2018-03-09 20:44:13 +00:00
|
|
|
var nonce [24]byte
|
|
|
|
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
2018-12-04 02:52:11 +00:00
|
|
|
log.Errorf("Cannot read from random: %v\n", err)
|
2018-09-27 00:08:54 +00:00
|
|
|
return nil, err
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
2020-07-14 00:46:05 +00:00
|
|
|
wire, err := json.Marshal(message)
|
2019-11-08 00:39:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-03-15 20:53:22 +00:00
|
|
|
encrypted := secretbox.Seal(nonce[:], []byte(wire), &nonce, &g.GroupKey)
|
2018-09-27 00:08:54 +00:00
|
|
|
return encrypted, nil
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
|
|
|
|
2018-03-15 16:33:26 +00:00
|
|
|
// DecryptMessage takes a ciphertext and returns true and the decrypted message if the
|
|
|
|
// cipher text can be successfully decrypted,else false.
|
2020-07-14 00:46:05 +00:00
|
|
|
func (g *Group) DecryptMessage(ciphertext []byte) (bool, *groups.DecryptedGroupMessage) {
|
2019-11-08 00:39:27 +00:00
|
|
|
if len(ciphertext) > 24 {
|
|
|
|
var decryptNonce [24]byte
|
|
|
|
copy(decryptNonce[:], ciphertext[:24])
|
|
|
|
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &g.GroupKey)
|
|
|
|
if ok {
|
2020-07-14 00:46:05 +00:00
|
|
|
dm := &groups.DecryptedGroupMessage{}
|
|
|
|
err := json.Unmarshal(decrypted, dm)
|
2019-11-08 00:39:27 +00:00
|
|
|
if err == nil {
|
|
|
|
return true, dm
|
|
|
|
}
|
2018-03-15 20:53:22 +00:00
|
|
|
}
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
2018-03-15 20:53:22 +00:00
|
|
|
return false, nil
|
2018-03-09 20:44:13 +00:00
|
|
|
}
|
2018-11-02 23:43:40 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2021-05-14 18:26:04 +00:00
|
|
|
|
|
|
|
// ValidateInvite takes in a serialized invite and returns the invite structure if it is cryptographically valid
|
|
|
|
// and an error if it is not
|
|
|
|
func ValidateInvite(invite string) (*groups.GroupInvite, error) {
|
|
|
|
// We prefix invites for groups with torv3
|
|
|
|
if strings.HasPrefix(invite, GroupInvitePrefix) {
|
2021-05-18 19:23:13 +00:00
|
|
|
data, err := base64.StdEncoding.DecodeString(invite[len(GroupInvitePrefix):])
|
2021-05-14 18:26:04 +00:00
|
|
|
if err == nil {
|
|
|
|
// First attempt to unmarshal the json...
|
|
|
|
var gci groups.GroupInvite
|
|
|
|
err := json.Unmarshal(data, &gci)
|
|
|
|
if err == nil {
|
|
|
|
|
|
|
|
// Validate the Invite by first checking that the server is a valid v3 onion
|
2021-06-02 18:34:57 +00:00
|
|
|
if !tor.IsValidHostname(gci.ServerHost) {
|
2021-05-14 18:26:04 +00:00
|
|
|
return nil, errors.New("server is not a valid v3 onion")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the length of the shared key...
|
|
|
|
if len(gci.SharedKey) != 32 {
|
|
|
|
return nil, errors.New("key length is not 32 bytes")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Derive the servers public key (we can ignore the error checking here because it's already been
|
|
|
|
// done by IsValidHostname, and check that we derive the same groupID...
|
|
|
|
derivedGroupID := deriveGroupID(gci.SharedKey, gci.ServerHost)
|
|
|
|
if derivedGroupID != gci.GroupID {
|
|
|
|
return nil, errors.New("group id is invalid")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Replace the original with the derived, this should be a no-op at this point but defense in depth...
|
|
|
|
gci.GroupID = derivedGroupID
|
|
|
|
return &gci, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.New("invite has invalid structure")
|
|
|
|
}
|