cwtch/model/group.go

229 lines
7.4 KiB
Go
Raw Normal View History

2018-03-09 20:44:13 +00:00
package model
import (
"crypto/ed25519"
2018-03-09 20:44:13 +00:00
"crypto/rand"
"crypto/sha512"
2020-07-14 00:46:05 +00:00
"cwtch.im/cwtch/protocol/groups"
"encoding/base32"
2021-05-03 23:32:48 +00:00
"encoding/base64"
"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"
"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"
"golang.org/x/crypto/pbkdf2"
2018-03-30 21:16:51 +00:00
"io"
"strings"
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
const CurrentGroupVersion = 4
// 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 {
// GroupID is now derived from the GroupKey and the GroupServer
GroupID string
GroupKey [32]byte
GroupServer string
Version int
Timeline Timeline `json:"-"`
LocalID string
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
func NewGroup(server string) (*Group, error) {
2018-03-09 20:44:13 +00:00
group := new(Group)
2021-06-02 18:34:57 +00:00
if !tor.IsValidHostname(server) {
return nil, errors.New("server is not a valid v3 onion")
}
2018-03-09 20:44:13 +00:00
group.GroupServer = server
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)
return nil, err
2018-03-09 20:44:13 +00:00
}
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)
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.
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-05-16 20:18:47 +00:00
// Invite generates a invitation that can be sent to a cwtch peer
func (g *Group) Invite(name string) (string, error) {
2018-05-16 20:18:47 +00:00
2020-07-14 00:46:05 +00:00
gci := &groups.GroupInvite{
GroupID: g.GroupID,
GroupName: name,
SharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
}
2020-07-14 00:46:05 +00:00
invite, err := json.Marshal(gci)
serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite))
2021-05-03 23:32:48 +00:00
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.Timeline.Insert(&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()
//
// return g.Timeline.SetSendError(sig, error)
//}
//// GetMessage returns the message at index `index` if it exists. Otherwise returns false.
//// This routine also returns the length of the timeline
//// If go has an optional type this would return Option<Message>...
//func (g *Group) GetMessage(index int) (bool, Message, int) {
// g.lock.Lock()
// defer g.lock.Unlock()
//
// length := len(g.Timeline.Messages)
//
// if length > index {
// return true, g.Timeline.Messages[index], length
// }
// return false, Message{}, length
//}
//// AddMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
//func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*Message, int) {
//
// g.lock.Lock()
// defer g.lock.Unlock()
//
// 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,
// }
// index := g.Timeline.Insert(timelineMessage)
//
// return timelineMessage, index
//}
//// GetTimeline provides a safe copy of the timeline
//func (g *Group) GetTimeline() (timeline []Message) {
// g.lock.Lock()
// defer g.lock.Unlock()
// return g.Timeline.GetMessages()
//}
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)
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
}
encrypted := secretbox.Seal(nonce[:], []byte(wire), &nonce, &g.GroupKey)
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-09 20:44:13 +00:00
}
return false, nil
2018-03-09 20:44:13 +00:00
}
2018-11-02 23:43:40 +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):])
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) {
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")
}