From 026a00f1715536fef6c00f83b1eb461a08e567a6 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 25 Aug 2021 12:16:50 -0700 Subject: [PATCH] Group Experiment: Do not require GroupID to be Secret --- model/group.go | 9 +-------- model/profile.go | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/model/group.go b/model/group.go index d537deb..92146ba 100644 --- a/model/group.go +++ b/model/group.go @@ -23,7 +23,7 @@ import ( ) // 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 +const CurrentGroupVersion = 4 // GroupInvitePrefix identifies a particular string as being a serialized group invite. const GroupInvitePrefix = "torv3" @@ -58,13 +58,6 @@ func NewGroup(server string) (*Group, error) { 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) diff --git a/model/profile.go b/model/profile.go index be05f55..3ce9668 100644 --- a/model/profile.go +++ b/model/profile.go @@ -5,6 +5,7 @@ import ( "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/protocol/groups" "encoding/base32" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -322,7 +323,7 @@ func (p *Profile) GetContact(onion string) (*PublicProfile, bool) { return contact, ok } -// VerifyGroupMessage confirms the authenticity of a message given an sender onion, ciphertext and signature. +// 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) @@ -334,7 +335,7 @@ func (p *Profile) GetContact(onion string) (*PublicProfile, bool) { // 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 (p *Profile) VerifyGroupMessage(onion string, groupID string, ciphertext []byte, signature []byte) bool { +func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, signature []byte) bool { group := p.GetGroup(groupID) if group == nil { @@ -342,7 +343,7 @@ func (p *Profile) VerifyGroupMessage(onion string, groupID string, ciphertext [] } // We use our group id, a known reference server and the ciphertext of the message. - m := groupID + group.GroupServer + string(ciphertext) + m := groupID + group.GroupServer + message // If the message is ostensibly from us then we check it against our public key... if onion == p.Onion { @@ -425,7 +426,33 @@ func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool, for _, group := range p.Groups { success, dgm := group.DecryptMessage(ciphertext) if success { - verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, ciphertext, signature) + + // 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, group.GroupID, nil, false + } + + // 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 := p.VerifyGroupMessage(dgm.Onion, group.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 group.Version == 3 { + verified = p.VerifyGroupMessage(dgm.Onion, group.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) @@ -494,7 +521,8 @@ func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte, if err != nil { return nil, nil, err } - signature := p.SignMessage(groupID + group.GroupServer + string(ciphertext)) + serialized, _ := json.Marshal(dm) + signature := p.SignMessage(groupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized)) group.AddSentMessage(dm, signature) return ciphertext, signature, nil }