package model import ( "crypto/ed25519" "crypto/rand" "crypto/sha512" "cwtch.im/cwtch/protocol/groups" "encoding/base32" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/pbkdf2" "io" "strings" "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 = 4 // 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 GroupName string GroupKey [32]byte GroupServer string Attributes map[string]string //legacy to not use Version int Timeline Timeline `json:"-"` LocalID string } // NewGroup initializes a new group associated with a given CwtchServer func NewGroup(server string) (*Group, error) { group := new(Group) if !tor.IsValidHostname(server) { return nil, errors.New("server is not a valid v3 onion") } group.GroupServer = server 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. var err error group.GroupID, err = deriveGroupID(groupKey[:], server) return group, err } // CheckGroup returns true only if the ID of the group is cryptographically valid. func (g *Group) CheckGroup() bool { id, _ := deriveGroupID(g.GroupKey[:], g.GroupServer) return g.GroupID == id } // 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, error) { data, err := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname)) if err != nil { return "", err } pubkey := data[0:ed25519.PublicKeySize] return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New)), nil } // 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.GroupName, SharedKey: g.GroupKey[:], ServerHost: g.GroupServer, } invite, err := json.Marshal(gci) serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite)) return serializedInvite, err } // 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 } // 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[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 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") } // AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups. // If successful, adds the message to the group's timeline func (g *Group) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *groups.DecryptedGroupMessage) { success, dgm := g.DecryptMessage(ciphertext) // the second check here is not needed, but DecryptMessage violates the usual // go calling convention and we want static analysis tools to pick it up if success && dgm != nil { // Attempt to serialize this message serialized, err := json.Marshal(dgm) // Someone send a message that isn't a valid Decrypted Group Message. Since we require this struct in orer // to verify the message, we simply ignore it. if err != nil { return false, nil } // This now requires knowledge of the Sender, the Onion and the Specific Decrypted Group Message (which should only // be derivable from the cryptographic key) which contains many unique elements such as the time and random padding verified := g.VerifyGroupMessage(dgm.Onion, g.GroupID, base64.StdEncoding.EncodeToString(serialized), signature) if !verified { // An earlier version of this protocol mistakenly signed the ciphertext of the message // instead of the serialized decrypted group message. // This has 2 issues: // 1. A server with knowledge of group members public keys AND the Group ID would be able to detect valid messages // 2. It made the metadata-security of a group dependent on keeping the cryptographically derived Group ID secret. // While not awful, it also isn't good. For Version 3 groups only we permit Cwtch to check this older signature // structure in a backwards compatible way for the duration of the Groups Experiment. // TODO: Delete this check when Groups are no long Experimental if g.Version == 3 { verified = g.VerifyGroupMessage(dgm.Onion, g.GroupID, string(ciphertext), signature) } } // So we have a message that has a valid group key, but the signature can't be verified. // The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious) // Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised. if !verified { return false, nil } return true, dgm } // If we couldn't find a group to decrypt the message with we just return false. This is an expected case return false, nil } // VerifyGroupMessage confirms the authenticity of a message given an sender onion, message and signature. // The goal of this function is 2-fold: // 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least // knows the senders private key) // 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't // matter if we actually received this message from the server or from a hybrid protocol, all that matters is // that the sender and receivers agree that this message was intended for the group // // The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at // https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups // on two different servers with the same key and then forwards messages between them to convince the parties in // each group that they are actually in one big group (with the intent to later censor and/or selectively send messages // to each group). func (g *Group) VerifyGroupMessage(onion string, groupID string, message string, signature []byte) bool { // We use our group id, a known reference server and the ciphertext of the message. m := groupID + g.GroupServer + message // Otherwise we derive the public key from the sender and check it against that. decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion)) if err == nil && len(decodedPub) >= 32 { return ed25519.Verify(decodedPub[:32], []byte(m), signature) } return false } // EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and // profile func EncryptMessageToGroup(message string, author primitives.Identity, group *Group, prevSig string) ([]byte, []byte, *groups.DecryptedGroupMessage, error) { if len(message) > MaxGroupMessageLength { return nil, nil, nil, errors.New("group message is too long") } timestamp := time.Now().Unix() lenPadding := MaxGroupMessageLength - len(message) padding := make([]byte, lenPadding) getRandomness(&padding) hexGroupID, err := hex.DecodeString(group.GroupID) if err != nil { return nil, nil, nil, err } prevSigBytes, err := base64.StdEncoding.DecodeString(prevSig) if err != nil { return nil, nil, nil, err } dm := &groups.DecryptedGroupMessage{ Onion: author.Hostname(), Text: message, SignedGroupID: hexGroupID, Timestamp: uint64(timestamp), PreviousMessageSig: prevSigBytes, Padding: padding[:], } ciphertext, err := group.EncryptMessage(dm) if err != nil { return nil, nil, nil, err } serialized, _ := json.Marshal(dm) signature := author.Sign([]byte(group.GroupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized))) return ciphertext, signature, dm, nil }