Browse Source

Squah commit

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

9
README.md

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

83
client/client.go

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

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

@ -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
client/fetch/peer_fetch_channel.go

@ -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
client/fetch/peer_fetch_channel_test.go

@ -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
client/listen/peer_listen_channel.go

@ -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
client/listen/peer_listen_channel_test.go

@ -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
client/send/peer_send_channel.go

@ -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
client/send/peer_send_channel_test.go

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

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

106
editor.go

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

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

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

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

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

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

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

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

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