Browse Source

Group Experiment: Do not require GroupID to be Secret

Sarah Jamie Lewis 4 weeks ago
  1. 9
  2. 38


@ -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)


@ -5,6 +5,7 @@ import (
@ -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