Squah commit

This commit is contained in:
Sarah Jamie Lewis 2018-03-09 12:44:13 -08:00
commit 5d7bdce118
50 changed files with 3864 additions and 0 deletions

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# Cwtch
## Minimal Viable Product Feature Set
* Connecting Dialog
* Profiles
* Server Setup
* Verification
* Peer-2-Peer Chat

83
client/client.go Normal file
View File

@ -0,0 +1,83 @@
package client
import (
"crypto/rand"
"crypto/rsa"
"github.com/s-rah/go-ricochet"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/connection"
"github.com/s-rah/go-ricochet/identity"
"time"
)
type CwtchClient struct {
connection.AutoConnectionHandler
In chan string
Out chan string
}
func (cc *CwtchClient) JoinServer(hostname string) {
pk, _ := rsa.GenerateKey(rand.Reader, 1024)
cc.Init()
cc.RegisterChannelHandler("im.ricochet.chat", func() channels.Handler {
chat := new(channels.ChatChannel)
chat.Handler = cc
return chat
})
rc, err := goricochet.Open(hostname)
if err == nil {
_, err := connection.HandleOutboundConnection(rc).ProcessAuthAsClient(identity.Initialize("", pk))
if err == nil {
go func() {
rc.Do(func() error {
rc.EnableFeatures([]string{"im.ricochet.chat"})
rc.RequestOpenChannel("im.ricochet.chat", &channels.ChatChannel{
Handler: cc,
})
return nil
})
sendMessage := func(message string) {
rc.Do(func() error {
channel := rc.Channel("im.ricochet.chat", channels.Outbound)
if channel != nil {
chatchannel, ok := channel.Handler.(*channels.ChatChannel)
if ok {
chatchannel.SendMessage(message)
}
} else {
//XXX: FIXME
}
return nil
})
}
for {
message := <-cc.In
sendMessage(message)
}
}()
rc.Process(cc)
}
}
}
// OnClosed ...
func (cc *CwtchClient) OnClosed(err error) {
}
// ChatMessage passes the response to messages.
func (cc *CwtchClient) ChatMessage(messageID uint32, when time.Time, message string) bool {
cc.Out <- message
return true
}
// ChatMessageAck does nothing.
func (cc *CwtchClient) ChatMessageAck(messageID uint32, accepted bool) {
}

175
client/cwtch_peer.go Normal file
View File

@ -0,0 +1,175 @@
package client
import (
"encoding/json"
"git.mascherari.press/cwtch/model"
"git.mascherari.press/cwtch/protocol"
"github.com/s-rah/go-ricochet"
"github.com/s-rah/go-ricochet/application"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/connection"
"github.com/s-rah/go-ricochet/identity"
"io/ioutil"
"sync"
)
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
}
func NewCwtchPeer(name string) *CwtchPeer {
peer := new(CwtchPeer)
peer.Profile = model.GenerateNewProfile(name)
peer.PendingInvites = make(map[string][]string)
peer.Log = make(chan string)
peer.Init()
return peer
}
func (cp *CwtchPeer) Save(profilefile string) error {
cp.mutex.Lock()
bytes, _ := json.Marshal(cp)
err := ioutil.WriteFile(profilefile, bytes, 0600)
cp.mutex.Unlock()
return err
}
func LoadCwtchPeer(profilefile string) (*CwtchPeer, error) {
bytes, _ := ioutil.ReadFile(profilefile)
peer := new(CwtchPeer)
err := json.Unmarshal(bytes, &peer)
return peer, err
}
// 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()
}
// 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) PeerListen(onion string) {
}
func (cp *CwtchPeer) JoinServer(onion string) {
}
func (cp *CwtchPeer) SendMessageToGroup(groupid string, message string) {
// Lookup Group
// Lookup Sever Connection
// If no server connection, spin off server connection
// Else group.EncryptMessage(message) and send result to server
}
func (cp *CwtchPeer) Listen() error {
cwtchpeer := new(application.RicochetApplication)
l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", cp.Profile.OnionPrivateKey, 9878)
if err != nil {
return err
}
af := application.ApplicationInstanceFactory{}
af.Init()
af.AddHandler("im.cwtch.peer", func(rai *application.ApplicationInstance) func() channels.Handler {
cpi := new(CwtchPeerInstance)
cpi.Init(rai, cwtchpeer)
return func() channels.Handler {
chat := new(protocol.CwtchPeerChannel)
chat.Handler = &CwtchPeerHandler{Onion: rai.RemoteHostname, Peer: cp}
return chat
}
})
cwtchpeer.Init(cp.Profile.OnionPrivateKey, af, new(application.AcceptAllContactManager))
cp.Log <- "Running cwtch peer on " + l.Addr().String()
cwtchpeer.Run(l)
return nil
}
func (cp *CwtchPeer) EstablishContact(onion string) {
rc, err := goricochet.Open(onion)
if err == nil {
cp.Log <- "Connected to " + onion
cp.RegisterChannelHandler("im.cwtch.peer", func() channels.Handler {
peer := new(protocol.CwtchPeerChannel)
peer.Handler = &CwtchPeerHandler{Onion: onion, Peer: cp}
return peer
})
if err == nil {
_, err := connection.HandleOutboundConnection(rc).ProcessAuthAsClient(identity.Initialize("", cp.Profile.OnionPrivateKey))
if err == nil {
cp.Log <- "Authed to " + onion
go func() {
rc.Do(func() error {
rc.RequestOpenChannel("im.cwtch.peer", &protocol.CwtchPeerChannel{
Handler: &CwtchPeerHandler{Onion: onion, Peer: cp},
})
return nil
})
sendIdentity := func(message []byte) {
rc.Do(func() error {
channel := rc.Channel("im.cwtch.peer", channels.Outbound)
if channel != nil {
cwtchchannel, ok := channel.Handler.(*protocol.CwtchPeerChannel)
if ok {
cwtchchannel.SendMessage(message)
}
} else {
cp.Log <- "Error finding cwtch channel " + onion
}
return nil
})
}
sendIdentity(cp.Profile.GetCwtchIdentityPacket())
}()
rc.Process(cp)
}
}
}
// onion is offline
// TODO Sleep, Wake, Retry (assuming contact still exists)
}
type CwtchPeerHandler struct {
Onion string
Peer *CwtchPeer
}
func (cph *CwtchPeerHandler) ClientIdentity(ci *protocol.CwtchIdentity) {
cph.Peer.Log <- "Received Client Identity from " + cph.Onion + " " + ci.String()
cph.Peer.Profile.AddCwtchIdentity(cph.Onion, *ci)
}
func (cph *CwtchPeerHandler) HandleGroupInvite() {
}

15
client/cwtch_peer_test.go Normal file
View File

@ -0,0 +1,15 @@
package client
import (
"testing"
)
func TestCwtchPeerGenerate(t *testing.T) {
sarah := NewCwtchPeer("sarah")
sarah.Save("test_profile")
sarahLoaded, err := LoadCwtchPeer("test_profile")
if err != nil || sarahLoaded.Profile.Name != "sarah" {
t.Errorf("something went wrong saving and loading profiles %v %v", err, sarahLoaded)
}
t.Logf("%v", sarahLoaded)
}

View File

@ -0,0 +1,89 @@
package fetch
import (
"errors"
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/utils"
"github.com/s-rah/go-ricochet/wire/control"
)
// CwtchPeerFetchChannel is the peer implementation of the im.cwtch.server.fetch
// channel.
type CwtchPeerFetchChannel struct {
channel *channels.Channel
Handler CwtchPeerFetchChannelHandler
}
// CwtchPeerFetchChannelHandlersould be implemented by peers to receive new messages.
type CwtchPeerFetchChannelHandler interface {
HandleGroupMessage(*protocol.GroupMessage)
}
// Type returns the type string for this channel, e.g. "im.ricochet.server.fetch)
func (cpfc *CwtchPeerFetchChannel) Type() string {
return "im.cwtch.server.fetch"
}
// Closed is called when the channel is closed for any reason.
func (cpfc *CwtchPeerFetchChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch server channels only client can open
func (cpfc *CwtchPeerFetchChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cpfc *CwtchPeerFetchChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cpfc *CwtchPeerFetchChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch server channels require no auth.
func (cpfc *CwtchPeerFetchChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound - cwtch server peer implementations shouldnever respond to inbound requests
func (cpfc *CwtchPeerFetchChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
return nil, errors.New("client does not receive inbound listen channels")
}
// OpenOutbound sets up a new cwtch fetch channel
func (cpfc *CwtchPeerFetchChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cpfc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cpfc.Type()), nil
}
// OpenOutboundResult confirms a previous open channel request
func (cpfc *CwtchPeerFetchChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cpfc.channel.Pending = false
}
}
}
// Packet is called for each raw packet received on this channel.
func (cpfc *CwtchPeerFetchChannel) Packet(data []byte) {
csp := &protocol.CwtchServerPacket{}
err := proto.Unmarshal(data, csp)
if err == nil {
if csp.GetGroupMessages() != nil {
gms := csp.GetGroupMessages()
for _, gm := range gms {
cpfc.Handler.HandleGroupMessage(gm)
}
}
}
// After a fetch we close the channel.
cpfc.channel.CloseChannel()
}

View File

@ -0,0 +1,89 @@
package fetch
import (
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/wire/control"
"testing"
)
type TestHandler struct {
Received bool
}
func (th *TestHandler) HandleGroupMessage(m *protocol.GroupMessage) {
th.Received = true
}
func TestPeerFetchChannelAttributes(t *testing.T) {
cssc := new(CwtchPeerFetchChannel)
if cssc.Type() != "im.cwtch.server.fetch" {
t.Errorf("cwtch channel type is incorrect %v", cssc.Type())
}
if !cssc.OnlyClientCanOpen() {
t.Errorf("only clients should be able to open im.cwtch.server.Fetch channel")
}
if cssc.Bidirectional() {
t.Errorf("im.cwtch.server.fetch should not be bidirectional")
}
if !cssc.Singleton() {
t.Errorf("im.cwtch.server.fetch should be a Singleton")
}
if cssc.RequiresAuthentication() != "none" {
t.Errorf("cwtch channel required auth is incorrect %v", cssc.RequiresAuthentication())
}
}
func TestPeerFetchChannelOpenInbound(t *testing.T) {
cssc := new(CwtchPeerFetchChannel)
channel := new(channels.Channel)
_, err := cssc.OpenInbound(channel, nil)
if err == nil {
t.Errorf("client implementation of im.cwtch.server.Fetch should never open an inbound channel")
}
}
func TestPeerFetchChannel(t *testing.T) {
pfc := new(CwtchPeerFetchChannel)
th := new(TestHandler)
pfc.Handler = th
channel := new(channels.Channel)
channel.ID = 3
result, err := pfc.OpenOutbound(channel)
if err != nil {
t.Errorf("expected result but also got non-nil error: result:%v, err: %v", result, err)
}
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(3),
Opened: proto.Bool(true),
}
pfc.OpenOutboundResult(nil, cr)
if channel.Pending {
t.Errorf("once opened channel should no longer be pending")
}
csp := &protocol.CwtchServerPacket{
GroupMessages: []*protocol.GroupMessage{
{Ciphertext: []byte("hello"),Spamguard: []byte{},},
},
}
packet, _ := proto.Marshal(csp)
pfc.Packet(packet)
if th.Received != true {
t.Errorf("group message should not have been received")
}
pfc.Closed(nil)
}

View File

@ -0,0 +1,97 @@
package listen
import (
"errors"
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/utils"
"github.com/s-rah/go-ricochet/wire/control"
)
// CwtchPeerListenChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.Cwtch". The channel may be inbound or outbound.
//
// CwtchChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchChannelHandler implementation to handle Cwtch events.
type CwtchPeerListenChannel struct {
channel *channels.Channel
Handler CwtchPeerSendChannelHandler
}
// CwtchChannelHandler is implemented by an application type to receive
// events from a CwtchChannel.
type CwtchPeerSendChannelHandler interface {
HandleGroupMessage(*protocol.GroupMessage)
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cc *CwtchPeerListenChannel) Type() string {
return "im.cwtch.server.listen"
}
// Closed is called when the channel is closed for any reason.
func (cc *CwtchPeerListenChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cc *CwtchPeerListenChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cc *CwtchPeerListenChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cc *CwtchPeerListenChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cc *CwtchPeerListenChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cc *CwtchPeerListenChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
return nil, errors.New("client does not receive inbound listen channels")
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cplc *CwtchPeerListenChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cplc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cplc.Type()), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cplc *CwtchPeerListenChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cplc.channel.Pending = false
}
}
}
// Packet is called for each raw packet received on this channel.
func (cplc *CwtchPeerListenChannel) Packet(data []byte) {
csp := &protocol.CwtchServerPacket{}
err := proto.Unmarshal(data, csp)
if err == nil {
if csp.GetGroupMessage() != nil {
gm := csp.GetGroupMessage()
cplc.Handler.HandleGroupMessage(gm)
}
}
}

View File

@ -0,0 +1,87 @@
package listen
import (
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/wire/control"
"testing"
)
type TestHandler struct {
Received bool
}
func (th *TestHandler) HandleGroupMessage(m *protocol.GroupMessage) {
th.Received = true
}
func TestPeerListenChannelAttributes(t *testing.T) {
cssc := new(CwtchPeerListenChannel)
if cssc.Type() != "im.cwtch.server.listen" {
t.Errorf("cwtch channel type is incorrect %v", cssc.Type())
}
if !cssc.OnlyClientCanOpen() {
t.Errorf("only clients should be able to open im.cwtch.server.listen channel")
}
if cssc.Bidirectional() {
t.Errorf("im.cwtch.server.listen should not be bidirectional")
}
if !cssc.Singleton() {
t.Errorf("im.cwtch.server.listen should be a Singleton")
}
if cssc.RequiresAuthentication() != "none" {
t.Errorf("cwtch channel required auth is incorrect %v", cssc.RequiresAuthentication())
}
}
func TestPeerListenChannelOpenInbound(t *testing.T) {
cssc := new(CwtchPeerListenChannel)
channel := new(channels.Channel)
_, err := cssc.OpenInbound(channel, nil)
if err == nil {
t.Errorf("client implementation of im.cwtch.server.Listen should never open an inbound channel")
}
}
func TestPeerListenChannel(t *testing.T) {
pfc := new(CwtchPeerListenChannel)
th := new(TestHandler)
pfc.Handler = th
channel := new(channels.Channel)
channel.ID = 3
result, err := pfc.OpenOutbound(channel)
if err != nil {
t.Errorf("expected result but also got non-nil error: result:%v, err: %v", result, err)
}
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(3),
Opened: proto.Bool(true),
}
pfc.OpenOutboundResult(nil, cr)
if channel.Pending {
t.Errorf("once opened channel should no longer be pending")
}
csp := &protocol.CwtchServerPacket{
GroupMessage: &protocol.GroupMessage{Ciphertext: []byte("hello"),Spamguard: []byte{},},
}
packet, _ := proto.Marshal(csp)
pfc.Packet(packet)
if th.Received != true {
t.Errorf("group message should not have been received")
}
pfc.Closed(nil)
}

View File

@ -0,0 +1,102 @@
package listen
import (
"errors"
"git.mascherari.press/cwtch/protocol"
"git.mascherari.press/cwtch/protocol/spam"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/utils"
"github.com/s-rah/go-ricochet/wire/control"
)
// CwtchChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.Cwtch". The channel may be inbound or outbound.
//
// CwtchChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchChannelHandler implementation to handle Cwtch events.
type CwtchPeerSendChannel struct {
channel *channels.Channel
spamGuard spam.SpamGuard
challenge []byte
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cc *CwtchPeerSendChannel) Type() string {
return "im.cwtch.server.send"
}
// Closed is called when the channel is closed for any reason.
func (cc *CwtchPeerSendChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cc *CwtchPeerSendChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cc *CwtchPeerSendChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cc *CwtchPeerSendChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cc *CwtchPeerSendChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cc *CwtchPeerSendChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
return nil, errors.New("client does not receive inbound listen channels")
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cplc *CwtchPeerSendChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cplc.spamGuard.Difficulty = 2
cplc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cplc.Type()), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cplc *CwtchPeerSendChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cplc.channel.Pending = false
ce, _ := proto.GetExtension(crm, protocol.E_ServerNonce)
cplc.challenge = ce.([]byte)[:]
}
}
}
// SendGroupMessage
func (cplc *CwtchPeerSendChannel) SendGroupMessage(gm *protocol.GroupMessage) {
sgsolve := cplc.spamGuard.SolveChallenge(cplc.challenge, gm.GetCiphertext())
gm.Spamguard = sgsolve
csp := &protocol.CwtchServerPacket{
GroupMessage: gm,
}
packet, _ := proto.Marshal(csp)
cplc.channel.SendMessage(packet)
}
// Packet is called for each raw packet received on this channel.
func (cc *CwtchPeerSendChannel) Packet(data []byte) {
// If we receive a packet on this channel, close the connection
cc.channel.CloseChannel()
}

View File

@ -0,0 +1,111 @@
package listen
import (
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/wire/control"
"git.mascherari.press/cwtch/protocol/spam"
"testing"
)
type TestHandler struct {
Received bool
}
func (th *TestHandler) HandleGroupMessage(m *protocol.GroupMessage) {
th.Received = true
}
func TestPeerSendChannelAttributes(t *testing.T) {
cssc := new(CwtchPeerSendChannel)
if cssc.Type() != "im.cwtch.server.send" {
t.Errorf("cwtch channel type is incorrect %v", cssc.Type())
}
if !cssc.OnlyClientCanOpen() {
t.Errorf("only clients should be able to open im.cwtch.server.send channel")
}
if cssc.Bidirectional() {
t.Errorf("im.cwtch.server.listen should not be bidirectional")
}
if !cssc.Singleton() {
t.Errorf("im.cwtch.server.listen should be a Singleton")
}
if cssc.RequiresAuthentication() != "none" {
t.Errorf("cwtch channel required auth is incorrect %v", cssc.RequiresAuthentication())
}
}
func TestPeerSendChannelOpenInbound(t *testing.T) {
cssc := new(CwtchPeerSendChannel)
channel := new(channels.Channel)
_, err := cssc.OpenInbound(channel, nil)
if err == nil {
t.Errorf("client implementation of im.cwtch.server.Listen should never open an inbound channel")
}
}
func TestPeerSendChannelClosesOnPacket(t *testing.T) {
pfc := new(CwtchPeerSendChannel)
channel := new(channels.Channel)
closed := false
channel.CloseChannel = func () {
closed = true
}
pfc.OpenOutbound(channel)
pfc.Packet([]byte{})
if !closed {
t.Errorf("send channel should close if server attempts to send packets")
}
}
func TestPeerSendChannel(t *testing.T) {
pfc := new(CwtchPeerSendChannel)
channel := new(channels.Channel)
channel.ID = 3
success := false
var sg spam.SpamGuard
sg.Difficulty = 2
channel.SendMessage = func(message []byte) {
packet := new(protocol.CwtchServerPacket)
proto.Unmarshal(message[:], packet)
if packet.GetGroupMessage() != nil {
success = sg.ValidateChallenge(packet.GetGroupMessage().GetCiphertext(), packet.GetGroupMessage().GetSpamguard())
}
}
result, err := pfc.OpenOutbound(channel)
if err != nil {
t.Errorf("expected result but also got non-nil error: result:%v, err: %v", result, err)
}
challenge := sg.GenerateChallenge(3)
control := new(Protocol_Data_Control.Packet)
proto.Unmarshal(challenge[:], control)
pfc.OpenOutboundResult(nil, control.GetChannelResult())
if channel.Pending {
t.Errorf("once opened channel should no longer be pending")
}
gm := &protocol.GroupMessage{Ciphertext: []byte("hello"),}
pfc.SendGroupMessage(gm)
if !success {
t.Errorf("send channel should have successfully sent a valid group message")
}
pfc.Closed(nil)
}

1
client/test_profile Normal file
View File

@ -0,0 +1 @@
{"Profile":{"Name":"sarah","Ed25519PublicKey":"QUqkM0hmJ6UnfLFirkIqC0ZzxNVbMc55ePtpihk4QP4=","Contacts":{},"Ed25519PrivateKey":"B3gR0WdoGfGLONyV6Mq685ltyAE3coKo39Z6FtjOg8pBSqQzSGYnpSd8sWKuQioLRnPE1Vsxznl4+2mKGThA/g==","OnionPrivateKey":null,"Groups":{}},"PendingContacts":null,"PendingInvites":{}}

106
editor.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"strings"
"github.com/jroimartin/gocui"
)
func editor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
//state.HideHelp = true
//if state.Mode == modeEscape {
// escEditor(v, key, ch, mod)
// return
//}
if ch != 0 && mod == 0 {
v.EditWrite(ch)
}
switch key {
case gocui.KeyEsc:
//state.Mode = modeEscape
//state.KeepAutoscrolling = true
// Space, backspace, Del
case gocui.KeySpace:
v.EditWrite(' ')
case gocui.KeyBackspace, gocui.KeyBackspace2:
v.EditDelete(true)
moveAhead(v)
case gocui.KeyDelete:
v.EditDelete(false)
// Cursor movement
case gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
moveAhead(v)
case gocui.KeyArrowRight:
x, _ := v.Cursor()
x2, _ := v.Origin()
x += x2
buf := v.Buffer()
// I don't know really how this works, this was mostly obtained through trial
// and error. Anyway, this system impedes going on a newline by moving right.
// This is usually possible because once you write something to the buffer
// it automatically adds " \n", which is two characters. Sooo yeah.
if buf != "" && len(buf) > (x+2) {
v.MoveCursor(1, 0, false)
}
case gocui.KeyEnter:
buf := v.Buffer()
v.Clear()
v.SetCursor(0, 0)
if buf != "" {
buf = buf[:len(buf)-1]
}
if strings.TrimSpace(buf) != "" {
//state.PushAction(buf)
//state.ActionIndex = -1
}
enterActionConnect(buf)
}
}
func setText(v *gocui.View, text string) {
v.Clear()
// Why are we doing this? Because normally when you write a line
// gocui adds " \n" at the end of it. Whe clearing and adding, though,
// the space isn't added back.
v.Write([]byte(text + " "))
v.SetCursor(len(text), 0)
}
// moveAhead displays the next 10 characters when moving backwards,
// in order to see where we're moving or what we're deleting.
func moveAhead(v *gocui.View) {
cX, _ := v.Cursor()
oX, _ := v.Origin()
if cX < 10 && oX > 0 {
newOX := oX - 10
forward := 10
if newOX < 0 {
forward += newOX
newOX = 0
}
v.SetOrigin(newOX, 0)
v.MoveCursor(forward, 0, false)
}
}
func enterActionConnect(buf string) {
//log.Printf("Connecting: %s",buf)
connect(buf)
}
func moveDown(v *gocui.View) {
_, yPos := v.Cursor()
if _, err := v.Line(yPos + 1); err == nil {
v.MoveCursor(0, 1, false)
}
}

231
main.go Normal file
View File

@ -0,0 +1,231 @@
package main
import (
//"fmt"
"git.mascherari.press/cwtch/client"
"git.mascherari.press/cwtch/ui"
//"git.mascherari.press/cwtch/server"
//"github.com/fatih/color"
"github.com/jroimartin/gocui"
"log"
"os"
"strconv"
"strings"
//"time"
)
var cwtch_client *client.CwtchPeer
var state ui.State
func run_client(hostname string) {
//cwtch_client.In = make(chan string)
//cwtch_client.Out = make(chan string)
cwtch_client = client.NewCwtchPeer("sarah")
go cwtch_client.Listen()
}
func fetch_messages(g *gocui.Gui) {
for {
message := <-cwtch_client.Log
screen := state.GetScreen(0)
screen.AppendToLog(message)
state.Render()
}
/*for {
message := <-cwtch_client.Out
parts := strings.SplitN(message, " ", 2)
if len(parts) == 2 && len(parts[0]) == 16 {
colorPurple := color.New(color.FgMagenta).Fprint
colorYellow := color.New(color.FgGreen).Fprint
if v, err := g.SetCurrentView("out"); err == nil {
Update(func(*gocui.Gui) error {
t := time.Now()
h := strconv.Itoa(t.Hour())
if len(h) < 2 {
h = "0" + h
}
m := strconv.Itoa(t.Minute())
if len(m) < 2 {
m = "0" + m
}
s := strconv.Itoa(t.Second())
if len(s) < 2 {
s = "0" + s
}
colorYellow(v, "["+h+":"+m+":"+s+"] ")
colorPurple(v, fmt.Sprintf("%s: ", parts[0]))
v.Write([]byte(fmt.Sprintf("%s\n", parts[1])))
return nil
})
}
}
}*/
}
func main() {
if len(os.Args) != 2 {
os.Exit(1)
}
//if os.Args[1] == "server" {
// cwtchserver := new(server.Server)
// cwtchserver.Init()
// cwtchserver.Run("server_key")
// os.Exit(0)
//}
run_client(os.Args[1])
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
state.NewScreen("Welcome", false)
state.NewScreen("#anonymity", true)
state.NewScreen("Sarah", false)
state.Gui = g
//state.ExecuteFunc = g.Update
g.SetManagerFunc(layout)
g.Cursor = true
g.InputEsc = true
g.Mouse = false
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, next); err != nil {
log.Panicln(err)
}
/** if err := g.SetKeybinding("",
gocui.MouseWheelUp,
gocui.ModNone, ScrollUp); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("",
gocui.MouseWheelDown,
gocui.ModNone, ScrollDown); err != nil {
log.Panicln(err)
}**/
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
var SLTWriter *gocui.View
var Update func(func(*gocui.Gui) error)
func layout(g *gocui.Gui) error {
// Set when doing a double-esc
//if state.ShouldQuit {
// return gocui.ErrQuit
//}
Update = g.Update
maxX, maxY := g.Size()
if v, err := g.SetView("cmd", 1, maxY-2, maxX, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.Editable = true
v.Editor = gocui.EditorFunc(editor)
v.Clear()
}
if v, err := g.SetView("menu", -1, maxY-4, maxX, maxY-2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.FgColor = gocui.Attribute(15 + 1)
v.BgColor = gocui.Attribute(0)
v.Frame = false
v.Editable = false
v.Wrap = false
v.Clear()
}
v, err := g.SetView("out", -1, 1, maxX, maxY-4)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
v.Wrap = true
v.Editor = gocui.EditorFunc(editor)
v.Editable = true
SLTWriter = v
}
// For more information about KeepAutoscrolling, see Scrolling in editor.go
v.Autoscroll = true
g.SetViewOnTop("out")
//go func (){
// for range time.Tick(time.Millisecond * 100) {
state.Render()
// }
// }()
go fetch_messages(g)
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func prev(g *gocui.Gui, v *gocui.View) error {
state.PreviousScreen()
return state.Render()
}
func next(g *gocui.Gui, v *gocui.View) error {
state.NextScreen()
return state.Render()
}
func connect(command string) {
if len(command) > 1 && command[0] == '/' {
parts := strings.Split(command, " ")
switch parts[0] {
case "/win":
if len(parts) == 2 {
i, e := strconv.Atoi(parts[1])
if e == nil {
state.GotoScreen(i)
}
}
case "/newgroup":
// TODO /newgroup [name]
case "/pm":
// TODO /pm [name]
case "/newfriend":
// TODO /newfriend [onion]
go cwtch_client.AddContactRequest(parts[1])
case "/block":
// TODO /block [name]
case "/invite":
// TODO /invite [name]...
case "/accept":
// TODO /accept (a group inivite / a friend request)
case "/reject":
// TODO /invite (a group invite / a friend request)
case "/rejoin":
// TODO /rejoin group
case "/leave":
// TODO /leave (a group)
case "/close":
// TODO /close (a group)
default:
screen := state.GetScreen(0)
screen.AppendToLog("Error: Unknown Command " + parts[0])
state.Render()
}
}
}

60
model/group.go Normal file
View File

@ -0,0 +1,60 @@
package model
import (
"crypto/rand"
"fmt"
"golang.org/x/crypto/nacl/secretbox"
"io"
)
type Group struct {
GroupID string
GroupKey [32]byte
GroupServer string
Timeline []Message
}
func NewGroup(server string) *Group {
group := new(Group)
group.GroupServer = server
var groupID [16]byte
if _, err := io.ReadFull(rand.Reader, groupID[:]); err != nil {
panic(err)
}
group.GroupID = fmt.Sprintf("%x", groupID)
var groupKey [32]byte
if _, err := io.ReadFull(rand.Reader, groupKey[:]); err != nil {
panic(err)
}
copy(group.GroupKey[:], groupKey[:])
return group
}
func (g *Group) AddMember() {
// TODO: Rotate Key
}
func (g *Group) RemoveMember() {
// TODO: Rotate Key
}
func (g *Group) EncryptMessage(message string) []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)
return encrypted
}
func (g *Group) DecryptMessage(ciphertext []byte) (bool, string) {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &g.GroupKey)
if ok {
return true, string(decrypted)
}
return false, ""
}

16
model/group_test.go Normal file
View File

@ -0,0 +1,16 @@
package model
import (
"testing"
)
func TestGroup(t *testing.T) {
g := NewGroup("server.onion")
enc_message := g.EncryptMessage("Hello World")
ok, message := g.DecryptMessage(enc_message)
if !ok || message != "Hello World" {
t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
return
}
t.Logf("Got message %v", message)
}

13
model/message.go Normal file
View File

@ -0,0 +1,13 @@
package model
import (
"time"
)
type Message struct {
Timestamp time.Time
PeerID string
Message string
Signature []byte
Verified bool
}

149
model/profile.go Normal file
View File

@ -0,0 +1,149 @@
package model
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/utils"
"golang.org/x/crypto/ed25519"
"io/ioutil"
)
type PublicProfile struct {
Name string
Ed25519PublicKey ed25519.PublicKey
}
type Profile struct {
PublicProfile
Contacts map[string]PublicProfile
Ed25519PrivateKey ed25519.PrivateKey
OnionPrivateKey *rsa.PrivateKey
Groups map[string]*Group
}
func GenerateNewProfile(name string) *Profile {
p := new(Profile)
p.Name = name
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
p.Ed25519PublicKey = pub
p.Ed25519PrivateKey = priv
p.OnionPrivateKey, _ = utils.GeneratePrivateKey()
p.Contacts = make(map[string]PublicProfile)
p.Groups = make(map[string]*Group)
return p
}
// GetCwtchIdentity returns the wire message for conveying this profiles identity.
func (p *Profile) GetCwtchIdentityPacket() (message []byte) {
ci := &protocol.CwtchIdentity{
Name: p.Name,
Ed25519PublicKey: p.Ed25519PublicKey,
}
cpp := &protocol.CwtchPeerPacket{
CwtchIdentify: ci,
}
message, err := proto.Marshal(cpp)
utils.CheckError(err)
return
}
// GetCwtchIdentity returns the wire message for conveying this profiles identity.
func (p *Profile) GetCwtchIdentity() (message []byte) {
ci := &protocol.CwtchIdentity{
Name: p.Name,
Ed25519PublicKey: p.Ed25519PublicKey,
}
message, err := proto.Marshal(ci)
utils.CheckError(err)
return
}
// AddCwtchIdentity takes a wire message and if it is a CwtchIdentity message adds the identity as a contact
// otherwise returns an error
func (p *Profile) AddCwtchIdentity(onion string, ci protocol.CwtchIdentity) {
p.AddContact(onion, PublicProfile{Name: ci.GetName(), Ed25519PublicKey: ci.GetEd25519PublicKey()})
}
// AddContact allows direct manipulation of cwtch contacts
func (p *Profile) AddContact(onion string, profile PublicProfile) {
p.Contacts[onion] = profile
}
// VerifyMessage confirms the authenticity of a message given an onion, message and signature.
func (p *Profile) VerifyMessage(onion string, message string, signature []byte) bool {
return ed25519.Verify(p.Contacts[onion].Ed25519PublicKey, []byte(message), signature)
}
func (p *Profile) SignMessage(message string) []byte {
sig := ed25519.Sign(p.Ed25519PrivateKey, []byte(message))
return sig
}
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,
}
invite, err := proto.Marshal(gci)
utils.CheckError(err)
return
}
// ProcessInvite
func (p *Profile) ProcessInvite(gci protocol.GroupChatInvite) {
group := new(Group)
group.GroupID = gci.GetGroupName()
copy(group.GroupKey[:], gci.GetGroupSharedKey()[:])
group.GroupServer = gci.GetServerHost()
p.AddGroup(group)
}
// AddGroup
func (p *Profile) AddGroup(group *Group) {
p.Groups[group.GroupID] = group
}
// EncryptMessageToGroup
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)
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
}
}
return false, "", "", ""
}
// EncryptMessageToGroup
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))
return
}
func (p *Profile) Save(profilefile string) error {
bytes, _ := json.Marshal(p)
return ioutil.WriteFile(profilefile, bytes, 0600)
}
func LoadProfile(profilefile string) (*Profile, error) {
bytes, _ := ioutil.ReadFile(profilefile)
profile := new(Profile)
err := json.Unmarshal(bytes, &profile)
return profile, err
}

1
model/profile_test Normal file
View File

@ -0,0 +1 @@
{"Name":"Sarah","Ed25519PublicKey":"M2dFnsHcdR7Lha8FpZ1FmgGqAAgs6w2NNBQgfVywj94=","Contacts":{},"Ed25519PrivateKey":"ZtbrDR982Ff9bLOI6kxbK0XiyNnRFyF1o3OlsKBZz6gzZ0Wewdx1HsuFrwWlnUWaAaoACCzrDY00FCB9XLCP3g==","OnionPrivateKey":{"N":142724584074611204803266575696785422729907718147588329252653937092083270046866173891429018691086667376925028577112559384958192132540674032393033261326073927443209907420866237116331281544745323643361522344826234584754455383914989807467870567115328170590992672168466646833750432351388078416618817943207651099679,"E":65537,"D":27413782509593143507690612247602650431421614600025052850029872592812368636799844316116216599530020735473583772963255833160714139561194511798884640639220872144174129684767880956395874567876885238587107317423104962863566342958842528741354706109784913872633964913685129018256002959052669282525505036968820501953,"Primes":[12356772725624677442234968801387889317557092443561606881006476461028875195308317938803572198485774285421241858540915953911278767498206501125289565176882519,11550312305950094688252881670532217665771581816925739370264710346800345350361389988692815692927550198499850330516112470522826081432865910985979998877995641],"Precomputed":{"Dp":7210207147117098586577161632877218600068675284713053828166816704246535794189318801559232257233234576632951003440242412584054981886551334506190369744253847,"Dq":11436989285172123299807475214421132081153867745358788633809269775016580117587806001590423448995841167301666041297474749562968639524305360750784839818555153,"Qinv":7742377143202795780729871745805336552195620685743089969461639809992143700499555945496100753389564870688108703251095656049359483242592495125530931590060376,"CRTValues":[]}},"Groups":{}}

78
model/profile_test.go Normal file
View File

@ -0,0 +1,78 @@
package model
import (
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"testing"
)
func TestProfile(t *testing.T) {
profile := GenerateNewProfile("Sarah")
err := profile.Save("./profile_test")
if err != nil {
t.Errorf("Should have saved profile, but got error:", err)
}
loadedProfile, err := LoadProfile("./profile_test")
if err != nil || loadedProfile.Name != "Sarah" {
t.Errorf("Issue loading profile from file %v %v", err, loadedProfile)
}
}
func TestProfileIdentity(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
message := sarah.GetCwtchIdentity()
ci := &protocol.CwtchIdentity{}
err := proto.Unmarshal(message, ci)
if err != nil {
t.Errorf("alice should have added sarah as a contact %v", err)
}
alice.AddCwtchIdentity("sarah.onion", *ci)
if alice.Contacts["sarah.onion"].Name != "Sarah" {
t.Errorf("alice should have added sarah as a contact %v", alice.Contacts)
}
t.Logf("%v", alice)
}
func TestProfileGroup(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
sarah.AddContact("alice.onion", alice.PublicProfile)
alice.AddContact("sarah.onion", sarah.PublicProfile)
group := NewGroup("server.onion")
alice.AddGroup(group)
sarah.AddGroup(group)
c, s := sarah.EncryptMessageToGroup("Hello World", group.GroupID)
ok, gid, onion, message := alice.AttemptDecryption(c, s)
if ok && gid == group.GroupID && onion == "sarah.onion" && message == "Hello World" {
t.Logf("Success!")
} else {
t.Errorf("Failed to decrypt group message %v %v %v %v", ok, gid, onion, message)
}
group2 := NewGroup("server2.onion")
sarah.AddGroup(group2)
alice.AddGroup(group2)
c2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID)
ok, gid, onion, message = alice.AttemptDecryption(c2, s)
if onion != "not-verified" {
t.Errorf("verification should have failed %v %v %v %v", ok, gid, onion, message)
}
bob := GenerateNewProfile("Bob")
bob.AddGroup(group)
c, s = bob.EncryptMessageToGroup("Hello", group.GroupID)
ok, gid, onion, message = alice.AttemptDecryption(c, s)
if ok && gid == group.GroupID && onion == "not-verified" && message == "Hello" {
t.Logf("Success!")
} else {
t.Errorf("Failed to decrypt unverified group message %v %v %v %v", ok, gid, onion, message)
}
}

View File

@ -0,0 +1,314 @@
# Metadata-Resistant Group Chat
# Introduction
# Use Cases
* Harm Reduction Forums (TODO: Rasmus probably wrote a paper on this)
- Long lived pseudonym linked to long term reputation building, fundamentally anonymous due to underlying anonymity network.
- Repudiation not a hard requirement, in many cases actively harmful - the ability to state that an expert gave a certain piece of advice is vital in both building reputation and for the safety of the participants.
- Relies on a trusted server to store messages
* Public Ricochet identities in Private Group Chat
- Repudiation is a more useful to protect against compromise of the group. //XXX Does the benefit of repudiation outweigh the inefficiency of implementing SSA in this way?
* *Confidentiality*: Only the intended recipients are able to read a message. Specifically, the message must not be readable by a server operator that is not a conversation participant.
* *Integrity*: No honest party will accept a message that has been modified in transit.
* *Authentication*: Each participant in the conversation receives proof of possession of a known long-term secret from all other participants that they believe to be participating in the conversation. In addition, each participant is able to verify that a message was sent from the claimed source.
* *Participant Consistency*: At any point when a message is accepted by an honest party, all honest parties are guaranteed to have the same view of the participant list.
* *Destination Validation*: When a message is accepted by an honest party, they can verify that they were included in the set of intended recipients for the message.
* *Forward Secrecy*: Compromising all key material does not enable decryption of previously encrypted data.
* *Backward Secrecy*: Compromising all key material does not enable decryption of succeeding encrypted data.
* *Anonymity Preserving*: Any anonymity features provided by the underlying transport privacy architecture are not undermined (e.g., if the transport privacy system provides anonymity, the conversation security level does not deanonymize users by linking key identifiers).
* *Speaker Consistency*: All participants agree on the sequence of messages sent by each participant. A protocol might perform consistency checks on blocks of messages during the protocol, or after every message is sent.
* *Causality Preserving*: Implementations can avoid displaying a message before messages that causally precede it.
* *Global Transcript*: All participants see all messages in the same order.
* *Computational Equality* - All chat participants share equal computational load
* *Trust Equality* - No participant is more trusted than any other
* *Subgroup messaging*: Messages can be sent to a subset of participants without forming a new conversation.
* *Contractible Membership*: After the conversation begins, participants can leave without restarting the protocol.
* *Expandable Membership*: After the conversation begins, participants can join without restarting the protocol
In addition we require the following usability criteria
* *Out-of-Order Resilient*: If a message is delayed in transit, but eventually arrives, its contents are accessible upon arrival.
* *Dropped Message Resilient*: Messages can be decrypted without receipt of all previous messages. This is desirable for asynchronous and unreliable network services.
* *Asynchronous*: Messages can be sent securely to disconnected recipients and received upon their next connection.
* *Multi-Device Support*: A user can participate in the conversation using multiple devices at once. Each device must be able to send and receive messages. Ideally, all devices have identical views of the conversation. The devices might use a synchronized long-term key or distinct keys.
*Privacy Preserving*: The approach leaks no conversation metadata to other participants or even service operators.
*Sender Anonymity*: When a chat message is received, no non-global entities except for the sender can determine which entity produced the message.
*Recipient Anonymity*: No non-global entities except the receiver of a chat message know which entity received it.
*Participation Anonymity*: No non-global entities except the conversation participants can discover which set of network nodes are engaged in a conversation.
*Unlinkability*: No non-global entities except the conversation participants can discover that two protocol messages belong to the same conversation
# Metadata-Resistance
## Ricochet: An Overview
Ricochet is a secure messaging protocol which, through it's use of the Tor hidden service protocol provides online 2-party instant messaging with *sender anonymity*, *recipient anonymity*, *participation anonymity* and partial *unlikability* (to network adversaries with limited scope).
Ricochet is *Anonymity Preserving* and provides a number of other properties including *Confidentiality*, *Integrity* , *Authentication*, *Speaker Consistency* and *Causality Preservation* for 2-way instant messaging.
In this paper we will build upon the ricochet protocol to define and implement a metadata-resistant group chat protocol.
To start it is important to understand the properties that Ricochet cannot give us at all, as well as properties which Ricochet does provide for 2-party exchanges but which cannot extend to multi-party protocols.
Ricochet is not *Asynchronous* it requires both parties to be online at the same time in order to exchange messages.
Further properties like *Forward Secrecy*, *Participation Anonymity*, *Authentication* are derived from the hidden service connection between two servers, and thus cannot be trivially extended to a group setting, and must instead be reinforced at a higher level.
The the next section we will demonstrate how these properties can be achieved.
## Online Group Chat
A naive implementation of metadata-resistant group chat which supports the above threat model is a scheme we will call, Online Group Chat (*ORC*).
The protocol for ORC is as follows:
* Setup: Each client involved in the group chat establishes a ricochet channel with every other client.
* Messaging: When a client wishes to send a message they must first encrypt the message to every participating client, then send these messages to every client along with a signature.
* Message Receipt / Attestation: Once a client has received a message, they must decrypt it and compare the contents of the message with the hash, if they match, they must then check with all the other clients to ensure that they all received the same hash.
* Teardown: The group chat ends when the clients destroy their ricochet channels. Offline clients are unable to participate in the chat from then on.
ORC requires every client involved in the group chat to maintain a connection to every other client, this requires `n!/2`s communication channels. As such, like Ricochet, this scheme is not *Asynchronous*
However this approach does provide many of our desirable properties including *Authentication*, *Anonymity Preserving*, *Causality Preserving*, *Computational Equality* and *Trust Equality*
It is clear that a pure peer-2-peer solution based on hidden services cannot account for all of our desired properties. We will now show how a hybrid solution features peer-to-peer channels as well as relay-server can be used to achieve all of our desired properties.
## Introducing a Server
In order to attain the property of *Asynchronous* communication, we must introduce long-lived infrastructure. [citation?]
As stated above it is essential that this infrastructure *not* be trusted, and that is must gain no knowledge about the participants in a particular group conversation.
For simplicity we will separate the concept of *Asynchronous Key-Exchange* and *Asynchronous Conversation* and first demonstrate how *Asynchronous Conversation* can be attained, if we assume *Online Key-Exchange*.
If it can be assumed that every participant in the group performs some kind of key exchange operation with every other participant prior to the conversation starting, and then all future communication is mediated through sending and receiving encrypted packets to the server then we can achieve all of our desired properties.
This is of course a very hand-wavy assumption, and we will return later in this paper to more formally defining a key-exchange mechanism which fully satisfies this property in a meaningful way.
The originator of the group chat, Alice sends a group-key `GI` and a group-chat-server identifier `S` to participants Bob and Carol.
Alice, Bob and Carol all create ricochet connections to `S` using ephemeral ricochet IDs.
When any of Alice, Bob and Carol which to send a message, they sign it using long term signing keys, and encrypt it using the group key and send the resulting ciphertext to `S` where it can be downloaded by the others.
`S` has an idea of the number of ephemeral connections to a given group, and thus can derive the number of participants - but gains no information as to who is speaking, what is being said.
If each message includes a hash of the previous seen message identifiers, then `S` has no ability to modify the transcript (by e.g. not distributing a message to the rest of the group) without being detected. Because each connection to `S` is ephemeral and is regularly torn-down and rebuild `S` gains no information useful to target modification.
# Key Exchange
Pair-wise key exchange can be done when each client is online by establishing a ricochet connection and then simply transmitting public keys for encryption and signing.
Session keys for the groups can then be pairwise encrypted and uploaded to the server.
To expand the group the initiator invites a new user by generating a new session key, and pairwise encrypting that to all users.
When the group contracts, the same thing happens. To avoid rollover vulnerability i.e. the time between a user leaving the group and the initiator sending out new keys, session keys can be generated before hand and send pairwise to each member (minus the one who may leave).
# Public Channels
As discussed in an above section on use cases, there are occasionally instances where open-group chat channels are desired, but where we still wish to retain anonymity, authentication and transcript consistency.
In this case many properties such as forward & backward secrecy
are not applicable
# Untrusted Server
IN cwtch we assume all supporting infrastructure is untrusted,even in cases where i may be setup by one of the chat participants.
Cwtch servers may be used by multiple groups or just one.
Cwtch servers should never learn the identity of participants within a group, the content of any communication, any group session keys.
All participants within a cwtch session must be able to detect and/or successfully mitigate when a cwtch server is acting dishonestly. Dishonest behavior is defined as:
* Failing to relay any message - this will be detected when a message id appears in subsequent messages, but which is not known to some participants. In this case, participants will request a message is resent.
* Modifying a relayed message - this will be detected as a failure to decrypt
* Attaching duplicate messages to the timeline - duplicate messages will be ignored.
Private message are just group messages that only include a subset of the participants - these require an additional group setup and are indistinguishable from simply setting up a new group.
# Types of Messages
* Group Public Messages - All general chat messages.
* Group Administration Messages - Requesting a message be resent.
## Supporting Multiple Devices
Multiple devices can be supported by simply including multiple people within the group setting and having these devices interact with the group as regular participants.
### PROBLEMS
This is METADATA, how to resolve getting the same
Alice(a) -> Carol
Alice(a) -> Alice(b)
Carol -> Alice(a)
Carol -> Alice(b)
If only Alice has a shared device this breaks private message indistinguishably group property.
The *only* way to fix this (without introducing random delays) is to have people sent multiple encrypted messages per private message.
Number of messages to send are max(keys[user])
THIS IS UGLY
##
Peer Channels
Server Channels
## SPAM
Group bucketting on the server
Buckets prevent spam flooding, but also expose group metadata to the server.
To not have bucketing means clients attempt to parse all valid messages on the server...which doesn't scale...and prevents us from being able to detect a malicious server (if the majority of messages are always unable to be decrypted by clients of a server then we have no mechanism for detecting bad behaving servers)
- Overloading the server with too many messages
- DDoS the server such that clients cannot send messages
Private servers will always have some insight into group activity
Public servers are vulnerable to spam floods.
Metadata Protection vs Resistance
A --->S
^ ^
. |
. . > B
# Cascade Design
To keep the design simple we propose that all clients of a server receive every message from a server.
This makes it impossible for a server to associate relationships between the message senders, as everyone receieves a copy of the timeline ( this is equivilant to a naive PIR design)
The reason we can get away with such a simple design is that, by design, Cwtch has no central servers. Each group can choose any random cwtch server to act as a relay for the messages for that group.
We can even Cascade servers by having a server act as a client to another server. This allows us to scale reading resources.
One major potential pitfall with this kind of design is spam. While someone counteracted by the decentralized nature of the protocl (anyone can setup and user a cwtch server), we must consider how to prevent an individual cwtch server from being overwhelmed by bogus messages.
Proof-of-Work places a cap on the number of messages that are accepted by the server that is proportional to the computational power of an adversary.
This certainly doesn't prevent a moderately funded adversary from overwhelming the system, but combined with the ability of groups to select and move to arbitrary servers, it makes targetted attacks on the communication of particular groups difficult.
# Signature Security
In order to preserve metadata resistant, the system requires that a participant requires access to the group and access to the peer identity in order to verify a message came from a given peer.
As such it is not sufficient to sign just the message, doing so would allow someone to spoof a message from a given peer by first obtaining a message in one group and then broadcasting it to another.
As such signatures in cwtch have the following structure
Sign(Message . Ciphertext)
The ciphertext is encrypted
# Lit Reviews
OnionPIR, requires registration of users, makes no attempt to solve challenges of malicious servers. Recommends blinded signatures as an approach to solve spam.
Time Epoch based systems.
Demmler, Daniel, Marco Holz, and Thomas Schneider. "OnionPIR: Effective Protection of Sensitive Metadata in Online Communication Networks." International Conference on Applied Cryptography and Network Security. Springer, Cham, 2017.
Corrigan-Gibbs, Henry, Dan Boneh, and David Mazières. "Riposte: An anonymous messaging system handling millions of users." Security and Privacy (SP), 2015 IEEE Symposium on. IEEE, 2015.

View File

@ -0,0 +1 @@
# Metdata-Resistant Protocols: An Overview

141
protocol.md Normal file
View File

@ -0,0 +1,141 @@
Title: Cwtch Protocol: Facilitating Anonymous Collaboration
Author: Sarah Jamie Lewis
Doc Class : [11pt, twocolumn]article
# Actors & Identity
All parties in a Cwtch System run ricochet nodes.
## Cwtch Client
A Cwtch Client identity consists of:
* A Name
* An Onion Address (and an associated private key)
* A Nacl Public Key (and an associated private key)
## Cwtch Server
A Cwtch Server identity consists of:
* A Name
* An Onion Address (and an associated private key)
# Threat Model
## Impersonation
One of the key attacks we must defend against in a multiple-party messaging
system is the risk of one party attempting to impersonate another.
There are two levels to this kind of the attack, systemic impersonation and superficial
impersonation.
Systemic Impersonation would result from the system being unable to distinguish between
two parties. Due to the anonymous nature of our approach, we adopt a long lived public key
that can be used to link & verify that seperate messages belong to the same party.
Superficial Impersonation is more tricky, it would result from a user being tricked to accepting
that two distinct identities are the same. We cannot rely on the user to compare public keys to avoid
this. An attacker may try and change their profile name to match a target - it should be noted that restricting profile names to be unique per Cwtch system is not sufficient to prevent this attack (because of homoglyph style attacks), nor is such a restriction desirable.
To prevent superficial style impersonation we take a multi step approach:
// TODO
# Client <-> Server Messages
When a client wishes to join a Cwtch system, they must first identify the address of the Cwtch server.
One a Cwtch server is known, the client connects to the Cwtch server over Ricochet, and performs a standard `im.ricochet.auth.hidden-service` authentication.
At this point the Cwtch Server initiates an `EnableFeatures` setup in an attempt to detect that the client is indeed a Cwtch client, or if it is a bare bones Ricochet client.
message ServerIdentify {
string name = 1;
string topic = 2;
int32 difficulty = 3;
}
If a Cwtch Client is detected, that the CwtchServer sets up a `im.cwtch.event` through which all other communication between the client and the server takes place.
enum EventType {
NIL = 0;
JOIN = 1;
LEAVE = 2;
MESSAGE = 3;
}
message Event {
EventType type = 1;
string client = 2;
int64 timestamp = 3;
string detail = 4;
bytes proof = 5;
}
## Event Types
* *JOIN* events are sent when a client connects to the server
* *LEAVE* events are sent when a client disconnects from the server
* *MESSAGE* events are sent when a client sends a message to the server, these are broadcast to every other connected client.
## Sending Messages
message Message {
string message = 1;
bytes signature = 2;
bytes spamguard = 3;
}
## Message Signatures
### Preventing Spam Through SpamGuard
To prevent abusive clients from flooding the server (and therefore other clients) with
a large number of message, all messages must include a complet `spamguard` field.
This field is `nonce|sha256(message.signature.nonce)`. Valid spamguard fields must
ensure that the sha256 digest begins with `difficulty` number of `0x00` as defined
by the server profile.
This requires that clients try a number of random nonces prior to sending any messages.
Message packaets with invalid SpamGuard digests will be discarded by the server.
To ensure that a client cannot find a single valid SpamGuard digest, e.g. for the message `hi`,
and use that to spam clients.
# Client <-> Client Messages
## ClientIdentify
When 2 clients are online at the same time, they can use the opporunity to exchange
`ClientIdentify` messages over an `im.cwtch.client.identify` channel:
message ClientIdentify {
string name = 1;
bytes ed25519_public_key = 2;
bytes nacl_public_key = 3;
}
* `name` is a readable name to help identify this client.
* `ed25519_public_key `is used to authenticate public messages from this client.
* `nacl_public_key` is used to authenticate private messages from this client.
Once a client has the Profile of another client, they can proceed to validate that
the messages they have received from the server via the `im.cwtch.event` that are tagged
as originating from the client do in fact validate.
## Private Messages
When both clients are online, private messages can be sent to eachother using an
`im.ricochet.chat` channel.
When either client is offline, private messages are sent utilizing the `im.cwtch.message` channel - instead of a regular message being sent, the message is begins `ENC-` and contains an encrypted message.
Note that there is no metadata associated with private messages, and as such each client must attempt to decrypt the message with their own keys, and discard messages that fail.

103
protocol/channel.go Normal file
View File

@ -0,0 +1,103 @@
package protocol
import (
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/utils"
"github.com/s-rah/go-ricochet/wire/control"
)
// CwtchPeerChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.Cwtch". The channel may be inbound or outbound.
//
// CwtchPeerChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchPeerChannelHandler implementation to handle Cwtch events.
type CwtchPeerChannel struct {
// Methods of Handler are called for Cwtch events on this channel
Handler CwtchPeerChannelHandler
channel *channels.Channel
}
// CwtchPeerChannelHandler is implemented by an application type to receive
// events from a CwtchPeerChannel.
type CwtchPeerChannelHandler interface {
ClientIdentity(*CwtchIdentity)
HandleGroupInvite()
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cpc *CwtchPeerChannel) SendMessage(data []byte) {
cpc.channel.SendMessage(data)
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cpc *CwtchPeerChannel) Type() string {
return "im.cwtch.peer"
}
// Closed is called when the channel is closed for any reason.
func (cpc *CwtchPeerChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cpc *CwtchPeerChannel) OnlyClientCanOpen() bool {
return false
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cpc *CwtchPeerChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cpc *CwtchPeerChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cpc *CwtchPeerChannel) RequiresAuthentication() string {
return "im.ricochet.auth.hidden-service"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cpc *CwtchPeerChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
cpc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.AckOpenChannel(channel.ID), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cpc *CwtchPeerChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cpc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cpc.Type()), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cpc *CwtchPeerChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cpc.channel.Pending = false
}
}
}
// Packet is called for each raw packet received on this channel.
func (cpc *CwtchPeerChannel) Packet(data []byte) {
cpp := &CwtchPeerPacket{}
err := proto.Unmarshal(data, cpp)
if err == nil {
if cpp.GetCwtchIdentify() != nil {
cpc.Handler.ClientIdentity(cpp.GetCwtchIdentify())
}
}
}

View File

@ -0,0 +1,124 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: cwtch-profile.proto
package protocol
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type CwtchPeerPacket struct {
CwtchIdentify *CwtchIdentity `protobuf:"bytes,1,opt,name=cwtch_identify,json=cwtchIdentify" json:"cwtch_identify,omitempty"`
GroupChatInvite *GroupChatInvite `protobuf:"bytes,2,opt,name=group_chat_invite,json=groupChatInvite" json:"group_chat_invite,omitempty"`
}
func (m *CwtchPeerPacket) Reset() { *m = CwtchPeerPacket{} }
func (m *CwtchPeerPacket) String() string { return proto.CompactTextString(m) }
func (*CwtchPeerPacket) ProtoMessage() {}
func (*CwtchPeerPacket) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} }
func (m *CwtchPeerPacket) GetCwtchIdentify() *CwtchIdentity {
if m != nil {
return m.CwtchIdentify
}
return nil
}
func (m *CwtchPeerPacket) GetGroupChatInvite() *GroupChatInvite {
if m != nil {
return m.GroupChatInvite
}
return nil
}
type CwtchIdentity struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Ed25519PublicKey []byte `protobuf:"bytes,2,opt,name=ed25519_public_key,json=ed25519PublicKey,proto3" json:"ed25519_public_key,omitempty"`
}
func (m *CwtchIdentity) Reset() { *m = CwtchIdentity{} }
func (m *CwtchIdentity) String() string { return proto.CompactTextString(m) }
func (*CwtchIdentity) ProtoMessage() {}
func (*CwtchIdentity) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{1} }
func (m *CwtchIdentity) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *CwtchIdentity) GetEd25519PublicKey() []byte {
if m != nil {
return m.Ed25519PublicKey
}
return nil
}
// [name] has invited you to join a group chat: [message]
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"`
}
func (m *GroupChatInvite) Reset() { *m = GroupChatInvite{} }
func (m *GroupChatInvite) String() string { return proto.CompactTextString(m) }
func (*GroupChatInvite) ProtoMessage() {}
func (*GroupChatInvite) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{2} }
func (m *GroupChatInvite) GetGroupName() string {
if m != nil {
return m.GroupName
}
return ""
}
func (m *GroupChatInvite) GetGroupSharedKey() []byte {
if m != nil {
return m.GroupSharedKey
}
return nil
}
func (m *GroupChatInvite) GetServerHost() string {
if m != nil {
return m.ServerHost
}
return ""
}
func init() {
proto.RegisterType((*CwtchPeerPacket)(nil), "protocol.CwtchPeerPacket")
proto.RegisterType((*CwtchIdentity)(nil), "protocol.CwtchIdentity")
proto.RegisterType((*GroupChatInvite)(nil), "protocol.GroupChatInvite")
}
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,
}

View File

@ -0,0 +1,19 @@
syntax = "proto3";
package protocol;
message CwtchPeerPacket {
CwtchIdentity cwtch_identify = 1;
GroupChatInvite group_chat_invite = 2;
}
message CwtchIdentity {
string name = 1;
bytes ed25519_public_key = 2;
}
// [name] has invited you to join a group chat: [message]
message GroupChatInvite {
string group_name = 1;
bytes group_shared_key = 2;
string server_host = 3;
}

View File

@ -0,0 +1,126 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: group_message.proto
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"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type CwtchServerPacket struct {
GroupMessage *GroupMessage `protobuf:"bytes,1,opt,name=group_message,json=groupMessage" json:"group_message,omitempty"`
FetchMessage *FetchMessage `protobuf:"bytes,2,opt,name=fetch_message,json=fetchMessage" json:"fetch_message,omitempty"`
GroupMessages []*GroupMessage `protobuf:"bytes,3,rep,name=group_messages,json=groupMessages" json:"group_messages,omitempty"`
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) GetGroupMessage() *GroupMessage {
if m != nil {
return m.GroupMessage
}
return nil
}
func (m *CwtchServerPacket) GetFetchMessage() *FetchMessage {
if m != nil {
return m.FetchMessage
}
return nil
}
func (m *CwtchServerPacket) GetGroupMessages() []*GroupMessage {
if m != nil {
return m.GroupMessages
}
return nil
}
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() {}
type GroupMessage struct {
Ciphertext []byte `protobuf:"bytes,1,req,name=ciphertext" json:"ciphertext,omitempty"`
Spamguard []byte `protobuf:"bytes,2,req,name=spamguard" json:"spamguard,omitempty"`
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) GetCiphertext() []byte {
if m != nil {
return m.Ciphertext
}
return nil
}
func (m *GroupMessage) GetSpamguard() []byte {
if m != nil {
return m.Spamguard
}
return nil
}
// DecryptedGroupMessage is *never* sent in the clear on the wire
// and is only ever sent when encrypted in the ciphertext parameter of
// 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"`
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) GetOnion() string {
if m != nil && m.Onion != nil {
return *m.Onion
}
return ""
}
func (m *DecryptedGroupMessage) GetText() string {
if m != nil && m.Text != nil {
return *m.Text
}
return ""
}
func (m *DecryptedGroupMessage) GetSignature() []byte {
if m != nil {
return m.Signature
}
return nil
}
var E_ServerNonce = &proto.ExtensionDesc{
ExtendedType: (*Protocol_Data_Control.ChannelResult)(nil),
ExtensionType: ([]byte)(nil),
Field: 8200,
Name: "im.cwtch.server_nonce",
Tag: "bytes,8200,opt,name=server_nonce",
}
func init() {
proto.RegisterExtension(E_ServerNonce)
}

View File

@ -0,0 +1,32 @@
syntax = "proto2";
package protocol;
import "ControlChannel.proto";
message CwtchServerPacket {
optional GroupMessage group_message = 1;
optional FetchMessage fetch_message = 2;
repeated GroupMessage group_messages = 3;
}
extend protocol.ChannelResult {
optional bytes server_nonce = 8200; // 32 random bytes
}
message FetchMessage {
}
message GroupMessage {
required bytes ciphertext = 1;
required bytes spamguard = 2;
}
// DecryptedGroupMessage is *never* sent in the clear on the wire
// and is only ever sent when encrypted in the ciphertext parameter of
// GroupMessage
message DecryptedGroupMessage {
required string onion = 1;
required int32 timestamp = 2;
required string text = 3;
required bytes signature = 4;
}

View File

@ -0,0 +1,84 @@
package spam
import (
"crypto/rand"
"crypto/sha256"
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/utils"
"github.com/s-rah/go-ricochet/wire/control"
"io"
)
type SpamGuard struct {
Difficulty int
nonce [24]byte
}
func (sg *SpamGuard) GenerateChallenge(channelID int32) []byte {
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(channelID),
Opened: proto.Bool(true),
}
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
utils.CheckError(err)
}
sg.nonce = nonce
err := proto.SetExtension(cr, protocol.E_ServerNonce, sg.nonce[:])
utils.CheckError(err)
pc := &Protocol_Data_Control.Packet{
ChannelResult: cr,
}
ret, err := proto.Marshal(pc)
utils.CheckError(err)
return ret
}
func (sg *SpamGuard) SolveChallenge(challenge []byte, message []byte) []byte {
solved := false
var spamguard [24]byte
sum := sha256.Sum256([]byte{})
solve := make([]byte, len(challenge)+len(message)+len(spamguard))
for !solved {
if _, err := io.ReadFull(rand.Reader, spamguard[:]); err != nil {
utils.CheckError(err)
}
copy(solve[0:], challenge[:])
copy(solve[len(challenge):], message[:])
copy(solve[len(challenge)+len(message):], spamguard[:])
sum = sha256.Sum256(solve)
solved = true
for i := 0; i < sg.Difficulty; i++ {
if sum[i] != 0x00 {
solved = false
}
}
}
//log.Printf("Solved answer: %v %x %x\n", len(solve), solve, sum)
return spamguard[:]
}
func (sg *SpamGuard) ValidateChallenge(message []byte, spamguard []byte) bool {
//log.Printf("%v %v\n", sg.nonce[:], spamguard[:])
solve := make([]byte, len(sg.nonce)+len(message)+len(spamguard))
copy(solve[0:], sg.nonce[:])
copy(solve[len(sg.nonce):], message[:])
copy(solve[len(sg.nonce)+len(message):], spamguard[:])
sum := sha256.Sum256(solve)
//log.Printf("Got answer: %v %x %x\n", len(solve), solve, sum)
for i := 0; i < sg.Difficulty; i++ {
if sum[i] != 0x00 {
return false
}
}
return true
}

View File

@ -0,0 +1,33 @@
package spam
import (
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/wire/control"
"testing"
)
func TestSpamGuard(t *testing.T) {
var spamGuard SpamGuard
spamGuard.Difficulty = 2
challenge := spamGuard.GenerateChallenge(3)
control := new(Protocol_Data_Control.Packet)
proto.Unmarshal(challenge[:], control)
if control.GetChannelResult() != nil {
ce, _ := proto.GetExtension(control.GetChannelResult(), protocol.E_ServerNonce)
challenge := ce.([]byte)[:]
sgsolve := spamGuard.SolveChallenge(challenge, []byte("Hello"))
t.Logf("Solved: %v %v", challenge, sgsolve)
result := spamGuard.ValidateChallenge([]byte("Hello"), sgsolve)
if result != true {
t.Errorf("Validating SpamGuard Failed")
}
return
}
t.Errorf("Failed SpamGaurd")
}

View File

@ -0,0 +1,101 @@
package fetch
import (
"errors"
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/utils"
"github.com/s-rah/go-ricochet/wire/control"
)
// CwtchChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.Cwtch". The channel may be inbound or outbound.
//
// CwtchChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchChannelHandler implementation to handle Cwtch events.
type CwtchServerFetchChannel struct {
Handler CwtchServerFetchHandler
channel *channels.Channel
}
type CwtchServerFetchHandler interface {
HandleFetchRequest() []*protocol.GroupMessage
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cc *CwtchServerFetchChannel) Type() string {
return "im.cwtch.server.fetch"
}
// Closed is called when the channel is closed for any reason.
func (cc *CwtchServerFetchChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cc *CwtchServerFetchChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cc *CwtchServerFetchChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cc *CwtchServerFetchChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cc *CwtchServerFetchChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cc *CwtchServerFetchChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
cc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.AckOpenChannel(channel.ID), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cc *CwtchServerFetchChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
return nil, errors.New("server does not open Fetch channels")
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cc *CwtchServerFetchChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
// NOTE: Should never be called
}
// SendGroupMessage
func (cc *CwtchServerFetchChannel) SendGroupMessages(gm []*protocol.GroupMessage) {
csp := &protocol.CwtchServerPacket{
GroupMessages: gm,
}
packet, _ := proto.Marshal(csp)
cc.channel.SendMessage(packet)
}
// Packet is called for each raw packet received on this channel.
func (cc *CwtchServerFetchChannel) Packet(data []byte) {
csp := &protocol.CwtchServerPacket{}
err := proto.Unmarshal(data, csp)
if err == nil {
if csp.GetFetchMessage() != nil {
cc.SendGroupMessages(cc.Handler.HandleFetchRequest())
}
}
// If we receive a packet on this channel, close the connection
cc.channel.CloseChannel()
}

View File

@ -0,0 +1,109 @@
package fetch
import (
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/wire/control"
"testing"
)
func TestServerFetchChannelAttributes(t *testing.T) {
cslc := new(CwtchServerFetchChannel)
if cslc.Type() != "im.cwtch.server.fetch" {
t.Errorf("cwtch channel type is incorrect %v", cslc.Type())
}
if !cslc.OnlyClientCanOpen() {
t.Errorf("only clients should be able to open im.cwtch.server.fetch channel")
}
if cslc.Bidirectional() {
t.Errorf("im.cwtch.server.fetch should not be bidirectional")
}
if !cslc.Singleton() {
t.Errorf("im.cwtch.server.fetch should be a Singleton")
}
if cslc.RequiresAuthentication() != "none" {
t.Errorf("cwtch channel required auth is incorrect %v", cslc.RequiresAuthentication())
}
}
func TestServerFetchChannelOpenOutbound(t *testing.T) {
cslc := new(CwtchServerFetchChannel)
channel := new(channels.Channel)
_, err := cslc.OpenOutbound(channel)
if err == nil {
t.Errorf("server implementation of im.cwtch.server.fetch should never open an outbound channel")
}
}
type TestHandler struct {
}
func (th *TestHandler) HandleFetchRequest() []*protocol.GroupMessage {
gm := &protocol.GroupMessage{
Ciphertext: []byte("Hello"),
Spamguard: []byte{},
}
return []*protocol.GroupMessage{gm}
}
func TestServerFetchChannel(t *testing.T) {
cslc := new(CwtchServerFetchChannel)
th := new(TestHandler)
cslc.Handler = th
channel := new(channels.Channel)
channel.ID = 1
closed := false
channel.CloseChannel = func() {
closed = true
}
gotgm := false
channel.SendMessage = func([]byte) {
gotgm = true
}
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(1),
ChannelType: proto.String(cslc.Type()),
}
resp, err := cslc.OpenInbound(channel, oc)
if err != nil {
t.Errorf("OpenInbound for im.cwtch.server.Fetch should have succeeded, instead: %v", err)
}
control := new(Protocol_Data_Control.Packet)
proto.Unmarshal(resp[:], control)
if control.GetChannelResult() != nil {
fm := &protocol.FetchMessage{}
csp := &protocol.CwtchServerPacket{
FetchMessage: fm,
}
packet, _ := proto.Marshal(csp)
cslc.Packet(packet)
if !gotgm {
t.Errorf("Did not receive packet on wire as expected in Fetch channel")
}
if !closed {
t.Errorf("Fetch channel should be cosed")
}
if !closed {
t.Errorf("Fetch channel should be closed after incorrect packet received")
}
} else {
t.Errorf("Expected ChannelResult from im.cwtch.server.Fetch, instead: %v", control)
}
}

View File

@ -0,0 +1,89 @@
package listen
import (
"errors"
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/utils"
"github.com/s-rah/go-ricochet/wire/control"
)
// CwtchChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.Cwtch". The channel may be inbound or outbound.
//
// CwtchChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchChannelHandler implementation to handle Cwtch events.
type CwtchServerListenChannel struct {
channel *channels.Channel
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cc *CwtchServerListenChannel) Type() string {
return "im.cwtch.server.listen"
}
// Closed is called when the channel is closed for any reason.
func (cc *CwtchServerListenChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cc *CwtchServerListenChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cc *CwtchServerListenChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cc *CwtchServerListenChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cc *CwtchServerListenChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cc *CwtchServerListenChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
cc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.AckOpenChannel(channel.ID), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cc *CwtchServerListenChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
return nil, errors.New("server does not open listen channels")
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cc *CwtchServerListenChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
// NOTE: Should never be called
}
// SendGroupMessage
func (cc *CwtchServerListenChannel) SendGroupMessage(gm *protocol.GroupMessage) {
csp := &protocol.CwtchServerPacket{
GroupMessage: gm,
}
packet, _ := proto.Marshal(csp)
cc.channel.SendMessage(packet)
}
// Packet is called for each raw packet received on this channel.
func (cc *CwtchServerListenChannel) Packet(data []byte) {
// If we receive a packet on this channel, close the connection
cc.channel.CloseChannel()
}

View File

@ -0,0 +1,93 @@
package listen
import (
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/wire/control"
"testing"
)
func TestServerListenChannelAttributes(t *testing.T) {
cslc := new(CwtchServerListenChannel)
if cslc.Type() != "im.cwtch.server.listen" {
t.Errorf("cwtch channel type is incorrect %v", cslc.Type())
}
if !cslc.OnlyClientCanOpen() {
t.Errorf("only clients should be able to open im.cwtch.server.listen channel")
}
if cslc.Bidirectional() {
t.Errorf("im.cwtch.server.listen should not be bidirectional")
}
if !cslc.Singleton() {
t.Errorf("im.cwtch.server.listen should be a Singleton")
}
if cslc.RequiresAuthentication() != "none" {
t.Errorf("cwtch channel required auth is incorrect %v", cslc.RequiresAuthentication())
}
}
func TestServerListenChannelOpenOutbound(t *testing.T) {
cslc := new(CwtchServerListenChannel)
channel := new(channels.Channel)
_, err := cslc.OpenOutbound(channel)
if err == nil {
t.Errorf("server implementation of im.cwtch.server.listen should never open an outbound channel")
}
}
func TestServerListenChannel(t *testing.T) {
cslc := new(CwtchServerListenChannel)
channel := new(channels.Channel)
channel.ID = 1
closed := false
channel.CloseChannel = func() {
closed = true
}
gotgm := false
channel.SendMessage = func([]byte) {
gotgm = true
}
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(1),
ChannelType: proto.String(cslc.Type()),
}
resp, err := cslc.OpenInbound(channel, oc)
if err != nil {
t.Errorf("OpenInbound for im.cwtch.server.listen should have succeeded, instead: %v", err)
}
control := new(Protocol_Data_Control.Packet)
proto.Unmarshal(resp[:], control)
if control.GetChannelResult() != nil {
gm := &protocol.GroupMessage{
Ciphertext: []byte("Hello"),
Spamguard: []byte{},
}
cslc.SendGroupMessage(gm)
if !gotgm {
t.Errorf("Did not receive packet on wire as expected in listen channel")
}
if closed {
t.Errorf("listen channel should not be cosed")
}
cslc.Packet(nil)
if !closed {
t.Errorf("listen channel should be closed after incorrect packet received")
}
} else {
t.Errorf("Expected ChannelResult from im.cwtch.server.listen, instead: %v", control)
}
}

1
server/ms.test Normal file
View File

@ -0,0 +1 @@
{"ciphertext":"SGVsbG8gdGhpcyBpcyBhIGZhaXJseSBhdmVyYWdlIGxlbmd0aCBtZXNzYWdlIHRoYXQgd2UgYXJlIHdyaXRpbmcgaGVyZS4="}

View File

@ -0,0 +1,99 @@
package send
import (
"errors"
"git.mascherari.press/cwtch/protocol"
"git.mascherari.press/cwtch/protocol/spam"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/wire/control"
)
// CwtchChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.Cwtch". The channel may be inbound or outbound.
//
// CwtchChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchChannelHandler implementation to handle Cwtch events.
type CwtchServerSendChannel struct {
// Methods of Handler are called for Cwtch events on this channel
Handler CwtchServerSendChannelHandler
channel *channels.Channel
spamguard spam.SpamGuard
}
// CwtchChannelHandler is implemented by an application type to receive
// events from a CwtchChannel.
type CwtchServerSendChannelHandler interface {
HandleGroupMessage(*protocol.GroupMessage)
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cc *CwtchServerSendChannel) Type() string {
return "im.cwtch.server.send"
}
// Closed is called when the channel is closed for any reason.
func (cc *CwtchServerSendChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cc *CwtchServerSendChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cc *CwtchServerSendChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cc *CwtchServerSendChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cc *CwtchServerSendChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cc *CwtchServerSendChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
cc.channel = channel
cc.spamguard.Difficulty = 2
return cc.spamguard.GenerateChallenge(channel.ID), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cc *CwtchServerSendChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
return nil, errors.New("server does not open send channel")
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cc *CwtchServerSendChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
// NOTE: Should never be called
}
// Packet is called for each raw packet received on this channel.
func (cc *CwtchServerSendChannel) Packet(data []byte) {
csp := &protocol.CwtchServerPacket{}
err := proto.Unmarshal(data, csp)
if err == nil {
if csp.GetGroupMessage() != nil {
gm := csp.GetGroupMessage()
ok := cc.spamguard.ValidateChallenge(gm.GetCiphertext(), gm.GetSpamguard())
if ok {
cc.Handler.HandleGroupMessage(gm)
}
}
}
cc.channel.CloseChannel()
}

View File

@ -0,0 +1,172 @@
package send
import (
"git.mascherari.press/cwtch/protocol"
"git.mascherari.press/cwtch/protocol/spam"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/wire/control"
"testing"
)
type TestHandler struct {
Received bool
}
func (th *TestHandler) HandleGroupMessage(m *protocol.GroupMessage) {
th.Received = true
}
func TestServerSendChannelAttributes(t *testing.T) {
cssc := new(CwtchServerSendChannel)
if cssc.Type() != "im.cwtch.server.send" {
t.Errorf("cwtch channel type is incorrect %v", cssc.Type())
}
if !cssc.OnlyClientCanOpen() {
t.Errorf("only clients should be able to open im.cwtch.server.send channel")
}
if cssc.Bidirectional() {
t.Errorf("im.cwtch.server.send should not be bidirectional")
}
if !cssc.Singleton() {
t.Errorf("im.cwtch.server.send should be a Singleton")
}
if cssc.RequiresAuthentication() != "none" {
t.Errorf("cwtch channel required auth is incorrect %v", cssc.RequiresAuthentication())
}
}
func TestServerSendChannelOpenOutbound(t *testing.T) {
cssc := new(CwtchServerSendChannel)
channel := new(channels.Channel)
_, err := cssc.OpenOutbound(channel)
if err == nil {
t.Errorf("server implementation of im.cwtch.server.send should never open an outbound channel")
}
}
func TestServerSendChannel(t *testing.T) {
cssc := new(CwtchServerSendChannel)
th := new(TestHandler)
cssc.Handler = th
channel := new(channels.Channel)
channel.ID = 1
closed := false
channel.CloseChannel = func() {
closed = true
}
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(1),
ChannelType: proto.String(cssc.Type()),
}
resp, err := cssc.OpenInbound(channel, oc)
if err != nil {
t.Errorf("OpenInbound for im.cwtch.server.send should have succeeded, instead: %v", err)
}
control := new(Protocol_Data_Control.Packet)
proto.Unmarshal(resp[:], control)
if control.GetChannelResult() != nil {
var spamguard spam.SpamGuard
spamguard.Difficulty = 2
ce, _ := proto.GetExtension(control.GetChannelResult(), protocol.E_ServerNonce)
challenge := ce.([]byte)[:]
sgsolve := spamguard.SolveChallenge(challenge, []byte("Hello"))
//t.Logf("Solved: %x", sgsolve)
gm := &protocol.GroupMessage{
Ciphertext: []byte("Hello"),
Spamguard: sgsolve,
}
csp := &protocol.CwtchServerPacket{
GroupMessage: gm,
}
packet, _ := proto.Marshal(csp)
cssc.Packet(packet)
if !th.Received {
t.Errorf("group message should have been received")
}
if !closed {
t.Errorf("im.cwtch.server.send should have been closed after use")
}
} else {
t.Errorf("Expected ChannelResult from im.cwtch.server.send, instead: %v", control)
}
}
func TestServerSendChannelNoSpamGuard(t *testing.T) {
cssc := new(CwtchServerSendChannel)
th := new(TestHandler)
th.Received = false
cssc.Handler = th
channel := new(channels.Channel)
channel.ID = 1
closed := false
channel.CloseChannel = func() {
closed = true
}
oc := &Protocol_Data_Control.OpenChannel{
ChannelIdentifier: proto.Int32(1),
ChannelType: proto.String(cssc.Type()),
}
resp, err := cssc.OpenInbound(channel, oc)
if err != nil {
t.Errorf("OpenInbound for im.cwtch.server.send should have succeeded, instead: %v", err)
}
control := new(Protocol_Data_Control.Packet)
proto.Unmarshal(resp[:], control)
if control.GetChannelResult() != nil {
var spamguard spam.SpamGuard
spamguard.Difficulty = 2
ce, _ := proto.GetExtension(control.GetChannelResult(), protocol.E_ServerNonce)
challenge := ce.([]byte)[:]
sgsolve := spamguard.SolveChallenge(challenge, []byte("4234"))
//t.Logf("Solved: %x", sgsolve)
gm := &protocol.GroupMessage{
Ciphertext: []byte("hello"),
Spamguard: sgsolve,
}
csp := &protocol.CwtchServerPacket{
GroupMessage: gm,
}
packet, _ := proto.Marshal(csp)
cssc.Packet(packet)
if th.Received == true {
t.Errorf("group message should not have been received")
}
if !closed {
t.Errorf("im.cwtch.server.send should have been closed after use")
}
} else {
t.Errorf("Expected ChannelResult from im.cwtch.server.send, instead: %v", control)
}
}

68
server/server.go Normal file
View File

@ -0,0 +1,68 @@
package server
import (
//"crypto/rsa"
"git.mascherari.press/cwtch/server/fetch"
"git.mascherari.press/cwtch/server/listen"
"git.mascherari.press/cwtch/server/send"
"git.mascherari.press/cwtch/storage"
"github.com/s-rah/go-ricochet/application"
"github.com/s-rah/go-ricochet/channels"
"github.com/s-rah/go-ricochet/utils"
"log"
//"time"
)
type Server struct {
}
func (s *Server) Run(privateKeyFile string) {
cwtchserver := new(application.RicochetApplication)
pk, err := utils.LoadPrivateKeyFromFile(privateKeyFile)
if err != nil {
log.Fatalf("error reading private key file: %v", err)
}
l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", pk, 9878)
if err != nil {
log.Fatalf("error setting up onion service: %v", err)
}
af := application.ApplicationInstanceFactory{}
af.Init()
ms := new(storage.MessageStore)
ms.Init("cwtch.messages")
af.AddHandler("im.cwtch.server.listen", func(rai *application.ApplicationInstance) func() channels.Handler {
si := new(ServerInstance)
si.Init(rai, cwtchserver, ms)
return func() channels.Handler {
cslc := new(listen.CwtchServerListenChannel)
return cslc
}
})
af.AddHandler("im.cwtch.server.fetch", func(rai *application.ApplicationInstance) func() channels.Handler {
si := new(ServerInstance)
si.Init(rai, cwtchserver, ms)
return func() channels.Handler {
cssc := new(fetch.CwtchServerFetchChannel)
cssc.Handler = si
return cssc
}
})
af.AddHandler("im.cwtch.server.send", func(rai *application.ApplicationInstance) func() channels.Handler {
si := new(ServerInstance)
si.Init(rai, cwtchserver, ms)
return func() channels.Handler {
cssc := new(send.CwtchServerSendChannel)
cssc.Handler = si
return cssc
}
})
cwtchserver.Init(pk, af, new(application.AcceptAllContactManager))
cwtchserver.Run(l)
}

42
server/server_instance.go Normal file
View File

@ -0,0 +1,42 @@
package server
import (
"git.mascherari.press/cwtch/protocol"
"git.mascherari.press/cwtch/server/listen"
"git.mascherari.press/cwtch/storage"
"github.com/s-rah/go-ricochet/application"
"github.com/s-rah/go-ricochet/channels"
)
type ServerInstance struct {
rai *application.ApplicationInstance
ra *application.RicochetApplication
msi storage.MessageStoreInterface
}
func (si *ServerInstance) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication, msi storage.MessageStoreInterface) {
si.rai = rai
si.ra = ra
si.msi = msi
}
func (si *ServerInstance) HandleFetchRequest() []*protocol.GroupMessage {
return si.msi.FetchMessages()
}
// HandleGroupMessage
func (si *ServerInstance) HandleGroupMessage(gm *protocol.GroupMessage) {
si.msi.AddMessage(*gm)
go si.ra.Broadcast(func(rai *application.ApplicationInstance) {
rai.Connection.Do(func() error {
channel := rai.Connection.Channel("im.cwtch.server.listen", channels.Inbound)
if channel != nil {
cslc, ok := channel.Handler.(*listen.CwtchServerListenChannel)
if ok {
cslc.SendGroupMessage(gm)
}
}
return nil
})
})
}

View File

@ -0,0 +1,30 @@
package server
import (
"git.mascherari.press/cwtch/protocol"
"git.mascherari.press/cwtch/storage"
"github.com/s-rah/go-ricochet/application"
"os"
"testing"
)
func TestServerInstance(t *testing.T) {
si := new(ServerInstance)
ai := new(application.ApplicationInstance)
ra := new(application.RicochetApplication)
msi := new(storage.MessageStore)
os.Remove("ms.test")
msi.Init("ms.test")
gm := protocol.GroupMessage{
Ciphertext: []byte("Hello this is a fairly average length message that we are writing here."),
Spamguard: []byte{},
}
si.Init(ai, ra, msi)
msi.AddMessage(gm)
res := si.HandleFetchRequest()
if len(res) != 1 {
t.Errorf("Expected Group Messages Instead got %v", res)
}
}

67
storage/message_store.go Normal file
View File

@ -0,0 +1,67 @@
package storage
import (
"bufio"
"encoding/json"
"fmt"
"git.mascherari.press/cwtch/protocol"
"os"
"sync"
)
type MessageStoreInterface interface {
AddMessage(protocol.GroupMessage)
FetchMessages() []*protocol.GroupMessage
}
type MessageStore struct {
file *os.File
lock sync.Mutex
messages []*protocol.GroupMessage
}
func (ms *MessageStore) Close() {
ms.messages = nil
ms.file.Close()
}
func (ms *MessageStore) Init(filename string) {
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
if err != nil {
panic(err)
}
ms.file = f
scanner := bufio.NewScanner(f)
for scanner.Scan() {
gms := scanner.Text()
gm := &protocol.GroupMessage{}
err := json.Unmarshal([]byte(gms), gm)
if err == nil {
ms.messages = append(ms.messages, gm)
} else {
panic(err)
}
}
if err := scanner.Err(); err != nil {
panic(err)
}
}
func (ms *MessageStore) FetchMessages() (messages []*protocol.GroupMessage) {
messages = make([]*protocol.GroupMessage, len(ms.messages))
ms.lock.Lock()
copy(messages, ms.messages)
ms.lock.Unlock()
return
}
func (ms *MessageStore) AddMessage(gm protocol.GroupMessage) {
ms.lock.Lock()
ms.messages = append(ms.messages, &gm)
s, _ := json.Marshal(gm)
fmt.Fprintf(ms.file, "%s\n", s)
ms.lock.Unlock()
}

View File

@ -0,0 +1,29 @@
package storage
import (
"git.mascherari.press/cwtch/protocol"
"os"
"strconv"
"testing"
)
func TestMessageStore(t *testing.T) {
os.Remove("ms.test")
ms := new(MessageStore)
ms.Init("ms.test")
for i := 0; i < 100000; i++ {
gm := protocol.GroupMessage{
Ciphertext: []byte("Hello this is a fairly average length message that we are writing here. " + strconv.Itoa(i)),
Spamguard: []byte{},
}
ms.AddMessage(gm)
}
ms.Close()
ms.Init("ms.test")
m := ms.FetchMessages()
if len(m) != 100000 {
t.Errorf("Should have been 100000 was %v", len(m))
}
ms.Close()
os.Remove("ms.test")
}

18
todo.md Normal file
View File

@ -0,0 +1,18 @@
# Protocol Work
[ ] Cwtch Paper Plan
[X] Message channel with Spam guard
[ ] Profile Exchange Channel
[ ] Download all messages from server.
[ ] Server timeout
[ ] Server profile update
[ ] Complete threat model/protocol doc
[ ] Offline private messaging
# UI Work
[ ] Scroll back in the UX
[ ] Set Profile Name
Offline Anonymous Group Chat
- Define Threat Model
- Contrast with Other Approaches (SoK paper)

15
ui/action.go Normal file
View File

@ -0,0 +1,15 @@
package ui
type ActionType int
const (
NONE ActionType = iota
OPEN
SEND
)
type Action struct {
Type ActionType
ID string
Context string
}

33
ui/chat_screen.go Normal file
View File

@ -0,0 +1,33 @@
package ui
type ChatScreen struct {
Title string
Elements []Element
Position int
GroupID string
MessageBoxText string
}
func (cs *ChatScreen) Title() string {
return cs.Title
}
func (cs *ChatScreen) MoveUp() {
if cs.Position > 0 {
cs.Position -= 1
}
}
func (cs *ChatScreen) MoveDown() {
if cs.Position < len(cs.Elements)-1 {
cs.Position += 1
}
}
func (cs *ChatScreen) Transition() {
}
func (cs *ChatScreen) Action() Action {
return Action{SEND, cs.GroupID, cs.MessageBoxText}
}

37
ui/contact_screen.go Normal file
View File

@ -0,0 +1,37 @@
package ui
type ContactScreen struct {
Title string
Elements []Element
Position int
InSearch bool
}
func (cs *ContactScreen) Title() string {
return cs.Title
}
func (cs *ContactScreen) MoveUp() {
if cs.Position > 0 {
cs.Position -= 1
}
}
func (cs *ContactScreen) MoveDown() {
if cs.Position < len(cs.Elements)-1 {
cs.Position += 1
}
}
func (cs *ContactScreen) Transition() {
cs.InSearch = !cs.InSearch
}
func (cs *ContactScreen) Action() Action {
if cs.InSearch {
return Action{NONE, "", ""}
} else {
return Action{OPEN, cs.Elements[cs.Position].ID, ""}
}
return Action{NONE, "", ""}
}

30
ui/contact_screen_test.go Normal file
View File

@ -0,0 +1,30 @@
package ui
import (
"testing"
)
func TestBasicOperations(t *testing.T) {
screen := new(ContactScreen)
screen.Elements = []Element{{"alice", "1"}, {"bob", "2"}, {"carol", "3"}}
screen.MoveDown()
screen.MoveDown()
action := screen.Action()
if action.Type != OPEN || action.ID != "carol" {
t.Errorf("action sequence should have opened carol instead %v", action)
}
screen.MoveUp()
action = screen.Action()
if action.Type != OPEN || action.ID != "bob" {
t.Errorf("action sequence should have opened bob instead %v", action)
}
screen.Transition()
action = screen.Action()
if action.Type != NONE {
t.Errorf("action sequence should done nothing instead %v", action)
}
}

127
ui/layout.go Normal file
View File

@ -0,0 +1,127 @@
package ui
import (
"fmt"
"github.com/fatih/color"
"github.com/jroimartin/gocui"
)
type Element struct {
ID string
Contents string
}
type Layout struct {
Title string
Elements []Element
Position int
}
func (l *Layout) Setup(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("title", -1, -1, maxX, 1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Clear()
v.SetCursor(0, 0)
v.SetOrigin(0, 0)
v.Frame = false
//fmt.Fprintf(v, "%v %v",l.Title, l.Position)
}
if v, err := g.SetView("elements", -1, 0, maxX, maxY-2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Clear()
v.Frame = false
v.SetCursor(0, 0)
v.SetOrigin(0, 0)
if _, err := g.SetCurrentView("elements"); err != nil {
return err
}
}
l.Render(g)
return nil
}
func (l *Layout) Render(g *gocui.Gui) error {
maxX, maxY := g.Size()
colorTitle := color.New(color.BgMagenta).Sprintf
//
colorTitle2 := color.New(color.FgWhite, color.BgMagenta, color.Bold).Sprintf
if v, err := g.SetCurrentView("title"); err == nil {
v.Clear()
v.SetCursor(0, 0)
v.SetOrigin(0, 0)
title := "cwtch ♥ anonymity"
center := (maxX / 2) - (len(title) / 2)
for i := 0; i < center; i++ {
v.Write([]byte(colorTitle(" ")))
}
v.Write([]byte(colorTitle2(title)))
//v.Write([]byte(colorTitle2()))
for i := len(title); i < maxX+1; i++ {
v.Write([]byte(colorTitle(" ")))
}
/** pandora := "cwtch " + l.Title
for i := 0; i < len([]rune(pandora)); i++ {
v.SetRune(i+1, 0, rune(pandora[i]), gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta)
}
v.SetRune(7, 0, '♥', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta)*/
}
if v, err := g.SetCurrentView("elements"); err == nil {
v.Clear()
v.SetCursor(0, 0)
v.SetOrigin(0, 0)
numElementsToShow := (maxY - 3)
listSelected := color.New(color.BgWhite, color.FgWhite, color.Bold).Sprintf
listAlt1 := color.New(color.BgWhite, color.FgBlack).Sprintf
//listAlt2:= color.New(color.BgBlack, color.FgWhite, color.Faint).Sprintf
fmt.Fprintf(v, "%v", listSelected(" "+l.Elements[l.Position].Contents))
for i := 0; i < maxX+1; i++ {
v.Write([]byte(listSelected(" ")))
}
fmt.Fprintln(v, "")
for i := l.Position + 1; i < numElementsToShow; i++ {
if i < len(l.Elements) {
// if i%2 != 0 {
fmt.Fprintf(v, "%v", listAlt1(" "+l.Elements[i].Contents))
for i := 0; i < maxX; i++ {
v.Write([]byte(listAlt1(" ")))
}
fmt.Fprintln(v, "")
}
}
}
return nil
}
func (l *Layout) MoveUp() {
if l.Position > 0 {
l.Position -= 1
}
}
func (l *Layout) MoveDown() {
if l.Position < len(l.Elements)-1 {
l.Position += 1
}
}

9
ui/screen.go Normal file
View File

@ -0,0 +1,9 @@
package ui
type Screen interface {
Title() string
MoveUp()
MoveDown()
Transition() string
Action() Action
}

149
ui/state.go Normal file
View File

@ -0,0 +1,149 @@
package ui
/**
import (
"fmt"
"github.com/fatih/color"
"github.com/jroimartin/gocui"
)
type Screen struct {
Title string
Log []string
LogPosition int
IsGroup bool
IsWelcome bool
}
func (sc *Screen) AppendToLog(log string) {
sc.Log = append(sc.Log, log)
sc.LogPosition++
}
type State struct {
CurrentScreen int
Screens []Screen
Gui *gocui.Gui
}
func (s *State) Render() error {
g := s.Gui
maxX, maxY := g.Size()
if v, err := g.SetCurrentView("out"); err == nil {
v.Clear()
v.SetCursor(0, 0)
v.SetOrigin(0, 0)
screen := s.Screens[s.CurrentScreen]
for i := 0; i < len(screen.Log); i++ {
v.Write([]byte(screen.Log[i] + "\n"))
}
}
if v, err := g.SetCurrentView("menu"); err == nil {
s.RenderMenu(g, v, s.Screens[s.CurrentScreen])
} else {
return err
}
if _, err := g.SetCurrentView("cmd"); err == nil {
for i := 0; i < maxX; i++ {
g.SetRune(i, maxY-2, '─', gocui.ColorWhite, gocui.ColorBlack)
}
for i := 0; i < maxX; i++ {
g.SetRune(i, 0, ' ', gocui.ColorWhite, gocui.ColorMagenta)
}
g.SetRune(0, maxY-1, '>', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta)
g.SetRune(1, maxY-1, ' ', gocui.ColorBlack, 0)
} else {
return err
}
pandora := "cwtch " + s.Screens[s.CurrentScreen].Title
for i := 0; i < len([]rune(pandora)); i++ {
g.SetRune(i+1, 0, rune(pandora[i]), gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta)
}
g.SetRune(7, 0, '♥', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta)
return nil
}
func (s *State) NewScreen(title string, isgroup bool) {
s.Screens = append(s.Screens, Screen{Title: title, IsGroup: isgroup, IsWelcome: (len(s.Screens) == 0)})
}
func (s *State) GetScreen(num int) *Screen {
if len(s.Screens) > num {
return &s.Screens[num]
}
return &s.Screens[0]
}
func (s *State) PreviousScreen() {
if len(s.Screens) > 1 {
if s.CurrentScreen == 0 {
s.CurrentScreen = len(s.Screens) - 1
} else {
s.CurrentScreen -= 1
}
}
}
func (s *State) GotoScreen(num int) {
if len(s.Screens) > num {
s.CurrentScreen = num
s.Render()
}
}
func (s *State) NextScreen() {
if len(s.Screens) == 1 || s.CurrentScreen == len(s.Screens)-1 {
s.CurrentScreen = 0
} else {
s.CurrentScreen += 1
}
}
func String(c int, str string) string {
return fmt.Sprintf("\x1b[38;5;%dm%s\x1b[0m", c, str)
}
func (s *State) RenderMenu(g *gocui.Gui, v *gocui.View, sc Screen) {
v.Clear()
v.SetCursor(0, 0)
v.SetOrigin(0, 0)
colorGroupChat := color.New(color.FgMagenta, color.Bold).Sprintf
colorPersonalChat := color.New(color.FgGreen, color.Bold).Sprintf
colorMenu := color.New(color.FgYellow, color.Bold).Sprintf
colorNumber := color.New(color.FgWhite).Sprintf
colorSelected := color.New(color.FgGreen, color.Bold).Sprintf
var group string
if sc.IsGroup {
group = colorGroupChat("G")
} else if !sc.IsWelcome {
group = colorPersonalChat("P")
} else {
group = colorMenu("W")
}
var screenSelection string
for i := 0; i < s.CurrentScreen; i++ {
screenSelection += colorNumber("%d ", i)
}
screenSelection += colorSelected("*%d:%s ", s.CurrentScreen, sc.Title)
for i := s.CurrentScreen + 1; i < len(s.Screens); i++ {
screenSelection += colorNumber("%d ", i)
}
v.Write([]byte(fmt.Sprintf("⣿ %s [%s] ⡇%s",
colorMenu("MENU"),
group,
screenSelection)))
}*/

57
ux/main.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
//"fmt"
"git.mascherari.press/cwtch/ui"
"github.com/jroimartin/gocui"
"log"
)
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
var layout ui.Layout
func cursorDown(g *gocui.Gui, v *gocui.View) error {
layout.MoveDown()
// layout.Position = 2
layout.Render(g)
return nil
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
layout.MoveUp()
// layout.Position = 3
return layout.Render(g)
}
func main() {
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.Cursor = false
layout.Title = "Cwtch"
layout.Position = 1
layout.Elements = []ui.Element{{"Alice"}, {"Bob"}, {"Carol"}, {"Malory"}}
g.SetManagerFunc(layout.Setup)
if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
//return err
}
if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
//return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}