package model import ( "crypto/ed25519" "crypto/rand" "crypto/sha512" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/protocol/groups" "encoding/base32" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/pbkdf2" "io" "strings" "sync" "time" ) // CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date const CurrentGroupVersion = 3 // GroupInvitePrefix identifies a particular string as being a serialized group invite. const GroupInvitePrefix = "torv3" // 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 is now derived from the GroupKey and the GroupServer GroupID string GroupKey [32]byte GroupServer string Timeline Timeline `json:"-"` Accepted bool IsCompromised bool Attributes map[string]string lock sync.Mutex LocalID string State string `json:"-"` UnacknowledgedMessages []Message Version int } // NewGroup initializes a new group associated with a given CwtchServer func NewGroup(server string) (*Group, error) { group := new(Group) group.Version = CurrentGroupVersion group.LocalID = GenerateRandomID() if tor.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[:]) // 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) group.Attributes = make(map[string]string) // 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 return group, nil } // 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) } // 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. func deriveGroupID(groupKey []byte, serverHostname string) string { data, _ := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname)) pubkey := data[0:ed25519.PublicKeySize] return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New)) } // Compromised should be called if we detect a groupkey leak func (g *Group) Compromised() { g.IsCompromised = true } // Invite generates a invitation that can be sent to a cwtch peer func (g *Group) Invite() (string, error) { gci := &groups.GroupInvite{ GroupID: g.GroupID, GroupName: g.Attributes[attr.GetLocalScope("name")], SharedKey: g.GroupKey[:], ServerHost: g.GroupServer, } invite, err := json.Marshal(gci) serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite)) return serializedInvite, err } // AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte) Message { g.lock.Lock() defer g.lock.Unlock() timelineMessage := Message{ Message: message.Text, Timestamp: time.Unix(int64(message.Timestamp), 0), Received: time.Unix(0, 0), Signature: sig, PeerID: message.Onion, PreviousMessageSig: message.PreviousMessageSig, 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 *groups.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.Text, Timestamp: time.Unix(int64(message.Timestamp), 0), Received: time.Now(), Signature: sig, PeerID: message.Onion, PreviousMessageSig: message.PreviousMessageSig, ReceivedByServer: true, Error: "", Acknowledged: true, } 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 *groups.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 := json.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, *groups.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 := &groups.DecryptedGroupMessage{} err := json.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 } // 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) { data, err := base64.StdEncoding.DecodeString(invite[5:]) 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 if tor.IsValidHostname(gci.ServerHost) == false { 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") }