Browse Source

Squah commit

blocking-thread-safe
Sarah Jamie Lewis 2 years ago
commit
5d7bdce118
50 changed files with 3864 additions and 0 deletions
  1. +9
    -0
      README.md
  2. +83
    -0
      client/client.go
  3. +175
    -0
      client/cwtch_peer.go
  4. +15
    -0
      client/cwtch_peer_test.go
  5. +89
    -0
      client/fetch/peer_fetch_channel.go
  6. +89
    -0
      client/fetch/peer_fetch_channel_test.go
  7. +97
    -0
      client/listen/peer_listen_channel.go
  8. +87
    -0
      client/listen/peer_listen_channel_test.go
  9. +102
    -0
      client/send/peer_send_channel.go
  10. +111
    -0
      client/send/peer_send_channel_test.go
  11. +1
    -0
      client/test_profile
  12. +106
    -0
      editor.go
  13. +231
    -0
      main.go
  14. +60
    -0
      model/group.go
  15. +16
    -0
      model/group_test.go
  16. +13
    -0
      model/message.go
  17. +149
    -0
      model/profile.go
  18. +1
    -0
      model/profile_test
  19. +78
    -0
      model/profile_test.go
  20. +314
    -0
      papers/metadata-resistant-group-chat.md
  21. +1
    -0
      papers/metadata-resistant-protocols.md
  22. +141
    -0
      protocol.md
  23. +103
    -0
      protocol/channel.go
  24. +124
    -0
      protocol/cwtch-profile.pb.go
  25. +19
    -0
      protocol/cwtch-profile.proto
  26. +126
    -0
      protocol/group_message.pb.go
  27. +32
    -0
      protocol/group_message.proto
  28. +84
    -0
      protocol/spam/spamguard.go
  29. +33
    -0
      protocol/spam/spamguard_test.go
  30. +101
    -0
      server/fetch/server_fetch_channel.go
  31. +109
    -0
      server/fetch/server_fetch_channel_test.go
  32. +89
    -0
      server/listen/server_listen_channel.go
  33. +93
    -0
      server/listen/server_listen_channel_test.go
  34. +1
    -0
      server/ms.test
  35. +99
    -0
      server/send/server_send_channel.go
  36. +172
    -0
      server/send/server_send_channel_test.go
  37. +68
    -0
      server/server.go
  38. +42
    -0
      server/server_instance.go
  39. +30
    -0
      server/server_instance_test.go
  40. +67
    -0
      storage/message_store.go
  41. +29
    -0
      storage/message_store_test.go
  42. +18
    -0
      todo.md
  43. +15
    -0
      ui/action.go
  44. +33
    -0
      ui/chat_screen.go
  45. +37
    -0
      ui/contact_screen.go
  46. +30
    -0
      ui/contact_screen_test.go
  47. +127
    -0
      ui/layout.go
  48. +9
    -0
      ui/screen.go
  49. +149
    -0
      ui/state.go
  50. +57
    -0
      ux/main.go

+ 9
- 0
README.md View File

@@ -0,0 +1,9 @@
# Cwtch

## Minimal Viable Product Feature Set

* Connecting Dialog
* Profiles
* Server Setup
* Verification
* Peer-2-Peer Chat

+ 83
- 0
client/client.go 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
- 0
client/cwtch_peer.go 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
- 0
client/cwtch_peer_test.go 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)
}

+ 89
- 0
client/fetch/peer_fetch_channel.go 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()
}

+ 89
- 0
client/fetch/peer_fetch_channel_test.go 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)

}

+ 97
- 0
client/listen/peer_listen_channel.go 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)
}
}
}

+ 87
- 0
client/listen/peer_listen_channel_test.go 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)

}

+ 102
- 0
client/send/peer_send_channel.go 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()
}

+ 111
- 0
client/send/peer_send_channel_test.go 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
- 0
client/test_profile View File

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

+ 106
- 0
editor.go 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
- 0
main.go 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
- 0
model/group.go 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
- 0
model/group_test.go 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
- 0
model/message.go 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
- 0
model/profile.go 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
- 0
model/profile_test 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
- 0
model/profile_test.go 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)
}
}

+ 314
- 0
papers/metadata-resistant-group-chat.md 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.







































































+ 1
- 0
papers/metadata-resistant-protocols.md View File

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

+ 141
- 0
protocol.md 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
- 0
protocol/channel.go 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())
}
}
}

+ 124
- 0
protocol/cwtch-profile.pb.go View File

@@ -0,0 +1,124 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: cwtch-profile.proto