Merge pull request 'Replace old GroupID with new Dervied GroupID' (#357) from groupwiring into master
Reviewed-on: #357
This commit is contained in:
commit
0957aefdff
|
@ -66,6 +66,10 @@ const (
|
||||||
//TimestampReceived, TimestampSent, Data(Message), GroupID, Signature, PreviousSignature, RemotePeer
|
//TimestampReceived, TimestampSent, Data(Message), GroupID, Signature, PreviousSignature, RemotePeer
|
||||||
NewMessageFromGroup = Type("NewMessageFromGroup")
|
NewMessageFromGroup = Type("NewMessageFromGroup")
|
||||||
|
|
||||||
|
// Sent if a Group Key is detected as being used outside of expected parameters (e.g. with tampered signatures)
|
||||||
|
// GroupID: The ID of the Group that is presumed compromised
|
||||||
|
GroupCompromised = Type("GroupCompromised")
|
||||||
|
|
||||||
// an error was encountered trying to send a particular Message to a group
|
// an error was encountered trying to send a particular Message to a group
|
||||||
// attributes:
|
// attributes:
|
||||||
// GroupServer: The server the Message was sent to
|
// GroupServer: The server the Message was sent to
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module cwtch.im/cwtch
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.openprivacy.ca/cwtch.im/tapir v0.4.0
|
git.openprivacy.ca/cwtch.im/tapir v0.4.1
|
||||||
git.openprivacy.ca/openprivacy/connectivity v1.4.3
|
git.openprivacy.ca/openprivacy/connectivity v1.4.3
|
||||||
git.openprivacy.ca/openprivacy/log v1.0.2
|
git.openprivacy.ca/openprivacy/log v1.0.2
|
||||||
github.com/gtank/ristretto255 v0.1.2
|
github.com/gtank/ristretto255 v0.1.2
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -10,6 +10,8 @@ git.openprivacy.ca/cwtch.im/tapir v0.3.5 h1:AlqAhluY4ivznGoHh37Khyxy0u9IbtYskP93
|
||||||
git.openprivacy.ca/cwtch.im/tapir v0.3.5/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
git.openprivacy.ca/cwtch.im/tapir v0.3.5/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
||||||
git.openprivacy.ca/cwtch.im/tapir v0.4.0 h1:clG8uORt0NKEhT4P+Dpw1pzyUuYzYBMevGqn2pciKk8=
|
git.openprivacy.ca/cwtch.im/tapir v0.4.0 h1:clG8uORt0NKEhT4P+Dpw1pzyUuYzYBMevGqn2pciKk8=
|
||||||
git.openprivacy.ca/cwtch.im/tapir v0.4.0/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
git.openprivacy.ca/cwtch.im/tapir v0.4.0/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
||||||
|
git.openprivacy.ca/cwtch.im/tapir v0.4.1 h1:9LMpQX41IzecNNlRc1FZKXHg6wlFss679tFsa3vzb3Y=
|
||||||
|
git.openprivacy.ca/cwtch.im/tapir v0.4.1/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
||||||
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
|
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
|
||||||
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
|
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
|
||||||
git.openprivacy.ca/openprivacy/connectivity v1.4.0 h1:c7AANUCrlA4hIqXxIGDOWMtSe8CpDleD1877PShScbM=
|
git.openprivacy.ca/openprivacy/connectivity v1.4.0 h1:c7AANUCrlA4hIqXxIGDOWMtSe8CpDleD1877PShScbM=
|
||||||
|
|
|
@ -1,29 +1,38 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha512"
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
"cwtch.im/cwtch/protocol/groups"
|
"cwtch.im/cwtch/protocol/groups"
|
||||||
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date
|
// 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 = 2
|
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
|
// 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.
|
// tied to a server under a given group key. Each group has a set of Messages.
|
||||||
type Group struct {
|
type Group struct {
|
||||||
|
// GroupID is now derived from the GroupKey and the GroupServer
|
||||||
GroupID string
|
GroupID string
|
||||||
SignedGroupID []byte
|
|
||||||
GroupKey [32]byte
|
GroupKey [32]byte
|
||||||
GroupServer string
|
GroupServer string
|
||||||
Timeline Timeline `json:"-"`
|
Timeline Timeline `json:"-"`
|
||||||
|
@ -62,6 +71,11 @@ func NewGroup(server string) (*Group, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
copy(group.GroupKey[:], groupKey[:])
|
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)
|
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
|
// 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.
|
// codes around invite, we assume that this is always set.
|
||||||
|
@ -69,13 +83,20 @@ func NewGroup(server string) (*Group, error) {
|
||||||
return group, nil
|
return group, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignGroup adds a signature to the group.
|
// CheckGroup returns true only if the ID of the group is cryptographically valid.
|
||||||
func (g *Group) SignGroup(signature []byte) {
|
func (g *Group) CheckGroup() bool {
|
||||||
g.SignedGroupID = signature
|
return g.GroupID == deriveGroupID(g.GroupKey[:], g.GroupServer)
|
||||||
copy(g.Timeline.SignedGroupID[:], g.SignedGroupID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compromised should be called if we detect a a groupkey leak.
|
// 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() {
|
func (g *Group) Compromised() {
|
||||||
g.IsCompromised = true
|
g.IsCompromised = true
|
||||||
}
|
}
|
||||||
|
@ -83,20 +104,15 @@ func (g *Group) Compromised() {
|
||||||
// Invite generates a invitation that can be sent to a cwtch peer
|
// Invite generates a invitation that can be sent to a cwtch peer
|
||||||
func (g *Group) Invite() (string, error) {
|
func (g *Group) Invite() (string, error) {
|
||||||
|
|
||||||
if g.SignedGroupID == nil {
|
|
||||||
return "", errors.New("group isn't signed")
|
|
||||||
}
|
|
||||||
|
|
||||||
gci := &groups.GroupInvite{
|
gci := &groups.GroupInvite{
|
||||||
GroupID: g.GroupID,
|
GroupID: g.GroupID,
|
||||||
GroupName: g.Attributes[attr.GetLocalScope("name")],
|
GroupName: g.Attributes[attr.GetLocalScope("name")],
|
||||||
SharedKey: g.GroupKey[:],
|
SharedKey: g.GroupKey[:],
|
||||||
ServerHost: g.GroupServer,
|
ServerHost: g.GroupServer,
|
||||||
SignedGroupID: g.SignedGroupID[:],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
invite, err := json.Marshal(gci)
|
invite, err := json.Marshal(gci)
|
||||||
serializedInvite := fmt.Sprintf("torv3%v", base64.StdEncoding.EncodeToString(invite))
|
serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite))
|
||||||
return serializedInvite, err
|
return serializedInvite, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,3 +237,41 @@ func (g *Group) GetAttribute(name string) (value string, exists bool) {
|
||||||
value, exists = g.Attributes[name]
|
value, exists = g.Attributes[name]
|
||||||
return
|
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[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) == 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")
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"cwtch.im/cwtch/protocol/groups"
|
"cwtch.im/cwtch/protocol/groups"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -16,6 +19,23 @@ func TestGroup(t *testing.T) {
|
||||||
PreviousMessageSig: []byte{},
|
PreviousMessageSig: []byte{},
|
||||||
Padding: []byte{},
|
Padding: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invite, err := g.Invite()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating group invite: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
validatedInvite, err := ValidateInvite(invite)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error validating group invite: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if validatedInvite.GroupID != g.GroupID {
|
||||||
|
t.Fatalf("after validate group invite id should be identical to original: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
encMessage, _ := g.EncryptMessage(dgm)
|
encMessage, _ := g.EncryptMessage(dgm)
|
||||||
ok, message := g.DecryptMessage(encMessage)
|
ok, message := g.DecryptMessage(encMessage)
|
||||||
if !ok || message.Text != "Hello World!" {
|
if !ok || message.Text != "Hello World!" {
|
||||||
|
@ -36,3 +56,60 @@ func TestGroupErr(t *testing.T) {
|
||||||
t.Errorf("Group Setup Should Have Failed")
|
t.Errorf("Group Setup Should Have Failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test various group invite validation failures...
|
||||||
|
func TestGroupValidation(t *testing.T) {
|
||||||
|
|
||||||
|
group := &Group{
|
||||||
|
GroupID: "",
|
||||||
|
GroupKey: [32]byte{},
|
||||||
|
GroupServer: "",
|
||||||
|
Timeline: Timeline{},
|
||||||
|
Accepted: false,
|
||||||
|
IsCompromised: false,
|
||||||
|
Attributes: nil,
|
||||||
|
lock: sync.Mutex{},
|
||||||
|
LocalID: "",
|
||||||
|
State: "",
|
||||||
|
UnacknowledgedMessages: nil,
|
||||||
|
Version: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
invite, _ := group.Invite()
|
||||||
|
_, err := ValidateInvite(invite)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Group with empty group id should have been an error")
|
||||||
|
}
|
||||||
|
t.Logf("Error: %v", err)
|
||||||
|
|
||||||
|
// Generate a valid group but replace the group server...
|
||||||
|
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||||
|
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
|
||||||
|
invite, _ = group.Invite()
|
||||||
|
_, err = ValidateInvite(invite)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Group with empty group id should have been an error")
|
||||||
|
}
|
||||||
|
t.Logf("Error: %v", err)
|
||||||
|
|
||||||
|
// Generate a valid group but replace the group key...
|
||||||
|
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||||
|
group.GroupKey = sha256.Sum256([]byte{})
|
||||||
|
invite, _ = group.Invite()
|
||||||
|
_, err = ValidateInvite(invite)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Group with different group key should have errored")
|
||||||
|
}
|
||||||
|
t.Logf("Error: %v", err)
|
||||||
|
|
||||||
|
// mangle the invite
|
||||||
|
_, err = ValidateInvite(strings.ReplaceAll(invite, GroupInvitePrefix, ""))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Group with different group key should have errored")
|
||||||
|
}
|
||||||
|
t.Logf("Error: %v", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"cwtch.im/cwtch/protocol/groups"
|
"cwtch.im/cwtch/protocol/groups"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -304,20 +303,34 @@ func (p *Profile) GetContact(onion string) (*PublicProfile, bool) {
|
||||||
return contact, ok
|
return contact, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyGroupMessage confirms the authenticity of a message given an onion, message and signature.
|
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, ciphertext and signature.
|
||||||
func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, timestamp int32, ciphertext []byte, signature []byte) bool {
|
// 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 (p *Profile) VerifyGroupMessage(onion string, groupID string, ciphertext []byte, signature []byte) bool {
|
||||||
|
|
||||||
group := p.GetGroup(groupID)
|
group := p.GetGroup(groupID)
|
||||||
if group == nil {
|
if group == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We use our group id, a known reference server and the ciphertext of the message.
|
||||||
|
m := groupID + group.GroupServer + string(ciphertext)
|
||||||
|
|
||||||
|
// If the message is ostensibly from us then we check it against our public key...
|
||||||
if onion == p.Onion {
|
if onion == p.Onion {
|
||||||
m := groupID + group.GroupServer + string(ciphertext)
|
|
||||||
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
|
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
m := groupID + group.GroupServer + string(ciphertext)
|
// Otherwise we derive the public key from the sender and check it against that.
|
||||||
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
||||||
if err == nil && len(decodedPub) >= 32 {
|
if err == nil && len(decodedPub) >= 32 {
|
||||||
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
|
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
|
||||||
|
@ -339,8 +352,6 @@ func (p *Profile) StartGroup(server string) (groupID string, invite string, err
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
groupID = group.GroupID
|
groupID = group.GroupID
|
||||||
signedGroupID := p.SignMessage(groupID + server)
|
|
||||||
group.SignGroup(signedGroupID)
|
|
||||||
invite, err = group.Invite()
|
invite, err = group.Invite()
|
||||||
p.lock.Lock()
|
p.lock.Lock()
|
||||||
defer p.lock.Unlock()
|
defer p.lock.Unlock()
|
||||||
|
@ -356,29 +367,23 @@ func (p *Profile) GetGroup(groupID string) (g *Group) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessInvite adds a new group invite to the profile. returns the new group ID
|
// ProcessInvite validates a group invite and adds a new group invite to the profile if it is valid.
|
||||||
|
// returns the new group ID on success, error on fail.
|
||||||
func (p *Profile) ProcessInvite(invite string) (string, error) {
|
func (p *Profile) ProcessInvite(invite string) (string, error) {
|
||||||
if strings.HasPrefix(invite, "torv3") {
|
gci, err := ValidateInvite(invite)
|
||||||
data, err := base64.StdEncoding.DecodeString(invite[5:])
|
if err == nil {
|
||||||
if err == nil {
|
group := new(Group)
|
||||||
var gci groups.GroupInvite
|
group.Version = CurrentGroupVersion
|
||||||
err := json.Unmarshal(data, &gci)
|
group.GroupID = gci.GroupID
|
||||||
if err == nil {
|
group.LocalID = GenerateRandomID()
|
||||||
group := new(Group)
|
copy(group.GroupKey[:], gci.SharedKey[:])
|
||||||
group.Version = CurrentGroupVersion
|
group.GroupServer = gci.ServerHost
|
||||||
group.GroupID = gci.GroupID
|
group.Accepted = false
|
||||||
group.LocalID = GenerateRandomID()
|
group.Attributes = make(map[string]string)
|
||||||
group.SignedGroupID = gci.SignedGroupID
|
p.AddGroup(group)
|
||||||
copy(group.GroupKey[:], gci.SharedKey[:])
|
return gci.GroupID, nil
|
||||||
group.GroupServer = gci.ServerHost
|
|
||||||
group.Accepted = false
|
|
||||||
group.Attributes = make(map[string]string)
|
|
||||||
p.AddGroup(group)
|
|
||||||
return group.GroupID, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return "", errors.New("unsupported exported group type")
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddGroup is a convenience method for adding a group to a profile.
|
// AddGroup is a convenience method for adding a group to a profile.
|
||||||
|
@ -397,7 +402,7 @@ func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool,
|
||||||
for _, group := range p.Groups {
|
for _, group := range p.Groups {
|
||||||
success, dgm := group.DecryptMessage(ciphertext)
|
success, dgm := group.DecryptMessage(ciphertext)
|
||||||
if success {
|
if success {
|
||||||
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, dgm.Text, int32(dgm.Timestamp), ciphertext, signature)
|
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, ciphertext, signature)
|
||||||
|
|
||||||
// So we have a message that has a valid group key, but the signature can't be verified.
|
// 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)
|
// 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)
|
||||||
|
@ -437,21 +442,26 @@ func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte,
|
||||||
if group != nil {
|
if group != nil {
|
||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
|
|
||||||
|
// Select the latest message from the timeline as a reference point.
|
||||||
var prevSig []byte
|
var prevSig []byte
|
||||||
if len(group.Timeline.Messages) > 0 {
|
if len(group.Timeline.Messages) > 0 {
|
||||||
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
|
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
|
||||||
} else {
|
} else {
|
||||||
prevSig = group.SignedGroupID
|
prevSig = []byte(group.GroupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
lenPadding := MaxGroupMessageLength - len(message)
|
lenPadding := MaxGroupMessageLength - len(message)
|
||||||
padding := make([]byte, lenPadding)
|
padding := make([]byte, lenPadding)
|
||||||
getRandomness(&padding)
|
getRandomness(&padding)
|
||||||
|
hexGroupID, err := hex.DecodeString(group.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
dm := &groups.DecryptedGroupMessage{
|
dm := &groups.DecryptedGroupMessage{
|
||||||
Onion: p.Onion,
|
Onion: p.Onion,
|
||||||
Text: message,
|
Text: message,
|
||||||
SignedGroupID: group.SignedGroupID[:],
|
SignedGroupID: hexGroupID,
|
||||||
Timestamp: uint64(timestamp),
|
Timestamp: uint64(timestamp),
|
||||||
PreviousMessageSig: prevSig,
|
PreviousMessageSig: prevSig,
|
||||||
Padding: padding[:],
|
Padding: padding[:],
|
||||||
|
|
|
@ -688,6 +688,12 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.TimestampReceived: message.Received.Format(time.RFC3339Nano), event.TimestampSent: message.Timestamp.Format(time.RFC3339Nano), event.Data: message.Message, event.GroupID: groupID, event.Signature: base64.StdEncoding.EncodeToString(message.Signature), event.PreviousSignature: base64.StdEncoding.EncodeToString(message.PreviousMessageSig), event.RemotePeer: message.PeerID}))
|
cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.TimestampReceived: message.Received.Format(time.RFC3339Nano), event.TimestampSent: message.Timestamp.Format(time.RFC3339Nano), event.Data: message.Message, event.GroupID: groupID, event.Signature: base64.StdEncoding.EncodeToString(message.Signature), event.PreviousSignature: base64.StdEncoding.EncodeToString(message.PreviousMessageSig), event.RemotePeer: message.PeerID}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The group has been compromised
|
||||||
|
if !ok && groupID != "" {
|
||||||
|
if cp.Profile.GetGroup(groupID).IsCompromised {
|
||||||
|
cp.eventBus.Publish(event.NewEvent(event.GroupCompromised, map[event.Field]string{event.GroupID: groupID}))
|
||||||
|
}
|
||||||
|
}
|
||||||
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
|
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
|
||||||
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
||||||
cp.StoreMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
|
cp.StoreMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
|
||||||
|
|
|
@ -21,9 +21,13 @@ type GroupInvite struct {
|
||||||
|
|
||||||
// DecryptedGroupMessage is the main encapsulation of group message data
|
// DecryptedGroupMessage is the main encapsulation of group message data
|
||||||
type DecryptedGroupMessage struct {
|
type DecryptedGroupMessage struct {
|
||||||
Text string
|
Text string
|
||||||
Onion string
|
Onion string
|
||||||
Timestamp uint64
|
Timestamp uint64
|
||||||
|
// NOTE: SignedGroupID is now a misnomer, the only way this is signed is indirectly via the signed encrypted group messages
|
||||||
|
// We now treat GroupID as binding to a server/key rather than an "owner" - additional validation logic (to e.g.
|
||||||
|
// respect particular group constitutions) can be built on top of group messages, but the underlying groups are
|
||||||
|
// now agnostic to those models.
|
||||||
SignedGroupID []byte
|
SignedGroupID []byte
|
||||||
PreviousMessageSig []byte
|
PreviousMessageSig []byte
|
||||||
Padding []byte
|
Padding []byte
|
||||||
|
|
|
@ -73,7 +73,6 @@ func main() {
|
||||||
|
|
||||||
// TODO create a random group for testing
|
// TODO create a random group for testing
|
||||||
group, _ := model.NewGroup(tor.GetTorV3Hostname(serverConfig.PublicKey))
|
group, _ := model.NewGroup(tor.GetTorV3Hostname(serverConfig.PublicKey))
|
||||||
group.SignGroup([]byte{})
|
|
||||||
invite, err := group.Invite()
|
invite, err := group.Invite()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
Loading…
Reference in New Issue