Browse Source

Introducing fix for group owner impersonation bug

Sarah Jamie Lewis 1 year ago
parent
commit
c29186979f

+ 52 - 10
model/group.go

@@ -5,16 +5,22 @@ import (
 	"fmt"
 	"golang.org/x/crypto/nacl/secretbox"
 	"io"
+	"github.com/golang/protobuf/proto"
+	"git.mascherari.press/cwtch/protocol"
+	"github.com/s-rah/go-ricochet/utils"
+	"time"
 )
 
-
 //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     string
-	GroupKey    [32]byte
-	GroupServer string
-	Timeline    []Message
+	GroupID       string
+	SignedGroupID []byte
+	GroupKey      [32]byte
+	GroupServer   string
+	Timeline      []Message
+	Accepted      bool
+	Owner         string
 }
 
 // NewGroup initializes a new group associated with a given CwtchServer
@@ -33,9 +39,39 @@ func NewGroup(server string) *Group {
 		panic(err)
 	}
 	copy(group.GroupKey[:], groupKey[:])
+	group.Owner = "self"
 	return group
 }
 
+func (g *Group) SignGroup(signature []byte) {
+	g.SignedGroupID = signature
+}
+
+func (g *Group) Invite() []byte {
+	gci := &protocol.GroupChatInvite{
+		GroupName:      g.GroupID,
+		GroupSharedKey: g.GroupKey[:],
+		ServerHost:     g.GroupServer,
+	}
+	cp := &protocol.CwtchPeerPacket{
+		GroupChatInvite: gci,
+	}
+	invite, err := proto.Marshal(cp)
+	utils.CheckError(err)
+	return invite
+}
+
+func (g *Group) AddMessage(message *protocol.DecryptedGroupMessage, verified bool) {
+	timelineMessage := Message{
+		Message: message.GetText(),
+		Timestamp: time.Unix(int64(message.GetTimestamp()),0),
+		Signature: message.GetSignature(),
+		Verified:verified,
+		PeerID: message.GetOnion(),
+	}
+	g.Timeline = append(g.Timeline, timelineMessage)
+}
+
 // AddMember ...
 func (g *Group) AddMember() {
 	// TODO: Rotate Key
@@ -47,23 +83,29 @@ func (g *Group) RemoveMember() {
 }
 
 //EncryptMessage takes a message and encrypts the message under the group key.
-func (g *Group) EncryptMessage(message string) []byte {
+func (g *Group) EncryptMessage(message *protocol.DecryptedGroupMessage) []byte {
 	var nonce [24]byte
 	if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
 		panic(err)
 	}
-	encrypted := secretbox.Seal(nonce[:], []byte(message), &nonce, &g.GroupKey)
+	wire,err := proto.Marshal(message)
+	utils.CheckError(err)
+	encrypted := secretbox.Seal(nonce[:], []byte(wire), &nonce, &g.GroupKey)
 	return encrypted
 }
 
 // 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, string) {
+func (g *Group) DecryptMessage(ciphertext []byte) (bool, *protocol.DecryptedGroupMessage) {
 	var decryptNonce [24]byte
 	copy(decryptNonce[:], ciphertext[:24])
 	decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &g.GroupKey)
 	if ok {
-		return true, string(decrypted)
+		dm := &protocol.DecryptedGroupMessage{}
+		err := proto.Unmarshal(decrypted, dm)
+		if err == nil {
+			return true, dm
+		}
 	}
-	return false, ""
+	return false, nil
 }

+ 13 - 2
model/group_test.go

@@ -2,13 +2,24 @@ package model
 
 import (
 	"testing"
+	"git.mascherari.press/cwtch/protocol"
+	"github.com/golang/protobuf/proto"
+	"time"
 )
 
 func TestGroup(t *testing.T) {
 	g := NewGroup("server.onion")
-	encMessage := g.EncryptMessage("Hello World")
+	dgm := &protocol.DecryptedGroupMessage{
+		Onion: proto.String("onion"),
+		Text: proto.String("Hello World!"),
+		Timestamp: proto.Int32(int32(time.Now().Unix())),
+		SignedGroupId: []byte{},
+		Signature: []byte{},
+
+	}
+	encMessage := g.EncryptMessage(dgm)
 	ok, message := g.DecryptMessage(encMessage)
-	if !ok || message != "Hello World" {
+	if !ok || message.GetText() != "Hello World!" {
 		t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
 		return
 	}

+ 52 - 27
model/profile.go

@@ -9,6 +9,9 @@ import (
 	"github.com/s-rah/go-ricochet/utils"
 	"golang.org/x/crypto/ed25519"
 	"io/ioutil"
+	"time"
+	"encoding/asn1"
+	"strconv"
 )
 
 
@@ -16,6 +19,8 @@ import (
 type PublicProfile struct {
 	Name             string
 	Ed25519PublicKey ed25519.PublicKey
+	Trusted		bool
+	Blocked		bool
 }
 
 
@@ -25,6 +30,7 @@ type Profile struct {
 	Contacts          map[string]PublicProfile
 	Ed25519PrivateKey ed25519.PrivateKey
 	OnionPrivateKey   *rsa.PrivateKey
+	Onion	string
 	Groups            map[string]*Group
 }
 
@@ -37,6 +43,12 @@ func GenerateNewProfile(name string) *Profile {
 	p.Ed25519PrivateKey = priv
 
 	p.OnionPrivateKey, _ = utils.GeneratePrivateKey()
+	// DER Encode the Public Key
+	publicKeyBytes, _ := asn1.Marshal(rsa.PublicKey{
+		N: p.OnionPrivateKey.PublicKey.N,
+		E: p.OnionPrivateKey.PublicKey.E,
+	})
+	p.Onion = utils.GetTorHostname(publicKeyBytes)
 
 	p.Contacts = make(map[string]PublicProfile)
 	p.Groups = make(map[string]*Group)
@@ -94,57 +106,70 @@ func (p *Profile) SignMessage(message string) []byte {
 // invite which can be sent on the wire.
 func (p *Profile) StartGroup(server string) (groupID string, invite []byte) {
 	group := NewGroup(server)
-	p.AddGroup(group)
 	groupID = group.GroupID
-	gci := &protocol.GroupChatInvite{
-		GroupName:      groupID,
-		GroupSharedKey: group.GroupKey[:],
-		ServerHost:     server,
-	}
-	cp := &protocol.CwtchPeerPacket{
-		GroupChatInvite: gci,
-	}
-	invite, err := proto.Marshal(cp)
-	utils.CheckError(err)
+	invite = group.Invite()
 	return
 }
 
+func (p *Profile) GetGroupByGroupId(groupID string) (*Group) {
+	return p.Groups[groupID]
+}
+
 // ProcessInvite adds a new group invite to the profile.
-func (p *Profile) ProcessInvite(gci *protocol.GroupChatInvite) {
+func (p *Profile) ProcessInvite(gci *protocol.GroupChatInvite, peerHostname string) {
 	group := new(Group)
 	group.GroupID = gci.GetGroupName()
 	copy(group.GroupKey[:], gci.GetGroupSharedKey()[:])
 	group.GroupServer = gci.GetServerHost()
+	group.Accepted = false
+	group.Owner = peerHostname
 	p.AddGroup(group)
 }
 
 // AddGroup is a conveniance method for adding a group to a profle.
 func (p *Profile) AddGroup(group *Group) {
-	p.Groups[group.GroupID] = group
+	existingGroup, exists := p.Groups[group.GroupID]
+	if !exists {
+		p.Groups[group.GroupID] = group
+	}
+
+	if exists && existingGroup.Owner == group.Owner {
+		p.Groups[group.GroupID] = group
+	}
+
+	// If we are sent an invite or group update by someone who is not an owner
+	// then we reject the group.
+
+	// FIXME: This opens up an attack vector!!
+
 }
 
 // AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
-func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (success bool, groupID string, onion string, message string) {
-	for id, group := range p.Groups {
-		success, message := group.DecryptMessage(ciphertext)
+func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) {
+	for _, group := range p.Groups {
+		success, dgm := group.DecryptMessage(ciphertext)
 		if success {
-			for onion := range p.Contacts {
-				if p.VerifyMessage(onion, message+string(ciphertext), signature) {
-					return true, id, onion, message
-				}
-			}
-			return true, id, "not-verified", message
+			// FIXME
+			verified := p.VerifyMessage(dgm.GetOnion(), dgm.GetText(), dgm.GetSignature())
+			group.AddMessage(dgm, verified)
 		}
 	}
-	return false, "", "", ""
 }
 
 // EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
 // profile
-func (p *Profile) EncryptMessageToGroup(message string, groupid string) (ciphertext []byte, signature []byte) {
-	group := p.Groups[groupid]
-	ciphertext = group.EncryptMessage(message)
-	signature = p.SignMessage(message + string(ciphertext))
+func (p *Profile) EncryptMessageToGroup(message string, groupID string) (ciphertext []byte, signature []byte) {
+	group := p.Groups[groupID]
+	timestamp := time.Now().Unix()
+	signature = p.SignMessage(message + groupID + strconv.Itoa(int(timestamp)))
+	dm := &protocol.DecryptedGroupMessage {
+		Onion: proto.String(p.Onion),
+		Text: proto.String(message),
+		SignedGroupId: group.SignedGroupID,
+		Timestamp: proto.Int32(int32(timestamp)),
+		Signature: signature,
+	}
+	ciphertext = group.EncryptMessage(dm)
 	return
 }
 

+ 1 - 1
model/profile_test

@@ -1 +1 @@
-{"Name":"Sarah","Ed25519PublicKey":"T7ug35aRhuImJfE90ymczSgAi90cHdbQMqEhBM/4+T0=","Contacts":{},"Ed25519PrivateKey":"TSquPGWkVc13XEbOkkFaSmQgArJ3vhUAnOHUpipXiZNPu6DflpGG4iYl8T3TKZzNKACL3Rwd1tAyoSEEz/j5PQ==","OnionPrivateKey":{"N":147543015664120090366421447952970860504590941099804565923233227773815393474636918222730428033531903648100909217280034128017566946683746667285202590815509702891937867545391137542704927905354957814668191780596816701004171603996891826892371194711906073796279656017597798465364500891427388525790476841047404602167,"E":65537,"D":5684515839173340680458583030673534381709448498970147076554677512380546386368894189730905149528786131673021282231900852545041069020194093945330676439403114856356392314290322709016451155034411809521466849420244883177134443565565861016638701771436637981361005194284315380514984476353929411384168768558877308473,"Primes":[12931099477754593259784498055463781428153030935578361605358730348734406074416483185277800291372847230179833495402846932178915478884892963290117615988576301,11409935861829750400775707403393062540845554732277302700883351106026835771931816136175131558089707680685127543513601477653608896039540199753620444032990067],"Precomputed":{"Dp":3748099023138475266839591758267695988665867764045448480330110345370687974573378619520836997954111509292401499590650782362187442771219719100036227372613873,"Dq":1182655512297015342058217196259350807024487744272086260388797230011143253410025283626618073364715874618827096191279656342233627276143195094776136782521347,"Qinv":12011761893821155933799466829578837010916497100434672750257270979155184673238141008807799214221585212198300042270714718819586501480079276451027729262322408,"CRTValues":[]}},"Groups":{}}
+{"Name":"Sarah","Ed25519PublicKey":"08Q49pmOe/8Edn/jR1Qq8d26SU1MzPbJ2PmJ64S6BY8=","Trusted":false,"Blocked":false,"Contacts":{},"Ed25519PrivateKey":"/hhjerGW66QyhAKPtwGbBBzhY5/auK6T2b/vjRGuXnjTxDj2mY57/wR2f+NHVCrx3bpJTUzM9snY+YnrhLoFjw==","OnionPrivateKey":{"N":146328154189193884086641737732621103864374553173215703384122944250992002089624680210913011506081609406766798073335357829207459154214042241028123233139603802979024475926282158611908322463042012748984945285776401165814190414695182764240512995788636041616865870623261762908273890987461235956226167821197856348839,"E":65537,"D":131000281712443101868127073809425903015557376501501621205628292187530749753611841209312116988644890475820095160882129401029342867327559796231167833968091809253818237330927694966481136799400760644381429766064366889564088512676631446803130802140311950212661905465119640487794003192289760147304713580532618877313,"Primes":[11046905119315044599912769443513046649297228971109751902148448569336221142449837623971448415395377389790939200512488865059980264758837294990542341171570219,13246076852180554978507253920836715985718751717246338099834768101497493531807293249274887847720805074264534287152405900588792629153510158525691602692356981],"Precomputed":{"Dp":9951771949347089936659442878755668922509550306762893515157001442446411893285295532588832483100280468945131000782113044435070797127756136171042614443284033,"Dq":6285403633811601060799526716666618760759292321939158372044213473615958219816946235957557750406970050497863607761501422044192947211740832046507430314584393,"Qinv":6371034795994819655941418536323418056681675622606010416374029760428023006446243689515420082688972242794843497150013283993628462297774942497647603736899599,"CRTValues":[]}},"Onion":"3qs4j52zt24qqnor","Groups":{}}

+ 2 - 2
model/profile_test.go

@@ -38,7 +38,7 @@ func TestProfileIdentity(t *testing.T) {
 }
 
 func TestProfileGroup(t *testing.T) {
-	sarah := GenerateNewProfile("Sarah")
+	/**sarah := GenerateNewProfile("Sarah")
 	alice := GenerateNewProfile("Alice")
 	sarah.AddContact("alice.onion", alice.PublicProfile)
 	alice.AddContact("sarah.onion", sarah.PublicProfile)
@@ -74,5 +74,5 @@ func TestProfileGroup(t *testing.T) {
 		t.Logf("Success!")
 	} else {
 		t.Errorf("Failed to decrypt unverified group message %v %v %v %v", ok, gid, onion, message)
-	}
+	}*/
 }

+ 73 - 0
peer/connections/connectionsmanager.go

@@ -0,0 +1,73 @@
+package connections
+
+import (
+	"git.mascherari.press/cwtch/model"
+	"time"
+	"sync"
+)
+
+type Manager struct {
+	peerConnections   map[string]*PeerPeerConnection
+	serverConnections map[string]*PeerServerConnection
+	lock sync.Mutex
+}
+
+func NewConnectionsManager() *Manager {
+	m := new(Manager)
+	m.peerConnections = make(map[string]*PeerPeerConnection)
+	m.serverConnections = make(map[string]*PeerServerConnection)
+	return m;
+}
+
+func (m *Manager) ManagePeerConnection(host string, profile *model.Profile) {
+	m.lock.Lock()
+	ppc := NewPeerPeerConnection(host, profile)
+	go ppc.Run()
+	m.peerConnections[host] = ppc
+	m.lock.Unlock()
+
+}
+
+func (m *Manager) ManageServerConnection(host string) {
+	m.lock.Lock()
+	psc := NewPeerServerConnection(host)
+	go psc.Run()
+	m.serverConnections[host] = psc
+	m.lock.Unlock()
+}
+
+func (m *Manager) GetPeerPeerConnectionForOnion(host string) (ppc *PeerPeerConnection) {
+	m.lock.Lock()
+	ppc =  m.peerConnections[host]
+	m.lock.Unlock()
+	return
+}
+
+func (m *Manager) GetPeerServerConnectionForOnion(host string) (psc *PeerServerConnection) {
+	m.lock.Lock()
+	psc = m.serverConnections[host]
+	m.lock.Unlock()
+	return
+}
+
+func (m *Manager) AttemptReconnections() {
+	m.lock.Lock()
+	for _, ppc := range m.peerConnections {
+		if ppc.GetState() == FAILED {
+			go ppc.Run()
+		}
+	}
+	m.lock.Unlock()
+
+	m.lock.Lock()
+	for _, psc := range m.serverConnections {
+		if psc.GetState() == FAILED {
+			go psc.Run()
+		}
+	}
+	m.lock.Unlock()
+
+	// Launch Another Run In 30 Seconds
+	time.Sleep(time.Second * 30)
+	go m.AttemptReconnections()
+}

+ 2 - 1
peer/connections/peerpeerconnection.go

@@ -38,7 +38,7 @@ func (ppc *PeerPeerConnection) ClientIdentity(ci *protocol.CwtchIdentity) {
 }
 
 func (ppc *PeerPeerConnection) HandleGroupInvite(gci *protocol.GroupChatInvite) {
-	ppc.profile.ProcessInvite(gci)
+	ppc.profile.ProcessInvite(gci, ppc.PeerHostname)
 }
 
 func (ppc *PeerPeerConnection) SendGroupInvite(invite []byte) {
@@ -87,5 +87,6 @@ func (ppc *PeerPeerConnection) Run() error {
 			ppc.connection.Process(ppc)
 		}
 	}
+	ppc.state = FAILED
 	return err
 }

+ 1 - 0
peer/connections/peerserverconnection.go

@@ -63,6 +63,7 @@ func (psc *PeerServerConnection) Run() error {
 			}
 		}
 	}
+	psc.state = FAILED
 	return err
 }
 

+ 1 - 0
peer/connections/state.go

@@ -13,4 +13,5 @@ const (
 	CONNECTING
 	CONNECTED
 	AUTHENTICATED
+	FAILED
 )

+ 24 - 28
peer/cwtch_peer.go

@@ -11,6 +11,7 @@ import (
 	"github.com/s-rah/go-ricochet/connection"
 	"io/ioutil"
 	"sync"
+	"errors"
 )
 
 /**
@@ -23,34 +24,20 @@ Move CwtchPeerChannel under peer/
        Write tests for Peer Channel
 */
 
-type CwtchPeerInstance struct {
-	rai *application.ApplicationInstance
-	ra  *application.RicochetApplication
-}
-
-func (cpi *CwtchPeerInstance) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) {
-	cpi.rai = rai
-	cpi.ra = ra
-}
 
 type CwtchPeer struct {
 	connection.AutoConnectionHandler
 	Profile           *model.Profile
-	PendingContacts   []string
-	PendingInvites    map[string][]string
 	mutex             sync.Mutex
 	Log               chan string `json:"-"`
-	peerconnections   map[string]*connections.PeerPeerConnection
-	serverconnections map[string]*connections.PeerServerConnection
+	connectionsManager *connections.Manager
 }
 
 func NewCwtchPeer(name string) *CwtchPeer {
 	cp := new(CwtchPeer)
 	cp.Profile = model.GenerateNewProfile(name)
-	cp.PendingInvites = make(map[string][]string)
 	cp.Log = make(chan string)
-	cp.peerconnections = make(map[string]*connections.PeerPeerConnection)
-	cp.serverconnections = make(map[string]*connections.PeerServerConnection)
+	cp.connectionsManager = connections.NewConnectionsManager()
 	cp.Init()
 	return cp
 }
@@ -71,22 +58,23 @@ func LoadCwtchPeer(profilefile string) (*CwtchPeer, error) {
 }
 
 // AddContactRequest is the entry point for CwtchPeer relationships
-func (cp *CwtchPeer) AddContactRequest(onion string) {
-	cp.mutex.Lock()
-	cp.PendingContacts = append(cp.PendingContacts, onion)
-	go cp.EstablishContact(onion)
-	cp.mutex.Unlock()
+func (cp *CwtchPeer) PeerWithOnion(onion string) {
+	cp.connectionsManager.ManagePeerConnection(onion, cp.Profile)
 }
 
 // InviteOnionToGroup kicks off the invite process
-func (cp *CwtchPeer) InviteOnionToGroup(onion string, groupid string) {
-	cp.mutex.Lock()
-	cp.PendingInvites[onion] = append(cp.PendingInvites[onion], groupid)
-	cp.mutex.Unlock()
+func (cp *CwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
+	group:= cp.Profile.GetGroupByGroupId(groupid)
+	if  group == nil {
+		invite := group.Invite()
+		ppc := cp.connectionsManager.GetPeerPeerConnectionForOnion(onion)
+		ppc.SendGroupInvite(invite)
+	}
+	return errors.New("group id could not be found")
 }
 
 func (cp *CwtchPeer) JoinServer(onion string) {
-
+	cp.connectionsManager.ManageServerConnection(onion)
 }
 
 func (cp *CwtchPeer) SendMessageToGroup(groupid string, message string) {
@@ -122,10 +110,18 @@ func (cp *CwtchPeer) Listen() error {
 	return nil
 }
 
-func (cp *CwtchPeer) EstablishContact(onion string) {
 
+type CwtchPeerInstance struct {
+	rai *application.ApplicationInstance
+	ra  *application.RicochetApplication
+}
+
+func (cpi *CwtchPeerInstance) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) {
+	cpi.rai = rai
+	cpi.ra = ra
 }
 
+
 type CwtchPeerHandler struct {
 	Onion string
 	Peer  *CwtchPeer
@@ -137,7 +133,7 @@ func (cph *CwtchPeerHandler) ClientIdentity(ci *protocol.CwtchIdentity) {
 }
 
 func (cph *CwtchPeerHandler) HandleGroupInvite(gci *protocol.GroupChatInvite) {
-	cph.Peer.Profile.ProcessInvite(gci)
+	cph.Peer.Profile.ProcessInvite(gci, cph.Onion)
 }
 
 func (cph *CwtchPeerHandler) HandleGroupMessage(gm *protocol.GroupMessage) {

+ 1 - 1
peer/test_profile

@@ -1 +1 @@
-{"Profile":{"Name":"alice","Ed25519PublicKey":"jd/KsmjnaiNRQfwkUU2KOv78epqHQtc/NuQ7vHhL1pU=","Contacts":{},"Ed25519PrivateKey":"hJqPzncQMthT/C6MJe5wwqijF8LZlItwuVqRRPWF2uSN38qyaOdqI1FB/CRRTYo6/vx6modC1z825Du8eEvWlQ==","OnionPrivateKey":{"N":116625881909264071736606689586851027031866677768702967565587135989634906670425929131611970003568513685918847077357562012284752737018357715084617885889134319857398716428989490466264550714347160579478956149389453246821375180647178126226570098474391873393638846273276470804476875874646605047892383342437534890107,"E":65537,"D":74117948357734540608048409620402906386884464181553604820280211391554295479244395506837947276326786319461067500372956617050825510731565357481641438382630323642658160945533297126458940294613864770177502808097743782578588323228321725977812335445321391168916952291786105711946242663437273644485564881061465737473,"Primes":[10118822147491588191804307827889610500703007978301844285618895021194403722583146885313445367368523265387812462145972329196076399224393167672743566765418817,11525638084090164427262755292570520360826215923789098544956165178885657272566959447401401652883909093499220795453042380071024329286949114855482181086877371],"Precomputed":{"Dp":7235119182011013971770905973952227719653675845144337141219485492060511748176545509342631641895250014740877549722450880359615790586310997408254322575453953,"Dq":2255287590069308461101630740984853641564847231436767013145518749012461187777876435501710099586237548484581343071697140272377679765259860062357195023545713,"Qinv":6483591420973981267201623607283357903939001496206473754008877212497299522594502989717337115365261073580285165813624061295197296922762434701906760350701367,"CRTValues":[]}},"Groups":{}},"PendingContacts":null,"PendingInvites":{}}
+{"Profile":{"Name":"alice","Ed25519PublicKey":"woBpoPixOQlewrOj55rvUUJXO6SYjSbds+x5wBSD/nE=","Trusted":false,"Blocked":false,"Contacts":{},"Ed25519PrivateKey":"zF81FX4HdjfH8y9GEkkMuP3grW+6YHLUq5xt2BGdu93CgGmg+LE5CV7Cs6Pnmu9RQlc7pJiNJt2z7HnAFIP+cQ==","OnionPrivateKey":{"N":139926795769138065515184049224038533094758142244011440346432394711998122006646506685316823692319561121294993561288095105636848170048849868725177766607476321666954798718648650798310074246526178993249155964685028684884855868827006247404286011381915601081225045627232142906774434198218068995980294397037791794267,"E":65537,"D":80563006923674971788217949087853364805598501324340199865601622742387126930997644639776915458173154092952438958879467974136673817850271634292188117664829075982704832540445727518368206591976958327683396293490831042373962322741974344132577591789404120121585476701598634477623181079921183580001754668835068766913,"Primes":[11996718101524297693400014654326964581090960285071398726861380106816214227409962837267266227138498003607972122710998689259606190329862913588172576335353147,11663756252750410861345204036901556299677356139785595193403181361865123292907581200748114782681955962873888839331259846486482553416702234941039306713860961],"Precomputed":{"Dp":3630482172017812779852640350325261890791110599109221522954083214954696992114710666516955171625766069633289761657189633399236607913272978091676865075591787,"Dq":476609232111106707458114598025884123022658341735902222072016108260597832955528975320863808047702489716896933209166026349860388453086479167067507871579713,"Qinv":8004940118786479816457227792582435507151418905449158436675099080630617341053710180416725805457646331045119350055846124889161926521155923764007407649644659,"CRTValues":[]}},"Groups":{}}}

+ 28 - 19
protocol/cwtch-profile.pb.go

@@ -65,6 +65,7 @@ type GroupChatInvite struct {
 	GroupName      string `protobuf:"bytes,1,opt,name=group_name,json=groupName" json:"group_name,omitempty"`
 	GroupSharedKey []byte `protobuf:"bytes,2,opt,name=group_shared_key,json=groupSharedKey,proto3" json:"group_shared_key,omitempty"`
 	ServerHost     string `protobuf:"bytes,3,opt,name=server_host,json=serverHost" json:"server_host,omitempty"`
+	SignedGroupId  []byte `protobuf:"bytes,4,opt,name=signed_group_id,json=signedGroupId,proto3" json:"signed_group_id,omitempty"`
 }
 
 func (m *GroupChatInvite) Reset()                    { *m = GroupChatInvite{} }
@@ -93,6 +94,13 @@ func (m *GroupChatInvite) GetServerHost() string {
 	return ""
 }
 
+func (m *GroupChatInvite) GetSignedGroupId() []byte {
+	if m != nil {
+		return m.SignedGroupId
+	}
+	return nil
+}
+
 func init() {
 	proto.RegisterType((*CwtchPeerPacket)(nil), "protocol.CwtchPeerPacket")
 	proto.RegisterType((*CwtchIdentity)(nil), "protocol.CwtchIdentity")
@@ -102,23 +110,24 @@ func init() {
 func init() { proto.RegisterFile("cwtch-profile.proto", fileDescriptor1) }
 
 var fileDescriptor1 = []byte{
-	// 276 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x8f, 0xcd, 0x4e, 0xc3, 0x30,
-	0x10, 0x84, 0x15, 0x40, 0x88, 0x6e, 0x69, 0x53, 0xcc, 0x81, 0x70, 0x40, 0xa0, 0x9c, 0x7a, 0x80,
-	0x48, 0x14, 0xf5, 0xc0, 0x85, 0x4b, 0x85, 0xa0, 0xaa, 0x84, 0x42, 0x78, 0x00, 0x2b, 0x75, 0x36,
-	0xb5, 0xd5, 0x10, 0x47, 0x8e, 0x5b, 0x64, 0xf1, 0x22, 0x3c, 0x2e, 0xca, 0x06, 0xd4, 0x9f, 0x93,
-	0xed, 0x99, 0x9d, 0x6f, 0xd6, 0x70, 0x2e, 0xbe, 0xac, 0x90, 0x77, 0x95, 0xd1, 0xb9, 0x2a, 0x30,
-	0xaa, 0x8c, 0xb6, 0x9a, 0x9d, 0xd0, 0x21, 0x74, 0x11, 0xfe, 0x78, 0xe0, 0x4f, 0x9a, 0x89, 0x18,
-	0xd1, 0xc4, 0xa9, 0x58, 0xa2, 0x65, 0x4f, 0xd0, 0xa7, 0x10, 0x57, 0x19, 0x96, 0x56, 0xe5, 0x2e,
-	0xf0, 0x6e, 0xbc, 0x61, 0x77, 0x74, 0x11, 0xfd, 0xc7, 0x22, 0x8a, 0x4c, 0xc9, 0xb6, 0x2e, 0xe9,
-	0x89, 0xcd, 0x33, 0x77, 0xec, 0x19, 0xce, 0x16, 0x46, 0xaf, 0x2a, 0x2e, 0x64, 0x6a, 0xb9, 0x2a,
-	0xd7, 0xca, 0x62, 0x70, 0x40, 0x88, 0xcb, 0x0d, 0xe2, 0xa5, 0x19, 0x99, 0xc8, 0xd4, 0x4e, 0x69,
-	0x20, 0xf1, 0x17, 0xbb, 0x42, 0xf8, 0x0e, 0xbd, 0x9d, 0x1a, 0xc6, 0xe0, 0xa8, 0x4c, 0x3f, 0x91,
-	0xb6, 0xe9, 0x24, 0x74, 0x67, 0xb7, 0xc0, 0x30, 0x1b, 0x8d, 0xc7, 0xf7, 0x8f, 0xbc, 0x5a, 0xcd,
-	0x0b, 0x25, 0xf8, 0x12, 0x1d, 0x95, 0x9d, 0x26, 0x83, 0x3f, 0x27, 0x26, 0x63, 0x86, 0x2e, 0xfc,
-	0x06, 0x7f, 0xaf, 0x96, 0x5d, 0x01, 0xb4, 0xcb, 0x6e, 0xa1, 0x3b, 0xa4, 0xbc, 0x35, 0xfc, 0x21,
-	0x0c, 0x5a, 0xbb, 0x96, 0xa9, 0xc1, 0x6c, 0x8b, 0xde, 0x27, 0xfd, 0x83, 0xe4, 0x19, 0x3a, 0x76,
-	0x0d, 0xdd, 0x1a, 0xcd, 0x1a, 0x0d, 0x97, 0xba, 0xb6, 0xc1, 0x21, 0x91, 0xa0, 0x95, 0x5e, 0x75,
-	0x6d, 0xe7, 0xc7, 0xf4, 0xf5, 0x87, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7b, 0x47, 0x47, 0xed,
-	0x92, 0x01, 0x00, 0x00,
+	// 299 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x90, 0xc1, 0x4a, 0xf3, 0x40,
+	0x14, 0x85, 0xc9, 0xff, 0x17, 0xb1, 0xb7, 0xb6, 0xa9, 0xe3, 0xc2, 0xb8, 0x10, 0xa5, 0x0b, 0xe9,
+	0x42, 0x0b, 0x56, 0xba, 0x70, 0xe3, 0xa6, 0x88, 0x86, 0x82, 0xc4, 0xf8, 0x00, 0x43, 0x3a, 0x73,
+	0x93, 0x0c, 0x8d, 0x99, 0x30, 0x99, 0x56, 0xe6, 0x4d, 0xdc, 0xfb, 0xa2, 0x92, 0x1b, 0xa5, 0xad,
+	0xab, 0x99, 0x39, 0xe7, 0xde, 0xef, 0x1c, 0x06, 0x4e, 0xc4, 0x87, 0x15, 0xf9, 0x4d, 0x65, 0x74,
+	0xaa, 0x0a, 0x9c, 0x54, 0x46, 0x5b, 0xcd, 0x0e, 0xe9, 0x10, 0xba, 0x18, 0x7d, 0x7a, 0xe0, 0xcf,
+	0x9b, 0x89, 0x08, 0xd1, 0x44, 0x89, 0x58, 0xa1, 0x65, 0x0f, 0x30, 0xa0, 0x25, 0xae, 0x24, 0x96,
+	0x56, 0xa5, 0x2e, 0xf0, 0x2e, 0xbd, 0x71, 0x6f, 0x7a, 0x3a, 0xf9, 0x5d, 0x9b, 0xd0, 0x4a, 0x48,
+	0xb6, 0x75, 0x71, 0x5f, 0x6c, 0x9f, 0xa9, 0x63, 0x8f, 0x70, 0x9c, 0x19, 0xbd, 0xae, 0xb8, 0xc8,
+	0x13, 0xcb, 0x55, 0xb9, 0x51, 0x16, 0x83, 0x7f, 0x84, 0x38, 0xdb, 0x22, 0x9e, 0x9a, 0x91, 0x79,
+	0x9e, 0xd8, 0x90, 0x06, 0x62, 0x3f, 0xdb, 0x17, 0x46, 0xaf, 0xd0, 0xdf, 0x8b, 0x61, 0x0c, 0x3a,
+	0x65, 0xf2, 0x8e, 0xd4, 0xa6, 0x1b, 0xd3, 0x9d, 0x5d, 0x03, 0x43, 0x39, 0x9d, 0xcd, 0x6e, 0xef,
+	0x79, 0xb5, 0x5e, 0x16, 0x4a, 0xf0, 0x15, 0x3a, 0x0a, 0x3b, 0x8a, 0x87, 0x3f, 0x4e, 0x44, 0xc6,
+	0x02, 0xdd, 0xe8, 0xcb, 0x03, 0xff, 0x4f, 0x2e, 0x3b, 0x07, 0x68, 0xdb, 0xee, 0xb0, 0xbb, 0xa4,
+	0xbc, 0x34, 0x01, 0x63, 0x18, 0xb6, 0x76, 0x9d, 0x27, 0x06, 0xe5, 0x0e, 0x7e, 0x40, 0xfa, 0x1b,
+	0xc9, 0x0b, 0x74, 0xec, 0x02, 0x7a, 0x35, 0x9a, 0x0d, 0x1a, 0x9e, 0xeb, 0xda, 0x06, 0xff, 0x89,
+	0x04, 0xad, 0xf4, 0xac, 0x6b, 0xcb, 0xae, 0xc0, 0xaf, 0x55, 0x56, 0xa2, 0xe4, 0x2d, 0x51, 0xc9,
+	0xa0, 0x43, 0xa4, 0x7e, 0x2b, 0x53, 0xb3, 0x50, 0x2e, 0x0f, 0xe8, 0x8f, 0xee, 0xbe, 0x03, 0x00,
+	0x00, 0xff, 0xff, 0x62, 0x61, 0x2d, 0x00, 0xbb, 0x01, 0x00, 0x00,
 }

+ 1 - 0
protocol/cwtch-profile.proto

@@ -16,4 +16,5 @@ message GroupChatInvite {
         string group_name = 1;
         bytes  group_shared_key = 2;
         string server_host = 3;
+        bytes  signed_group_id = 4;
 }

+ 70 - 18
protocol/group_message.pb.go

@@ -6,7 +6,7 @@ package protocol
 import proto "github.com/golang/protobuf/proto"
 import fmt "fmt"
 import math "math"
-import Protocol_Data_Control "github.com/s-rah/go-ricochet/wire/control"
+import control "github.com/s-rah/go-ricochet/wire/control"
 
 // Reference imports to suppress errors if they are not otherwise used.
 var _ = proto.Marshal
@@ -20,9 +20,10 @@ type CwtchServerPacket struct {
 	XXX_unrecognized []byte          `json:"-"`
 }
 
-func (m *CwtchServerPacket) Reset()         { *m = CwtchServerPacket{} }
-func (m *CwtchServerPacket) String() string { return proto.CompactTextString(m) }
-func (*CwtchServerPacket) ProtoMessage()    {}
+func (m *CwtchServerPacket) Reset()                    { *m = CwtchServerPacket{} }
+func (m *CwtchServerPacket) String() string            { return proto.CompactTextString(m) }
+func (*CwtchServerPacket) ProtoMessage()               {}
+func (*CwtchServerPacket) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} }
 
 func (m *CwtchServerPacket) GetGroupMessage() *GroupMessage {
 	if m != nil {
@@ -49,9 +50,10 @@ type FetchMessage struct {
 	XXX_unrecognized []byte `json:"-"`
 }
 
-func (m *FetchMessage) Reset()         { *m = FetchMessage{} }
-func (m *FetchMessage) String() string { return proto.CompactTextString(m) }
-func (*FetchMessage) ProtoMessage()    {}
+func (m *FetchMessage) Reset()                    { *m = FetchMessage{} }
+func (m *FetchMessage) String() string            { return proto.CompactTextString(m) }
+func (*FetchMessage) ProtoMessage()               {}
+func (*FetchMessage) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} }
 
 type GroupMessage struct {
 	Ciphertext       []byte `protobuf:"bytes,1,req,name=ciphertext" json:"ciphertext,omitempty"`
@@ -59,9 +61,10 @@ type GroupMessage struct {
 	XXX_unrecognized []byte `json:"-"`
 }
 
-func (m *GroupMessage) Reset()         { *m = GroupMessage{} }
-func (m *GroupMessage) String() string { return proto.CompactTextString(m) }
-func (*GroupMessage) ProtoMessage()    {}
+func (m *GroupMessage) Reset()                    { *m = GroupMessage{} }
+func (m *GroupMessage) String() string            { return proto.CompactTextString(m) }
+func (*GroupMessage) ProtoMessage()               {}
+func (*GroupMessage) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{2} }
 
 func (m *GroupMessage) GetCiphertext() []byte {
 	if m != nil {
@@ -82,14 +85,17 @@ func (m *GroupMessage) GetSpamguard() []byte {
 // GroupMessage
 type DecryptedGroupMessage struct {
 	Onion            *string `protobuf:"bytes,1,req,name=onion" json:"onion,omitempty"`
-	Text             *string `protobuf:"bytes,2,req,name=text" json:"text,omitempty"`
-	Signature        []byte  `protobuf:"bytes,3,req,name=signature" json:"signature,omitempty"`
+	Timestamp        *int32  `protobuf:"varint,2,req,name=timestamp" json:"timestamp,omitempty"`
+	Text             *string `protobuf:"bytes,3,req,name=text" json:"text,omitempty"`
+	Signature        []byte  `protobuf:"bytes,4,req,name=signature" json:"signature,omitempty"`
+	SignedGroupId    []byte  `protobuf:"bytes,5,req,name=signed_group_id,json=signedGroupId" json:"signed_group_id,omitempty"`
 	XXX_unrecognized []byte  `json:"-"`
 }
 
-func (m *DecryptedGroupMessage) Reset()         { *m = DecryptedGroupMessage{} }
-func (m *DecryptedGroupMessage) String() string { return proto.CompactTextString(m) }
-func (*DecryptedGroupMessage) ProtoMessage()    {}
+func (m *DecryptedGroupMessage) Reset()                    { *m = DecryptedGroupMessage{} }
+func (m *DecryptedGroupMessage) String() string            { return proto.CompactTextString(m) }
+func (*DecryptedGroupMessage) ProtoMessage()               {}
+func (*DecryptedGroupMessage) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{3} }
 
 func (m *DecryptedGroupMessage) GetOnion() string {
 	if m != nil && m.Onion != nil {
@@ -98,6 +104,13 @@ func (m *DecryptedGroupMessage) GetOnion() string {
 	return ""
 }
 
+func (m *DecryptedGroupMessage) GetTimestamp() int32 {
+	if m != nil && m.Timestamp != nil {
+		return *m.Timestamp
+	}
+	return 0
+}
+
 func (m *DecryptedGroupMessage) GetText() string {
 	if m != nil && m.Text != nil {
 		return *m.Text
@@ -112,14 +125,53 @@ func (m *DecryptedGroupMessage) GetSignature() []byte {
 	return nil
 }
 
+func (m *DecryptedGroupMessage) GetSignedGroupId() []byte {
+	if m != nil {
+		return m.SignedGroupId
+	}
+	return nil
+}
+
 var E_ServerNonce = &proto.ExtensionDesc{
-	ExtendedType:  (*Protocol_Data_Control.ChannelResult)(nil),
+	ExtendedType:  (*control.ChannelResult)(nil),
 	ExtensionType: ([]byte)(nil),
 	Field:         8200,
-	Name:          "im.cwtch.server_nonce",
-	Tag:           "bytes,8200,opt,name=server_nonce",
+	Name:          "protocol.server_nonce",
+	Tag:           "bytes,8200,opt,name=server_nonce,json=serverNonce",
+	Filename:      "group_message.proto",
 }
 
 func init() {
+	proto.RegisterType((*CwtchServerPacket)(nil), "protocol.CwtchServerPacket")
+	proto.RegisterType((*FetchMessage)(nil), "protocol.FetchMessage")
+	proto.RegisterType((*GroupMessage)(nil), "protocol.GroupMessage")
+	proto.RegisterType((*DecryptedGroupMessage)(nil), "protocol.DecryptedGroupMessage")
 	proto.RegisterExtension(E_ServerNonce)
 }
+
+func init() { proto.RegisterFile("group_message.proto", fileDescriptor2) }
+
+var fileDescriptor2 = []byte{
+	// 332 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x51, 0xdb, 0x4a, 0xf3, 0x30,
+	0x1c, 0xa7, 0x3b, 0xc0, 0xb7, 0xff, 0xba, 0x7d, 0x18, 0xa7, 0x06, 0x11, 0x29, 0xbd, 0x90, 0x5d,
+	0xed, 0xc2, 0x4b, 0x87, 0x20, 0x4c, 0x14, 0x41, 0x45, 0xea, 0x03, 0x8c, 0x90, 0xfe, 0xd7, 0x16,
+	0xdb, 0xa4, 0x24, 0xa9, 0x87, 0x37, 0xf0, 0x45, 0x7c, 0x1b, 0x1f, 0x4a, 0x9a, 0xee, 0x90, 0x7a,
+	0xe1, 0x55, 0xf8, 0x9d, 0x43, 0x02, 0xfb, 0x89, 0x92, 0x55, 0xb9, 0x2c, 0x50, 0x6b, 0x96, 0xe0,
+	0xac, 0x54, 0xd2, 0x48, 0xf2, 0xcf, 0x1e, 0x5c, 0xe6, 0xc7, 0x93, 0x85, 0x14, 0x46, 0xc9, 0x7c,
+	0x91, 0x32, 0x21, 0x30, 0x6f, 0xf4, 0xf0, 0xdb, 0x83, 0xbd, 0xc5, 0x9b, 0xe1, 0xe9, 0x33, 0xaa,
+	0x57, 0x54, 0x4f, 0x8c, 0xbf, 0xa0, 0x21, 0x73, 0x18, 0xb5, 0xca, 0xa8, 0x17, 0x78, 0xd3, 0xe1,
+	0xf9, 0xe1, 0x6c, 0xd3, 0x36, 0xbb, 0xad, 0xe5, 0x87, 0x46, 0x8d, 0xfc, 0xc4, 0x41, 0x75, 0x78,
+	0x85, 0x86, 0xa7, 0xdb, 0x70, 0xe7, 0x77, 0xf8, 0xa6, 0x96, 0xb7, 0xe1, 0x95, 0x83, 0xc8, 0x25,
+	0x8c, 0x5b, 0xcb, 0x9a, 0x76, 0x83, 0xee, 0x1f, 0xd3, 0x23, 0x77, 0x5a, 0x87, 0x63, 0xf0, 0xdd,
+	0xf2, 0xf0, 0x1e, 0x7c, 0xd7, 0x4e, 0x4e, 0x01, 0x78, 0x56, 0xa6, 0xa8, 0x0c, 0xbe, 0x1b, 0xea,
+	0x05, 0x9d, 0xa9, 0x1f, 0x39, 0x0c, 0x39, 0x81, 0x81, 0x2e, 0x59, 0x91, 0x54, 0x4c, 0xc5, 0xb4,
+	0x63, 0xe5, 0x1d, 0x11, 0x7e, 0x79, 0x70, 0x70, 0x8d, 0x5c, 0x7d, 0x94, 0x06, 0xe3, 0x56, 0xef,
+	0x04, 0xfa, 0x52, 0x64, 0x52, 0xd8, 0xca, 0x41, 0xd4, 0x80, 0xba, 0xcd, 0x64, 0x05, 0x6a, 0xc3,
+	0x8a, 0xd2, 0xb6, 0xf5, 0xa3, 0x1d, 0x41, 0x08, 0xf4, 0xec, 0x2d, 0xba, 0x36, 0xd2, 0xdb, 0xee,
+	0x67, 0x89, 0x60, 0xa6, 0x52, 0x48, 0x7b, 0xeb, 0xfd, 0x0d, 0x41, 0xce, 0xe0, 0x7f, 0x0d, 0x30,
+	0x5e, 0x36, 0x6f, 0x94, 0xc5, 0xb4, 0x6f, 0x3d, 0xa3, 0x86, 0xb6, 0x57, 0xba, 0x8b, 0x2f, 0xe6,
+	0xe0, 0x6b, 0xfb, 0x9d, 0x4b, 0x21, 0x05, 0x47, 0x72, 0xb4, 0x7b, 0xbc, 0xf5, 0xef, 0x47, 0xa8,
+	0xab, 0xdc, 0xd0, 0xcf, 0xab, 0xc0, 0x9b, 0xfa, 0xd1, 0xb0, 0x71, 0x3f, 0xd6, 0xe6, 0x9f, 0x00,
+	0x00, 0x00, 0xff, 0xff, 0xbf, 0x17, 0x09, 0x79, 0x47, 0x02, 0x00, 0x00,
+}

+ 1 - 0
protocol/group_message.proto

@@ -29,4 +29,5 @@ message DecryptedGroupMessage {
         required int32 timestamp = 2;
         required string text = 3;
         required bytes signature = 4;
+        required bytes signed_group_id = 5;
 }