From 5d7bdce118e87e5b462e76d1f507b04eb6805027 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 9 Mar 2018 12:44:13 -0800 Subject: [PATCH] Squah commit --- README.md | 9 + client/client.go | 83 ++++++ client/cwtch_peer.go | 175 +++++++++++ client/cwtch_peer_test.go | 15 + client/fetch/peer_fetch_channel.go | 89 ++++++ client/fetch/peer_fetch_channel_test.go | 89 ++++++ client/listen/peer_listen_channel.go | 97 ++++++ client/listen/peer_listen_channel_test.go | 87 ++++++ client/send/peer_send_channel.go | 102 +++++++ client/send/peer_send_channel_test.go | 111 +++++++ client/test_profile | 1 + editor.go | 106 +++++++ main.go | 231 ++++++++++++++ model/group.go | 60 ++++ model/group_test.go | 16 + model/message.go | 13 + model/profile.go | 149 ++++++++++ model/profile_test | 1 + model/profile_test.go | 78 +++++ papers/metadata-resistant-group-chat.md | 314 ++++++++++++++++++++ papers/metadata-resistant-protocols.md | 1 + protocol.md | 141 +++++++++ protocol/channel.go | 103 +++++++ protocol/cwtch-profile.pb.go | 124 ++++++++ protocol/cwtch-profile.proto | 19 ++ protocol/group_message.pb.go | 126 ++++++++ protocol/group_message.proto | 32 ++ protocol/spam/spamguard.go | 84 ++++++ protocol/spam/spamguard_test.go | 33 ++ server/fetch/server_fetch_channel.go | 101 +++++++ server/fetch/server_fetch_channel_test.go | 109 +++++++ server/listen/server_listen_channel.go | 89 ++++++ server/listen/server_listen_channel_test.go | 93 ++++++ server/ms.test | 1 + server/send/server_send_channel.go | 99 ++++++ server/send/server_send_channel_test.go | 172 +++++++++++ server/server.go | 68 +++++ server/server_instance.go | 42 +++ server/server_instance_test.go | 30 ++ storage/message_store.go | 67 +++++ storage/message_store_test.go | 29 ++ todo.md | 18 ++ ui/action.go | 15 + ui/chat_screen.go | 33 ++ ui/contact_screen.go | 37 +++ ui/contact_screen_test.go | 30 ++ ui/layout.go | 127 ++++++++ ui/screen.go | 9 + ui/state.go | 149 ++++++++++ ux/main.go | 57 ++++ 50 files changed, 3864 insertions(+) create mode 100644 README.md create mode 100644 client/client.go create mode 100644 client/cwtch_peer.go create mode 100644 client/cwtch_peer_test.go create mode 100644 client/fetch/peer_fetch_channel.go create mode 100644 client/fetch/peer_fetch_channel_test.go create mode 100644 client/listen/peer_listen_channel.go create mode 100644 client/listen/peer_listen_channel_test.go create mode 100644 client/send/peer_send_channel.go create mode 100644 client/send/peer_send_channel_test.go create mode 100644 client/test_profile create mode 100644 editor.go create mode 100644 main.go create mode 100644 model/group.go create mode 100644 model/group_test.go create mode 100644 model/message.go create mode 100644 model/profile.go create mode 100644 model/profile_test create mode 100644 model/profile_test.go create mode 100644 papers/metadata-resistant-group-chat.md create mode 100644 papers/metadata-resistant-protocols.md create mode 100644 protocol.md create mode 100644 protocol/channel.go create mode 100644 protocol/cwtch-profile.pb.go create mode 100644 protocol/cwtch-profile.proto create mode 100644 protocol/group_message.pb.go create mode 100644 protocol/group_message.proto create mode 100644 protocol/spam/spamguard.go create mode 100644 protocol/spam/spamguard_test.go create mode 100644 server/fetch/server_fetch_channel.go create mode 100644 server/fetch/server_fetch_channel_test.go create mode 100644 server/listen/server_listen_channel.go create mode 100644 server/listen/server_listen_channel_test.go create mode 100644 server/ms.test create mode 100644 server/send/server_send_channel.go create mode 100644 server/send/server_send_channel_test.go create mode 100644 server/server.go create mode 100644 server/server_instance.go create mode 100644 server/server_instance_test.go create mode 100644 storage/message_store.go create mode 100644 storage/message_store_test.go create mode 100644 todo.md create mode 100644 ui/action.go create mode 100644 ui/chat_screen.go create mode 100644 ui/contact_screen.go create mode 100644 ui/contact_screen_test.go create mode 100644 ui/layout.go create mode 100644 ui/screen.go create mode 100644 ui/state.go create mode 100644 ux/main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf2ea32 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Cwtch + +## Minimal Viable Product Feature Set + +* Connecting Dialog +* Profiles +* Server Setup +* Verification +* Peer-2-Peer Chat diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..9d94d95 --- /dev/null +++ b/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) { + +} diff --git a/client/cwtch_peer.go b/client/cwtch_peer.go new file mode 100644 index 0000000..0ec1f00 --- /dev/null +++ b/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() { +} diff --git a/client/cwtch_peer_test.go b/client/cwtch_peer_test.go new file mode 100644 index 0000000..a1ea28f --- /dev/null +++ b/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) +} diff --git a/client/fetch/peer_fetch_channel.go b/client/fetch/peer_fetch_channel.go new file mode 100644 index 0000000..425d5c0 --- /dev/null +++ b/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() +} diff --git a/client/fetch/peer_fetch_channel_test.go b/client/fetch/peer_fetch_channel_test.go new file mode 100644 index 0000000..f1ab5fa --- /dev/null +++ b/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) + +} diff --git a/client/listen/peer_listen_channel.go b/client/listen/peer_listen_channel.go new file mode 100644 index 0000000..1c03fa4 --- /dev/null +++ b/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) + } + } +} diff --git a/client/listen/peer_listen_channel_test.go b/client/listen/peer_listen_channel_test.go new file mode 100644 index 0000000..8cd1497 --- /dev/null +++ b/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) + +} diff --git a/client/send/peer_send_channel.go b/client/send/peer_send_channel.go new file mode 100644 index 0000000..ad1d73c --- /dev/null +++ b/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() +} diff --git a/client/send/peer_send_channel_test.go b/client/send/peer_send_channel_test.go new file mode 100644 index 0000000..9018594 --- /dev/null +++ b/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) + +} diff --git a/client/test_profile b/client/test_profile new file mode 100644 index 0000000..203b81f --- /dev/null +++ b/client/test_profile @@ -0,0 +1 @@ +{"Profile":{"Name":"sarah","Ed25519PublicKey":"QUqkM0hmJ6UnfLFirkIqC0ZzxNVbMc55ePtpihk4QP4=","Contacts":{},"Ed25519PrivateKey":"B3gR0WdoGfGLONyV6Mq685ltyAE3coKo39Z6FtjOg8pBSqQzSGYnpSd8sWKuQioLRnPE1Vsxznl4+2mKGThA/g==","OnionPrivateKey":null,"Groups":{}},"PendingContacts":null,"PendingInvites":{}} \ No newline at end of file diff --git a/editor.go b/editor.go new file mode 100644 index 0000000..647c96d --- /dev/null +++ b/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) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b8efabb --- /dev/null +++ b/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() + } + } +} diff --git a/model/group.go b/model/group.go new file mode 100644 index 0000000..25eee3b --- /dev/null +++ b/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, "" +} diff --git a/model/group_test.go b/model/group_test.go new file mode 100644 index 0000000..580163d --- /dev/null +++ b/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) +} diff --git a/model/message.go b/model/message.go new file mode 100644 index 0000000..d5f86d0 --- /dev/null +++ b/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 +} diff --git a/model/profile.go b/model/profile.go new file mode 100644 index 0000000..e3d9a0b --- /dev/null +++ b/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 +} diff --git a/model/profile_test b/model/profile_test new file mode 100644 index 0000000..facd14b --- /dev/null +++ b/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":{}} \ No newline at end of file diff --git a/model/profile_test.go b/model/profile_test.go new file mode 100644 index 0000000..be358e7 --- /dev/null +++ b/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) + } +} diff --git a/papers/metadata-resistant-group-chat.md b/papers/metadata-resistant-group-chat.md new file mode 100644 index 0000000..6c6d656 --- /dev/null +++ b/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. + +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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/papers/metadata-resistant-protocols.md b/papers/metadata-resistant-protocols.md new file mode 100644 index 0000000..f5e52f9 --- /dev/null +++ b/papers/metadata-resistant-protocols.md @@ -0,0 +1 @@ +# Metdata-Resistant Protocols: An Overview diff --git a/protocol.md b/protocol.md new file mode 100644 index 0000000..09b3ac2 --- /dev/null +++ b/protocol.md @@ -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. + diff --git a/protocol/channel.go b/protocol/channel.go new file mode 100644 index 0000000..751f3ba --- /dev/null +++ b/protocol/channel.go @@ -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()) + } + } +} diff --git a/protocol/cwtch-profile.pb.go b/protocol/cwtch-profile.pb.go new file mode 100644 index 0000000..38cc6cc --- /dev/null +++ b/protocol/cwtch-profile.pb.go @@ -0,0 +1,124 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: cwtch-profile.proto + +package protocol + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type CwtchPeerPacket struct { + CwtchIdentify *CwtchIdentity `protobuf:"bytes,1,opt,name=cwtch_identify,json=cwtchIdentify" json:"cwtch_identify,omitempty"` + GroupChatInvite *GroupChatInvite `protobuf:"bytes,2,opt,name=group_chat_invite,json=groupChatInvite" json:"group_chat_invite,omitempty"` +} + +func (m *CwtchPeerPacket) Reset() { *m = CwtchPeerPacket{} } +func (m *CwtchPeerPacket) String() string { return proto.CompactTextString(m) } +func (*CwtchPeerPacket) ProtoMessage() {} +func (*CwtchPeerPacket) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } + +func (m *CwtchPeerPacket) GetCwtchIdentify() *CwtchIdentity { + if m != nil { + return m.CwtchIdentify + } + return nil +} + +func (m *CwtchPeerPacket) GetGroupChatInvite() *GroupChatInvite { + if m != nil { + return m.GroupChatInvite + } + return nil +} + +type CwtchIdentity struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Ed25519PublicKey []byte `protobuf:"bytes,2,opt,name=ed25519_public_key,json=ed25519PublicKey,proto3" json:"ed25519_public_key,omitempty"` +} + +func (m *CwtchIdentity) Reset() { *m = CwtchIdentity{} } +func (m *CwtchIdentity) String() string { return proto.CompactTextString(m) } +func (*CwtchIdentity) ProtoMessage() {} +func (*CwtchIdentity) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{1} } + +func (m *CwtchIdentity) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *CwtchIdentity) GetEd25519PublicKey() []byte { + if m != nil { + return m.Ed25519PublicKey + } + return nil +} + +// [name] has invited you to join a group chat: [message] +type GroupChatInvite struct { + GroupName string `protobuf:"bytes,1,opt,name=group_name,json=groupName" json:"group_name,omitempty"` + GroupSharedKey []byte `protobuf:"bytes,2,opt,name=group_shared_key,json=groupSharedKey,proto3" json:"group_shared_key,omitempty"` + ServerHost string `protobuf:"bytes,3,opt,name=server_host,json=serverHost" json:"server_host,omitempty"` +} + +func (m *GroupChatInvite) Reset() { *m = GroupChatInvite{} } +func (m *GroupChatInvite) String() string { return proto.CompactTextString(m) } +func (*GroupChatInvite) ProtoMessage() {} +func (*GroupChatInvite) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{2} } + +func (m *GroupChatInvite) GetGroupName() string { + if m != nil { + return m.GroupName + } + return "" +} + +func (m *GroupChatInvite) GetGroupSharedKey() []byte { + if m != nil { + return m.GroupSharedKey + } + return nil +} + +func (m *GroupChatInvite) GetServerHost() string { + if m != nil { + return m.ServerHost + } + return "" +} + +func init() { + proto.RegisterType((*CwtchPeerPacket)(nil), "protocol.CwtchPeerPacket") + proto.RegisterType((*CwtchIdentity)(nil), "protocol.CwtchIdentity") + proto.RegisterType((*GroupChatInvite)(nil), "protocol.GroupChatInvite") +} + +func init() { proto.RegisterFile("cwtch-profile.proto", fileDescriptor1) } + +var fileDescriptor1 = []byte{ + // 276 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x8f, 0xcd, 0x4e, 0xc3, 0x30, + 0x10, 0x84, 0x15, 0x40, 0x88, 0x6e, 0x69, 0x53, 0xcc, 0x81, 0x70, 0x40, 0xa0, 0x9c, 0x7a, 0x80, + 0x48, 0x14, 0xf5, 0xc0, 0x85, 0x4b, 0x85, 0xa0, 0xaa, 0x84, 0x42, 0x78, 0x00, 0x2b, 0x75, 0x36, + 0xb5, 0xd5, 0x10, 0x47, 0x8e, 0x5b, 0x64, 0xf1, 0x22, 0x3c, 0x2e, 0xca, 0x06, 0xd4, 0x9f, 0x93, + 0xed, 0x99, 0x9d, 0x6f, 0xd6, 0x70, 0x2e, 0xbe, 0xac, 0x90, 0x77, 0x95, 0xd1, 0xb9, 0x2a, 0x30, + 0xaa, 0x8c, 0xb6, 0x9a, 0x9d, 0xd0, 0x21, 0x74, 0x11, 0xfe, 0x78, 0xe0, 0x4f, 0x9a, 0x89, 0x18, + 0xd1, 0xc4, 0xa9, 0x58, 0xa2, 0x65, 0x4f, 0xd0, 0xa7, 0x10, 0x57, 0x19, 0x96, 0x56, 0xe5, 0x2e, + 0xf0, 0x6e, 0xbc, 0x61, 0x77, 0x74, 0x11, 0xfd, 0xc7, 0x22, 0x8a, 0x4c, 0xc9, 0xb6, 0x2e, 0xe9, + 0x89, 0xcd, 0x33, 0x77, 0xec, 0x19, 0xce, 0x16, 0x46, 0xaf, 0x2a, 0x2e, 0x64, 0x6a, 0xb9, 0x2a, + 0xd7, 0xca, 0x62, 0x70, 0x40, 0x88, 0xcb, 0x0d, 0xe2, 0xa5, 0x19, 0x99, 0xc8, 0xd4, 0x4e, 0x69, + 0x20, 0xf1, 0x17, 0xbb, 0x42, 0xf8, 0x0e, 0xbd, 0x9d, 0x1a, 0xc6, 0xe0, 0xa8, 0x4c, 0x3f, 0x91, + 0xb6, 0xe9, 0x24, 0x74, 0x67, 0xb7, 0xc0, 0x30, 0x1b, 0x8d, 0xc7, 0xf7, 0x8f, 0xbc, 0x5a, 0xcd, + 0x0b, 0x25, 0xf8, 0x12, 0x1d, 0x95, 0x9d, 0x26, 0x83, 0x3f, 0x27, 0x26, 0x63, 0x86, 0x2e, 0xfc, + 0x06, 0x7f, 0xaf, 0x96, 0x5d, 0x01, 0xb4, 0xcb, 0x6e, 0xa1, 0x3b, 0xa4, 0xbc, 0x35, 0xfc, 0x21, + 0x0c, 0x5a, 0xbb, 0x96, 0xa9, 0xc1, 0x6c, 0x8b, 0xde, 0x27, 0xfd, 0x83, 0xe4, 0x19, 0x3a, 0x76, + 0x0d, 0xdd, 0x1a, 0xcd, 0x1a, 0x0d, 0x97, 0xba, 0xb6, 0xc1, 0x21, 0x91, 0xa0, 0x95, 0x5e, 0x75, + 0x6d, 0xe7, 0xc7, 0xf4, 0xf5, 0x87, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7b, 0x47, 0x47, 0xed, + 0x92, 0x01, 0x00, 0x00, +} diff --git a/protocol/cwtch-profile.proto b/protocol/cwtch-profile.proto new file mode 100644 index 0000000..f27601b --- /dev/null +++ b/protocol/cwtch-profile.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; +package protocol; + +message CwtchPeerPacket { + CwtchIdentity cwtch_identify = 1; + GroupChatInvite group_chat_invite = 2; +} + +message CwtchIdentity { + string name = 1; + bytes ed25519_public_key = 2; +} + +// [name] has invited you to join a group chat: [message] +message GroupChatInvite { + string group_name = 1; + bytes group_shared_key = 2; + string server_host = 3; +} diff --git a/protocol/group_message.pb.go b/protocol/group_message.pb.go new file mode 100644 index 0000000..1da1699 --- /dev/null +++ b/protocol/group_message.pb.go @@ -0,0 +1,126 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: group_message.proto + +package protocol + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import Protocol_Data_Control "github.com/s-rah/go-ricochet/wire/control" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type CwtchServerPacket struct { + GroupMessage *GroupMessage `protobuf:"bytes,1,opt,name=group_message,json=groupMessage" json:"group_message,omitempty"` + FetchMessage *FetchMessage `protobuf:"bytes,2,opt,name=fetch_message,json=fetchMessage" json:"fetch_message,omitempty"` + GroupMessages []*GroupMessage `protobuf:"bytes,3,rep,name=group_messages,json=groupMessages" json:"group_messages,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CwtchServerPacket) Reset() { *m = CwtchServerPacket{} } +func (m *CwtchServerPacket) String() string { return proto.CompactTextString(m) } +func (*CwtchServerPacket) ProtoMessage() {} + +func (m *CwtchServerPacket) GetGroupMessage() *GroupMessage { + if m != nil { + return m.GroupMessage + } + return nil +} + +func (m *CwtchServerPacket) GetFetchMessage() *FetchMessage { + if m != nil { + return m.FetchMessage + } + return nil +} + +func (m *CwtchServerPacket) GetGroupMessages() []*GroupMessage { + if m != nil { + return m.GroupMessages + } + return nil +} + +type FetchMessage struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *FetchMessage) Reset() { *m = FetchMessage{} } +func (m *FetchMessage) String() string { return proto.CompactTextString(m) } +func (*FetchMessage) ProtoMessage() {} + +type GroupMessage struct { + Ciphertext []byte `protobuf:"bytes,1,req,name=ciphertext" json:"ciphertext,omitempty"` + Spamguard []byte `protobuf:"bytes,2,req,name=spamguard" json:"spamguard,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GroupMessage) Reset() { *m = GroupMessage{} } +func (m *GroupMessage) String() string { return proto.CompactTextString(m) } +func (*GroupMessage) ProtoMessage() {} + + +func (m *GroupMessage) GetCiphertext() []byte { + if m != nil { + return m.Ciphertext + } + return nil +} + +func (m *GroupMessage) GetSpamguard() []byte { + if m != nil { + return m.Spamguard + } + return nil +} + +// DecryptedGroupMessage is *never* sent in the clear on the wire +// and is only ever sent when encrypted in the ciphertext parameter of +// GroupMessage +type DecryptedGroupMessage struct { + Onion *string `protobuf:"bytes,1,req,name=onion" json:"onion,omitempty"` + Text *string `protobuf:"bytes,2,req,name=text" json:"text,omitempty"` + Signature []byte `protobuf:"bytes,3,req,name=signature" json:"signature,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *DecryptedGroupMessage) Reset() { *m = DecryptedGroupMessage{} } +func (m *DecryptedGroupMessage) String() string { return proto.CompactTextString(m) } +func (*DecryptedGroupMessage) ProtoMessage() {} + +func (m *DecryptedGroupMessage) GetOnion() string { + if m != nil && m.Onion != nil { + return *m.Onion + } + return "" +} + +func (m *DecryptedGroupMessage) GetText() string { + if m != nil && m.Text != nil { + return *m.Text + } + return "" +} + +func (m *DecryptedGroupMessage) GetSignature() []byte { + if m != nil { + return m.Signature + } + return nil +} + +var E_ServerNonce = &proto.ExtensionDesc{ + ExtendedType: (*Protocol_Data_Control.ChannelResult)(nil), + ExtensionType: ([]byte)(nil), + Field: 8200, + Name: "im.cwtch.server_nonce", + Tag: "bytes,8200,opt,name=server_nonce", +} + +func init() { + proto.RegisterExtension(E_ServerNonce) +} diff --git a/protocol/group_message.proto b/protocol/group_message.proto new file mode 100644 index 0000000..275f58b --- /dev/null +++ b/protocol/group_message.proto @@ -0,0 +1,32 @@ +syntax = "proto2"; +package protocol; + +import "ControlChannel.proto"; + +message CwtchServerPacket { + optional GroupMessage group_message = 1; + optional FetchMessage fetch_message = 2; + repeated GroupMessage group_messages = 3; +} + +extend protocol.ChannelResult { + optional bytes server_nonce = 8200; // 32 random bytes +} + +message FetchMessage { +} + +message GroupMessage { + required bytes ciphertext = 1; + required bytes spamguard = 2; +} + +// DecryptedGroupMessage is *never* sent in the clear on the wire +// and is only ever sent when encrypted in the ciphertext parameter of +// GroupMessage +message DecryptedGroupMessage { + required string onion = 1; + required int32 timestamp = 2; + required string text = 3; + required bytes signature = 4; +} diff --git a/protocol/spam/spamguard.go b/protocol/spam/spamguard.go new file mode 100644 index 0000000..d7399c1 --- /dev/null +++ b/protocol/spam/spamguard.go @@ -0,0 +1,84 @@ +package spam + +import ( + "crypto/rand" + "crypto/sha256" + "git.mascherari.press/cwtch/protocol" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/utils" + "github.com/s-rah/go-ricochet/wire/control" + "io" +) + +type SpamGuard struct { + Difficulty int + nonce [24]byte +} + +func (sg *SpamGuard) GenerateChallenge(channelID int32) []byte { + + cr := &Protocol_Data_Control.ChannelResult{ + ChannelIdentifier: proto.Int32(channelID), + Opened: proto.Bool(true), + } + + var nonce [24]byte + if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + utils.CheckError(err) + } + sg.nonce = nonce + err := proto.SetExtension(cr, protocol.E_ServerNonce, sg.nonce[:]) + utils.CheckError(err) + + pc := &Protocol_Data_Control.Packet{ + ChannelResult: cr, + } + ret, err := proto.Marshal(pc) + utils.CheckError(err) + return ret +} + +func (sg *SpamGuard) SolveChallenge(challenge []byte, message []byte) []byte { + solved := false + var spamguard [24]byte + sum := sha256.Sum256([]byte{}) + solve := make([]byte, len(challenge)+len(message)+len(spamguard)) + for !solved { + + if _, err := io.ReadFull(rand.Reader, spamguard[:]); err != nil { + utils.CheckError(err) + } + + copy(solve[0:], challenge[:]) + copy(solve[len(challenge):], message[:]) + copy(solve[len(challenge)+len(message):], spamguard[:]) + + sum = sha256.Sum256(solve) + + solved = true + for i := 0; i < sg.Difficulty; i++ { + if sum[i] != 0x00 { + solved = false + } + } + } + + //log.Printf("Solved answer: %v %x %x\n", len(solve), solve, sum) + return spamguard[:] +} + +func (sg *SpamGuard) ValidateChallenge(message []byte, spamguard []byte) bool { + //log.Printf("%v %v\n", sg.nonce[:], spamguard[:]) + solve := make([]byte, len(sg.nonce)+len(message)+len(spamguard)) + copy(solve[0:], sg.nonce[:]) + copy(solve[len(sg.nonce):], message[:]) + copy(solve[len(sg.nonce)+len(message):], spamguard[:]) + sum := sha256.Sum256(solve) + //log.Printf("Got answer: %v %x %x\n", len(solve), solve, sum) + for i := 0; i < sg.Difficulty; i++ { + if sum[i] != 0x00 { + return false + } + } + return true +} diff --git a/protocol/spam/spamguard_test.go b/protocol/spam/spamguard_test.go new file mode 100644 index 0000000..94c70cf --- /dev/null +++ b/protocol/spam/spamguard_test.go @@ -0,0 +1,33 @@ +package spam + +import ( + "git.mascherari.press/cwtch/protocol" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/wire/control" + "testing" +) + +func TestSpamGuard(t *testing.T) { + var spamGuard SpamGuard + spamGuard.Difficulty = 2 + challenge := spamGuard.GenerateChallenge(3) + + control := new(Protocol_Data_Control.Packet) + proto.Unmarshal(challenge[:], control) + + if control.GetChannelResult() != nil { + ce, _ := proto.GetExtension(control.GetChannelResult(), protocol.E_ServerNonce) + challenge := ce.([]byte)[:] + + sgsolve := spamGuard.SolveChallenge(challenge, []byte("Hello")) + t.Logf("Solved: %v %v", challenge, sgsolve) + result := spamGuard.ValidateChallenge([]byte("Hello"), sgsolve) + if result != true { + t.Errorf("Validating SpamGuard Failed") + } + return + } + + t.Errorf("Failed SpamGaurd") + +} diff --git a/server/fetch/server_fetch_channel.go b/server/fetch/server_fetch_channel.go new file mode 100644 index 0000000..f83376a --- /dev/null +++ b/server/fetch/server_fetch_channel.go @@ -0,0 +1,101 @@ +package fetch + +import ( + "errors" + "git.mascherari.press/cwtch/protocol" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/channels" + "github.com/s-rah/go-ricochet/utils" + "github.com/s-rah/go-ricochet/wire/control" +) + +// CwtchChannel implements the ChannelHandler interface for a channel of +// type "im.ricochet.Cwtch". The channel may be inbound or outbound. +// +// CwtchChannel implements protocol-level sanity and state validation, but +// does not handle or acknowledge Cwtch messages. The application must provide +// a CwtchChannelHandler implementation to handle Cwtch events. +type CwtchServerFetchChannel struct { + Handler CwtchServerFetchHandler + channel *channels.Channel +} + +type CwtchServerFetchHandler interface { + HandleFetchRequest() []*protocol.GroupMessage +} + +// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch". +func (cc *CwtchServerFetchChannel) Type() string { + return "im.cwtch.server.fetch" +} + +// Closed is called when the channel is closed for any reason. +func (cc *CwtchServerFetchChannel) Closed(err error) { + +} + +// OnlyClientCanOpen - for Cwtch channels any side can open +func (cc *CwtchServerFetchChannel) OnlyClientCanOpen() bool { + return true +} + +// Singleton - for Cwtch channels there can only be one instance per direction +func (cc *CwtchServerFetchChannel) Singleton() bool { + return true +} + +// Bidirectional - for Cwtch channels are not bidrectional +func (cc *CwtchServerFetchChannel) Bidirectional() bool { + return false +} + +// RequiresAuthentication - Cwtch channels require hidden service auth +func (cc *CwtchServerFetchChannel) RequiresAuthentication() string { + return "none" +} + +// OpenInbound is the first method called for an inbound channel request. +// If an error is returned, the channel is rejected. If a RawMessage is +// returned, it will be sent as the ChannelResult message. +func (cc *CwtchServerFetchChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) { + cc.channel = channel + messageBuilder := new(utils.MessageBuilder) + return messageBuilder.AckOpenChannel(channel.ID), nil +} + +// OpenOutbound is the first method called for an outbound channel request. +// If an error is returned, the channel is not opened. If a RawMessage is +// returned, it will be sent as the OpenChannel message. +func (cc *CwtchServerFetchChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) { + return nil, errors.New("server does not open Fetch channels") +} + +// OpenOutboundResult is called when a response is received for an +// outbound OpenChannel request. If `err` is non-nil, the channel was +// rejected and Closed will be called immediately afterwards. `raw` +// contains the raw protocol message including any extension data. +func (cc *CwtchServerFetchChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) { + // NOTE: Should never be called +} + +// SendGroupMessage +func (cc *CwtchServerFetchChannel) SendGroupMessages(gm []*protocol.GroupMessage) { + csp := &protocol.CwtchServerPacket{ + GroupMessages: gm, + } + packet, _ := proto.Marshal(csp) + cc.channel.SendMessage(packet) +} + +// Packet is called for each raw packet received on this channel. +func (cc *CwtchServerFetchChannel) Packet(data []byte) { + csp := &protocol.CwtchServerPacket{} + err := proto.Unmarshal(data, csp) + if err == nil { + if csp.GetFetchMessage() != nil { + cc.SendGroupMessages(cc.Handler.HandleFetchRequest()) + } + } + // If we receive a packet on this channel, close the connection + cc.channel.CloseChannel() +} diff --git a/server/fetch/server_fetch_channel_test.go b/server/fetch/server_fetch_channel_test.go new file mode 100644 index 0000000..2eb4f36 --- /dev/null +++ b/server/fetch/server_fetch_channel_test.go @@ -0,0 +1,109 @@ +package fetch + +import ( + "git.mascherari.press/cwtch/protocol" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/channels" + "github.com/s-rah/go-ricochet/wire/control" + "testing" +) + +func TestServerFetchChannelAttributes(t *testing.T) { + cslc := new(CwtchServerFetchChannel) + if cslc.Type() != "im.cwtch.server.fetch" { + t.Errorf("cwtch channel type is incorrect %v", cslc.Type()) + } + + if !cslc.OnlyClientCanOpen() { + t.Errorf("only clients should be able to open im.cwtch.server.fetch channel") + } + + if cslc.Bidirectional() { + t.Errorf("im.cwtch.server.fetch should not be bidirectional") + } + + if !cslc.Singleton() { + t.Errorf("im.cwtch.server.fetch should be a Singleton") + } + + if cslc.RequiresAuthentication() != "none" { + t.Errorf("cwtch channel required auth is incorrect %v", cslc.RequiresAuthentication()) + } + +} +func TestServerFetchChannelOpenOutbound(t *testing.T) { + cslc := new(CwtchServerFetchChannel) + channel := new(channels.Channel) + _, err := cslc.OpenOutbound(channel) + if err == nil { + t.Errorf("server implementation of im.cwtch.server.fetch should never open an outbound channel") + } +} + +type TestHandler struct { +} + +func (th *TestHandler) HandleFetchRequest() []*protocol.GroupMessage { + gm := &protocol.GroupMessage{ + Ciphertext: []byte("Hello"), + Spamguard: []byte{}, + } + return []*protocol.GroupMessage{gm} +} + +func TestServerFetchChannel(t *testing.T) { + cslc := new(CwtchServerFetchChannel) + th := new(TestHandler) + cslc.Handler = th + channel := new(channels.Channel) + channel.ID = 1 + closed := false + channel.CloseChannel = func() { + closed = true + } + gotgm := false + channel.SendMessage = func([]byte) { + gotgm = true + } + + oc := &Protocol_Data_Control.OpenChannel{ + ChannelIdentifier: proto.Int32(1), + ChannelType: proto.String(cslc.Type()), + } + + resp, err := cslc.OpenInbound(channel, oc) + if err != nil { + t.Errorf("OpenInbound for im.cwtch.server.Fetch should have succeeded, instead: %v", err) + } + + control := new(Protocol_Data_Control.Packet) + proto.Unmarshal(resp[:], control) + + if control.GetChannelResult() != nil { + + fm := &protocol.FetchMessage{} + + csp := &protocol.CwtchServerPacket{ + FetchMessage: fm, + } + + packet, _ := proto.Marshal(csp) + cslc.Packet(packet) + + if !gotgm { + t.Errorf("Did not receive packet on wire as expected in Fetch channel") + } + + if !closed { + t.Errorf("Fetch channel should be cosed") + } + + if !closed { + t.Errorf("Fetch channel should be closed after incorrect packet received") + } + + } else { + t.Errorf("Expected ChannelResult from im.cwtch.server.Fetch, instead: %v", control) + } + +} diff --git a/server/listen/server_listen_channel.go b/server/listen/server_listen_channel.go new file mode 100644 index 0000000..c6940db --- /dev/null +++ b/server/listen/server_listen_channel.go @@ -0,0 +1,89 @@ +package listen + +import ( + "errors" + "git.mascherari.press/cwtch/protocol" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/channels" + "github.com/s-rah/go-ricochet/utils" + "github.com/s-rah/go-ricochet/wire/control" +) + +// CwtchChannel implements the ChannelHandler interface for a channel of +// type "im.ricochet.Cwtch". The channel may be inbound or outbound. +// +// CwtchChannel implements protocol-level sanity and state validation, but +// does not handle or acknowledge Cwtch messages. The application must provide +// a CwtchChannelHandler implementation to handle Cwtch events. +type CwtchServerListenChannel struct { + channel *channels.Channel +} + +// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch". +func (cc *CwtchServerListenChannel) Type() string { + return "im.cwtch.server.listen" +} + +// Closed is called when the channel is closed for any reason. +func (cc *CwtchServerListenChannel) Closed(err error) { + +} + +// OnlyClientCanOpen - for Cwtch channels any side can open +func (cc *CwtchServerListenChannel) OnlyClientCanOpen() bool { + return true +} + +// Singleton - for Cwtch channels there can only be one instance per direction +func (cc *CwtchServerListenChannel) Singleton() bool { + return true +} + +// Bidirectional - for Cwtch channels are not bidrectional +func (cc *CwtchServerListenChannel) Bidirectional() bool { + return false +} + +// RequiresAuthentication - Cwtch channels require hidden service auth +func (cc *CwtchServerListenChannel) RequiresAuthentication() string { + return "none" +} + +// OpenInbound is the first method called for an inbound channel request. +// If an error is returned, the channel is rejected. If a RawMessage is +// returned, it will be sent as the ChannelResult message. +func (cc *CwtchServerListenChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) { + cc.channel = channel + messageBuilder := new(utils.MessageBuilder) + return messageBuilder.AckOpenChannel(channel.ID), nil +} + +// OpenOutbound is the first method called for an outbound channel request. +// If an error is returned, the channel is not opened. If a RawMessage is +// returned, it will be sent as the OpenChannel message. +func (cc *CwtchServerListenChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) { + return nil, errors.New("server does not open listen channels") +} + +// OpenOutboundResult is called when a response is received for an +// outbound OpenChannel request. If `err` is non-nil, the channel was +// rejected and Closed will be called immediately afterwards. `raw` +// contains the raw protocol message including any extension data. +func (cc *CwtchServerListenChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) { + // NOTE: Should never be called +} + +// SendGroupMessage +func (cc *CwtchServerListenChannel) SendGroupMessage(gm *protocol.GroupMessage) { + csp := &protocol.CwtchServerPacket{ + GroupMessage: gm, + } + packet, _ := proto.Marshal(csp) + cc.channel.SendMessage(packet) +} + +// Packet is called for each raw packet received on this channel. +func (cc *CwtchServerListenChannel) Packet(data []byte) { + // If we receive a packet on this channel, close the connection + cc.channel.CloseChannel() +} diff --git a/server/listen/server_listen_channel_test.go b/server/listen/server_listen_channel_test.go new file mode 100644 index 0000000..2671761 --- /dev/null +++ b/server/listen/server_listen_channel_test.go @@ -0,0 +1,93 @@ +package listen + +import ( + "git.mascherari.press/cwtch/protocol" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/channels" + "github.com/s-rah/go-ricochet/wire/control" + "testing" +) + +func TestServerListenChannelAttributes(t *testing.T) { + cslc := new(CwtchServerListenChannel) + if cslc.Type() != "im.cwtch.server.listen" { + t.Errorf("cwtch channel type is incorrect %v", cslc.Type()) + } + + if !cslc.OnlyClientCanOpen() { + t.Errorf("only clients should be able to open im.cwtch.server.listen channel") + } + + if cslc.Bidirectional() { + t.Errorf("im.cwtch.server.listen should not be bidirectional") + } + + if !cslc.Singleton() { + t.Errorf("im.cwtch.server.listen should be a Singleton") + } + + if cslc.RequiresAuthentication() != "none" { + t.Errorf("cwtch channel required auth is incorrect %v", cslc.RequiresAuthentication()) + } + +} +func TestServerListenChannelOpenOutbound(t *testing.T) { + cslc := new(CwtchServerListenChannel) + channel := new(channels.Channel) + _, err := cslc.OpenOutbound(channel) + if err == nil { + t.Errorf("server implementation of im.cwtch.server.listen should never open an outbound channel") + } +} + +func TestServerListenChannel(t *testing.T) { + cslc := new(CwtchServerListenChannel) + channel := new(channels.Channel) + channel.ID = 1 + closed := false + channel.CloseChannel = func() { + closed = true + } + gotgm := false + channel.SendMessage = func([]byte) { + gotgm = true + } + + oc := &Protocol_Data_Control.OpenChannel{ + ChannelIdentifier: proto.Int32(1), + ChannelType: proto.String(cslc.Type()), + } + + resp, err := cslc.OpenInbound(channel, oc) + if err != nil { + t.Errorf("OpenInbound for im.cwtch.server.listen should have succeeded, instead: %v", err) + } + + control := new(Protocol_Data_Control.Packet) + proto.Unmarshal(resp[:], control) + + if control.GetChannelResult() != nil { + gm := &protocol.GroupMessage{ + Ciphertext: []byte("Hello"), + Spamguard: []byte{}, + } + cslc.SendGroupMessage(gm) + if !gotgm { + t.Errorf("Did not receive packet on wire as expected in listen channel") + } + + if closed { + t.Errorf("listen channel should not be cosed") + } + + cslc.Packet(nil) + + if !closed { + t.Errorf("listen channel should be closed after incorrect packet received") + } + + } else { + t.Errorf("Expected ChannelResult from im.cwtch.server.listen, instead: %v", control) + } + +} diff --git a/server/ms.test b/server/ms.test new file mode 100644 index 0000000..63f8b40 --- /dev/null +++ b/server/ms.test @@ -0,0 +1 @@ +{"ciphertext":"SGVsbG8gdGhpcyBpcyBhIGZhaXJseSBhdmVyYWdlIGxlbmd0aCBtZXNzYWdlIHRoYXQgd2UgYXJlIHdyaXRpbmcgaGVyZS4="} diff --git a/server/send/server_send_channel.go b/server/send/server_send_channel.go new file mode 100644 index 0000000..3f31c05 --- /dev/null +++ b/server/send/server_send_channel.go @@ -0,0 +1,99 @@ +package send + +import ( + "errors" + "git.mascherari.press/cwtch/protocol" + "git.mascherari.press/cwtch/protocol/spam" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/channels" + "github.com/s-rah/go-ricochet/wire/control" +) + +// CwtchChannel implements the ChannelHandler interface for a channel of +// type "im.ricochet.Cwtch". The channel may be inbound or outbound. +// +// CwtchChannel implements protocol-level sanity and state validation, but +// does not handle or acknowledge Cwtch messages. The application must provide +// a CwtchChannelHandler implementation to handle Cwtch events. +type CwtchServerSendChannel struct { + // Methods of Handler are called for Cwtch events on this channel + Handler CwtchServerSendChannelHandler + channel *channels.Channel + spamguard spam.SpamGuard +} + +// CwtchChannelHandler is implemented by an application type to receive +// events from a CwtchChannel. +type CwtchServerSendChannelHandler interface { + HandleGroupMessage(*protocol.GroupMessage) +} + +// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch". +func (cc *CwtchServerSendChannel) Type() string { + return "im.cwtch.server.send" +} + +// Closed is called when the channel is closed for any reason. +func (cc *CwtchServerSendChannel) Closed(err error) { + +} + +// OnlyClientCanOpen - for Cwtch channels any side can open +func (cc *CwtchServerSendChannel) OnlyClientCanOpen() bool { + return true +} + +// Singleton - for Cwtch channels there can only be one instance per direction +func (cc *CwtchServerSendChannel) Singleton() bool { + return true +} + +// Bidirectional - for Cwtch channels are not bidrectional +func (cc *CwtchServerSendChannel) Bidirectional() bool { + return false +} + +// RequiresAuthentication - Cwtch channels require hidden service auth +func (cc *CwtchServerSendChannel) RequiresAuthentication() string { + return "none" +} + +// OpenInbound is the first method called for an inbound channel request. +// If an error is returned, the channel is rejected. If a RawMessage is +// returned, it will be sent as the ChannelResult message. +func (cc *CwtchServerSendChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) { + cc.channel = channel + cc.spamguard.Difficulty = 2 + return cc.spamguard.GenerateChallenge(channel.ID), nil +} + +// OpenOutbound is the first method called for an outbound channel request. +// If an error is returned, the channel is not opened. If a RawMessage is +// returned, it will be sent as the OpenChannel message. +func (cc *CwtchServerSendChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) { + return nil, errors.New("server does not open send channel") +} + +// OpenOutboundResult is called when a response is received for an +// outbound OpenChannel request. If `err` is non-nil, the channel was +// rejected and Closed will be called immediately afterwards. `raw` +// contains the raw protocol message including any extension data. +func (cc *CwtchServerSendChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) { + // NOTE: Should never be called +} + +// Packet is called for each raw packet received on this channel. +func (cc *CwtchServerSendChannel) Packet(data []byte) { + csp := &protocol.CwtchServerPacket{} + err := proto.Unmarshal(data, csp) + if err == nil { + if csp.GetGroupMessage() != nil { + gm := csp.GetGroupMessage() + ok := cc.spamguard.ValidateChallenge(gm.GetCiphertext(), gm.GetSpamguard()) + if ok { + cc.Handler.HandleGroupMessage(gm) + } + } + } + cc.channel.CloseChannel() +} diff --git a/server/send/server_send_channel_test.go b/server/send/server_send_channel_test.go new file mode 100644 index 0000000..2d7d9e2 --- /dev/null +++ b/server/send/server_send_channel_test.go @@ -0,0 +1,172 @@ +package send + +import ( + "git.mascherari.press/cwtch/protocol" + "git.mascherari.press/cwtch/protocol/spam" + "github.com/golang/protobuf/proto" + "github.com/s-rah/go-ricochet/channels" + "github.com/s-rah/go-ricochet/wire/control" + "testing" +) + +type TestHandler struct { + Received bool +} + +func (th *TestHandler) HandleGroupMessage(m *protocol.GroupMessage) { + th.Received = true +} + +func TestServerSendChannelAttributes(t *testing.T) { + cssc := new(CwtchServerSendChannel) + if cssc.Type() != "im.cwtch.server.send" { + t.Errorf("cwtch channel type is incorrect %v", cssc.Type()) + } + + if !cssc.OnlyClientCanOpen() { + t.Errorf("only clients should be able to open im.cwtch.server.send channel") + } + + if cssc.Bidirectional() { + t.Errorf("im.cwtch.server.send should not be bidirectional") + } + + if !cssc.Singleton() { + t.Errorf("im.cwtch.server.send should be a Singleton") + } + + if cssc.RequiresAuthentication() != "none" { + t.Errorf("cwtch channel required auth is incorrect %v", cssc.RequiresAuthentication()) + } + +} +func TestServerSendChannelOpenOutbound(t *testing.T) { + cssc := new(CwtchServerSendChannel) + channel := new(channels.Channel) + _, err := cssc.OpenOutbound(channel) + if err == nil { + t.Errorf("server implementation of im.cwtch.server.send should never open an outbound channel") + } +} + +func TestServerSendChannel(t *testing.T) { + cssc := new(CwtchServerSendChannel) + th := new(TestHandler) + cssc.Handler = th + channel := new(channels.Channel) + channel.ID = 1 + closed := false + channel.CloseChannel = func() { + closed = true + } + + oc := &Protocol_Data_Control.OpenChannel{ + ChannelIdentifier: proto.Int32(1), + ChannelType: proto.String(cssc.Type()), + } + + resp, err := cssc.OpenInbound(channel, oc) + if err != nil { + t.Errorf("OpenInbound for im.cwtch.server.send should have succeeded, instead: %v", err) + } + + control := new(Protocol_Data_Control.Packet) + proto.Unmarshal(resp[:], control) + + if control.GetChannelResult() != nil { + + var spamguard spam.SpamGuard + spamguard.Difficulty = 2 + + ce, _ := proto.GetExtension(control.GetChannelResult(), protocol.E_ServerNonce) + challenge := ce.([]byte)[:] + + sgsolve := spamguard.SolveChallenge(challenge, []byte("Hello")) + //t.Logf("Solved: %x", sgsolve) + + gm := &protocol.GroupMessage{ + Ciphertext: []byte("Hello"), + Spamguard: sgsolve, + } + + csp := &protocol.CwtchServerPacket{ + GroupMessage: gm, + } + packet, _ := proto.Marshal(csp) + + cssc.Packet(packet) + + if !th.Received { + t.Errorf("group message should have been received") + } + + if !closed { + t.Errorf("im.cwtch.server.send should have been closed after use") + } + } else { + t.Errorf("Expected ChannelResult from im.cwtch.server.send, instead: %v", control) + } + +} + +func TestServerSendChannelNoSpamGuard(t *testing.T) { + cssc := new(CwtchServerSendChannel) + th := new(TestHandler) + th.Received = false + cssc.Handler = th + channel := new(channels.Channel) + channel.ID = 1 + closed := false + channel.CloseChannel = func() { + closed = true + } + + oc := &Protocol_Data_Control.OpenChannel{ + ChannelIdentifier: proto.Int32(1), + ChannelType: proto.String(cssc.Type()), + } + + resp, err := cssc.OpenInbound(channel, oc) + if err != nil { + t.Errorf("OpenInbound for im.cwtch.server.send should have succeeded, instead: %v", err) + } + + control := new(Protocol_Data_Control.Packet) + proto.Unmarshal(resp[:], control) + + if control.GetChannelResult() != nil { + + var spamguard spam.SpamGuard + spamguard.Difficulty = 2 + + ce, _ := proto.GetExtension(control.GetChannelResult(), protocol.E_ServerNonce) + challenge := ce.([]byte)[:] + + sgsolve := spamguard.SolveChallenge(challenge, []byte("4234")) + //t.Logf("Solved: %x", sgsolve) + + gm := &protocol.GroupMessage{ + Ciphertext: []byte("hello"), + Spamguard: sgsolve, + } + + csp := &protocol.CwtchServerPacket{ + GroupMessage: gm, + } + packet, _ := proto.Marshal(csp) + + cssc.Packet(packet) + + if th.Received == true { + t.Errorf("group message should not have been received") + } + + if !closed { + t.Errorf("im.cwtch.server.send should have been closed after use") + } + + } else { + t.Errorf("Expected ChannelResult from im.cwtch.server.send, instead: %v", control) + } + +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..9838996 --- /dev/null +++ b/server/server.go @@ -0,0 +1,68 @@ +package server + +import ( + //"crypto/rsa" + "git.mascherari.press/cwtch/server/fetch" + "git.mascherari.press/cwtch/server/listen" + "git.mascherari.press/cwtch/server/send" + "git.mascherari.press/cwtch/storage" + "github.com/s-rah/go-ricochet/application" + "github.com/s-rah/go-ricochet/channels" + "github.com/s-rah/go-ricochet/utils" + "log" + //"time" +) + +type Server struct { +} + +func (s *Server) Run(privateKeyFile string) { + cwtchserver := new(application.RicochetApplication) + pk, err := utils.LoadPrivateKeyFromFile(privateKeyFile) + + if err != nil { + log.Fatalf("error reading private key file: %v", err) + } + + l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", pk, 9878) + + if err != nil { + log.Fatalf("error setting up onion service: %v", err) + } + + af := application.ApplicationInstanceFactory{} + af.Init() + ms := new(storage.MessageStore) + ms.Init("cwtch.messages") + af.AddHandler("im.cwtch.server.listen", func(rai *application.ApplicationInstance) func() channels.Handler { + si := new(ServerInstance) + si.Init(rai, cwtchserver, ms) + return func() channels.Handler { + cslc := new(listen.CwtchServerListenChannel) + return cslc + } + }) + + af.AddHandler("im.cwtch.server.fetch", func(rai *application.ApplicationInstance) func() channels.Handler { + si := new(ServerInstance) + si.Init(rai, cwtchserver, ms) + return func() channels.Handler { + cssc := new(fetch.CwtchServerFetchChannel) + cssc.Handler = si + return cssc + } + }) + + af.AddHandler("im.cwtch.server.send", func(rai *application.ApplicationInstance) func() channels.Handler { + si := new(ServerInstance) + si.Init(rai, cwtchserver, ms) + return func() channels.Handler { + cssc := new(send.CwtchServerSendChannel) + cssc.Handler = si + return cssc + } + }) + + cwtchserver.Init(pk, af, new(application.AcceptAllContactManager)) + cwtchserver.Run(l) +} diff --git a/server/server_instance.go b/server/server_instance.go new file mode 100644 index 0000000..3af18b1 --- /dev/null +++ b/server/server_instance.go @@ -0,0 +1,42 @@ +package server + +import ( + "git.mascherari.press/cwtch/protocol" + "git.mascherari.press/cwtch/server/listen" + "git.mascherari.press/cwtch/storage" + "github.com/s-rah/go-ricochet/application" + "github.com/s-rah/go-ricochet/channels" +) + +type ServerInstance struct { + rai *application.ApplicationInstance + ra *application.RicochetApplication + msi storage.MessageStoreInterface +} + +func (si *ServerInstance) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication, msi storage.MessageStoreInterface) { + si.rai = rai + si.ra = ra + si.msi = msi +} + +func (si *ServerInstance) HandleFetchRequest() []*protocol.GroupMessage { + return si.msi.FetchMessages() +} + +// HandleGroupMessage +func (si *ServerInstance) HandleGroupMessage(gm *protocol.GroupMessage) { + si.msi.AddMessage(*gm) + go si.ra.Broadcast(func(rai *application.ApplicationInstance) { + rai.Connection.Do(func() error { + channel := rai.Connection.Channel("im.cwtch.server.listen", channels.Inbound) + if channel != nil { + cslc, ok := channel.Handler.(*listen.CwtchServerListenChannel) + if ok { + cslc.SendGroupMessage(gm) + } + } + return nil + }) + }) +} diff --git a/server/server_instance_test.go b/server/server_instance_test.go new file mode 100644 index 0000000..92f4aaf --- /dev/null +++ b/server/server_instance_test.go @@ -0,0 +1,30 @@ +package server + +import ( + "git.mascherari.press/cwtch/protocol" + "git.mascherari.press/cwtch/storage" + "github.com/s-rah/go-ricochet/application" + "os" + "testing" +) + +func TestServerInstance(t *testing.T) { + si := new(ServerInstance) + ai := new(application.ApplicationInstance) + ra := new(application.RicochetApplication) + msi := new(storage.MessageStore) + os.Remove("ms.test") + msi.Init("ms.test") + gm := protocol.GroupMessage{ + Ciphertext: []byte("Hello this is a fairly average length message that we are writing here."), + Spamguard: []byte{}, + } + + si.Init(ai, ra, msi) + msi.AddMessage(gm) + res := si.HandleFetchRequest() + + if len(res) != 1 { + t.Errorf("Expected Group Messages Instead got %v", res) + } +} diff --git a/storage/message_store.go b/storage/message_store.go new file mode 100644 index 0000000..e01740a --- /dev/null +++ b/storage/message_store.go @@ -0,0 +1,67 @@ +package storage + +import ( + "bufio" + "encoding/json" + "fmt" + "git.mascherari.press/cwtch/protocol" + "os" + "sync" +) + +type MessageStoreInterface interface { + AddMessage(protocol.GroupMessage) + FetchMessages() []*protocol.GroupMessage +} + +type MessageStore struct { + file *os.File + lock sync.Mutex + messages []*protocol.GroupMessage +} + +func (ms *MessageStore) Close() { + ms.messages = nil + ms.file.Close() +} + +func (ms *MessageStore) Init(filename string) { + f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) + if err != nil { + panic(err) + } + ms.file = f + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + gms := scanner.Text() + gm := &protocol.GroupMessage{} + err := json.Unmarshal([]byte(gms), gm) + if err == nil { + ms.messages = append(ms.messages, gm) + } else { + panic(err) + } + } + + if err := scanner.Err(); err != nil { + panic(err) + } + +} + +func (ms *MessageStore) FetchMessages() (messages []*protocol.GroupMessage) { + messages = make([]*protocol.GroupMessage, len(ms.messages)) + ms.lock.Lock() + copy(messages, ms.messages) + ms.lock.Unlock() + return +} + +func (ms *MessageStore) AddMessage(gm protocol.GroupMessage) { + ms.lock.Lock() + ms.messages = append(ms.messages, &gm) + s, _ := json.Marshal(gm) + fmt.Fprintf(ms.file, "%s\n", s) + ms.lock.Unlock() +} diff --git a/storage/message_store_test.go b/storage/message_store_test.go new file mode 100644 index 0000000..46ef703 --- /dev/null +++ b/storage/message_store_test.go @@ -0,0 +1,29 @@ +package storage + +import ( + "git.mascherari.press/cwtch/protocol" + "os" + "strconv" + "testing" +) + +func TestMessageStore(t *testing.T) { + os.Remove("ms.test") + ms := new(MessageStore) + ms.Init("ms.test") + for i := 0; i < 100000; i++ { + gm := protocol.GroupMessage{ + Ciphertext: []byte("Hello this is a fairly average length message that we are writing here. " + strconv.Itoa(i)), + Spamguard: []byte{}, + } + ms.AddMessage(gm) + } + ms.Close() + ms.Init("ms.test") + m := ms.FetchMessages() + if len(m) != 100000 { + t.Errorf("Should have been 100000 was %v", len(m)) + } + ms.Close() + os.Remove("ms.test") +} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..598d8fd --- /dev/null +++ b/todo.md @@ -0,0 +1,18 @@ +# Protocol Work +[ ] Cwtch Paper Plan +[X] Message channel with Spam guard +[ ] Profile Exchange Channel +[ ] Download all messages from server. +[ ] Server timeout +[ ] Server profile update +[ ] Complete threat model/protocol doc +[ ] Offline private messaging + +# UI Work +[ ] Scroll back in the UX +[ ] Set Profile Name + + +Offline Anonymous Group Chat +- Define Threat Model +- Contrast with Other Approaches (SoK paper) diff --git a/ui/action.go b/ui/action.go new file mode 100644 index 0000000..b21e872 --- /dev/null +++ b/ui/action.go @@ -0,0 +1,15 @@ +package ui + +type ActionType int + +const ( + NONE ActionType = iota + OPEN + SEND +) + +type Action struct { + Type ActionType + ID string + Context string +} diff --git a/ui/chat_screen.go b/ui/chat_screen.go new file mode 100644 index 0000000..717a0ee --- /dev/null +++ b/ui/chat_screen.go @@ -0,0 +1,33 @@ +package ui + +type ChatScreen struct { + Title string + Elements []Element + Position int + GroupID string + MessageBoxText string +} + +func (cs *ChatScreen) Title() string { + return cs.Title +} + +func (cs *ChatScreen) MoveUp() { + if cs.Position > 0 { + cs.Position -= 1 + } +} + +func (cs *ChatScreen) MoveDown() { + if cs.Position < len(cs.Elements)-1 { + cs.Position += 1 + } +} + +func (cs *ChatScreen) Transition() { + +} + +func (cs *ChatScreen) Action() Action { + return Action{SEND, cs.GroupID, cs.MessageBoxText} +} diff --git a/ui/contact_screen.go b/ui/contact_screen.go new file mode 100644 index 0000000..aaf863e --- /dev/null +++ b/ui/contact_screen.go @@ -0,0 +1,37 @@ +package ui + +type ContactScreen struct { + Title string + Elements []Element + Position int + InSearch bool +} + +func (cs *ContactScreen) Title() string { + return cs.Title +} + +func (cs *ContactScreen) MoveUp() { + if cs.Position > 0 { + cs.Position -= 1 + } +} + +func (cs *ContactScreen) MoveDown() { + if cs.Position < len(cs.Elements)-1 { + cs.Position += 1 + } +} + +func (cs *ContactScreen) Transition() { + cs.InSearch = !cs.InSearch +} + +func (cs *ContactScreen) Action() Action { + if cs.InSearch { + return Action{NONE, "", ""} + } else { + return Action{OPEN, cs.Elements[cs.Position].ID, ""} + } + return Action{NONE, "", ""} +} diff --git a/ui/contact_screen_test.go b/ui/contact_screen_test.go new file mode 100644 index 0000000..cc81cad --- /dev/null +++ b/ui/contact_screen_test.go @@ -0,0 +1,30 @@ +package ui + +import ( + "testing" +) + +func TestBasicOperations(t *testing.T) { + screen := new(ContactScreen) + screen.Elements = []Element{{"alice", "1"}, {"bob", "2"}, {"carol", "3"}} + + screen.MoveDown() + screen.MoveDown() + action := screen.Action() + if action.Type != OPEN || action.ID != "carol" { + t.Errorf("action sequence should have opened carol instead %v", action) + } + + screen.MoveUp() + action = screen.Action() + if action.Type != OPEN || action.ID != "bob" { + t.Errorf("action sequence should have opened bob instead %v", action) + } + + screen.Transition() + action = screen.Action() + if action.Type != NONE { + t.Errorf("action sequence should done nothing instead %v", action) + } + +} diff --git a/ui/layout.go b/ui/layout.go new file mode 100644 index 0000000..2d18acc --- /dev/null +++ b/ui/layout.go @@ -0,0 +1,127 @@ +package ui + +import ( + "fmt" + "github.com/fatih/color" + "github.com/jroimartin/gocui" +) + +type Element struct { + ID string + Contents string +} + +type Layout struct { + Title string + Elements []Element + Position int +} + +func (l *Layout) Setup(g *gocui.Gui) error { + + maxX, maxY := g.Size() + + if v, err := g.SetView("title", -1, -1, maxX, 1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Clear() + v.SetCursor(0, 0) + v.SetOrigin(0, 0) + v.Frame = false + //fmt.Fprintf(v, "%v %v",l.Title, l.Position) + + } + + if v, err := g.SetView("elements", -1, 0, maxX, maxY-2); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Clear() + v.Frame = false + v.SetCursor(0, 0) + v.SetOrigin(0, 0) + if _, err := g.SetCurrentView("elements"); err != nil { + return err + } + } + l.Render(g) + return nil +} + +func (l *Layout) Render(g *gocui.Gui) error { + maxX, maxY := g.Size() + + colorTitle := color.New(color.BgMagenta).Sprintf + // + colorTitle2 := color.New(color.FgWhite, color.BgMagenta, color.Bold).Sprintf + if v, err := g.SetCurrentView("title"); err == nil { + v.Clear() + v.SetCursor(0, 0) + v.SetOrigin(0, 0) + title := "cwtch ♥ anonymity" + center := (maxX / 2) - (len(title) / 2) + + for i := 0; i < center; i++ { + v.Write([]byte(colorTitle(" "))) + } + + v.Write([]byte(colorTitle2(title))) + + //v.Write([]byte(colorTitle2())) + for i := len(title); i < maxX+1; i++ { + v.Write([]byte(colorTitle(" "))) + } + /** pandora := "cwtch " + l.Title + for i := 0; i < len([]rune(pandora)); i++ { + v.SetRune(i+1, 0, rune(pandora[i]), gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) + + } + v.SetRune(7, 0, '♥', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta)*/ + + } + + if v, err := g.SetCurrentView("elements"); err == nil { + v.Clear() + v.SetCursor(0, 0) + v.SetOrigin(0, 0) + numElementsToShow := (maxY - 3) + + listSelected := color.New(color.BgWhite, color.FgWhite, color.Bold).Sprintf + listAlt1 := color.New(color.BgWhite, color.FgBlack).Sprintf + //listAlt2:= color.New(color.BgBlack, color.FgWhite, color.Faint).Sprintf + fmt.Fprintf(v, "%v", listSelected(" "+l.Elements[l.Position].Contents)) + for i := 0; i < maxX+1; i++ { + v.Write([]byte(listSelected(" "))) + } + fmt.Fprintln(v, "") + + for i := l.Position + 1; i < numElementsToShow; i++ { + if i < len(l.Elements) { + // if i%2 != 0 { + fmt.Fprintf(v, "%v", listAlt1(" "+l.Elements[i].Contents)) + for i := 0; i < maxX; i++ { + v.Write([]byte(listAlt1(" "))) + } + fmt.Fprintln(v, "") + + } + + } + } + + return nil + +} + +func (l *Layout) MoveUp() { + if l.Position > 0 { + l.Position -= 1 + } +} + +func (l *Layout) MoveDown() { + if l.Position < len(l.Elements)-1 { + l.Position += 1 + } +} diff --git a/ui/screen.go b/ui/screen.go new file mode 100644 index 0000000..cf9f447 --- /dev/null +++ b/ui/screen.go @@ -0,0 +1,9 @@ +package ui + +type Screen interface { + Title() string + MoveUp() + MoveDown() + Transition() string + Action() Action +} diff --git a/ui/state.go b/ui/state.go new file mode 100644 index 0000000..9e87b9c --- /dev/null +++ b/ui/state.go @@ -0,0 +1,149 @@ +package ui + +/** +import ( + "fmt" + "github.com/fatih/color" + "github.com/jroimartin/gocui" +) + +type Screen struct { + Title string + Log []string + LogPosition int + IsGroup bool + IsWelcome bool +} + +func (sc *Screen) AppendToLog(log string) { + sc.Log = append(sc.Log, log) + sc.LogPosition++ +} + +type State struct { + CurrentScreen int + Screens []Screen + Gui *gocui.Gui +} + +func (s *State) Render() error { + g := s.Gui + maxX, maxY := g.Size() + + if v, err := g.SetCurrentView("out"); err == nil { + v.Clear() + v.SetCursor(0, 0) + v.SetOrigin(0, 0) + screen := s.Screens[s.CurrentScreen] + for i := 0; i < len(screen.Log); i++ { + v.Write([]byte(screen.Log[i] + "\n")) + } + } + + if v, err := g.SetCurrentView("menu"); err == nil { + s.RenderMenu(g, v, s.Screens[s.CurrentScreen]) + } else { + return err + } + + if _, err := g.SetCurrentView("cmd"); err == nil { + for i := 0; i < maxX; i++ { + g.SetRune(i, maxY-2, '─', gocui.ColorWhite, gocui.ColorBlack) + } + for i := 0; i < maxX; i++ { + g.SetRune(i, 0, ' ', gocui.ColorWhite, gocui.ColorMagenta) + } + g.SetRune(0, maxY-1, '>', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) + g.SetRune(1, maxY-1, ' ', gocui.ColorBlack, 0) + } else { + return err + } + + pandora := "cwtch " + s.Screens[s.CurrentScreen].Title + for i := 0; i < len([]rune(pandora)); i++ { + g.SetRune(i+1, 0, rune(pandora[i]), gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) + + } + g.SetRune(7, 0, '♥', gocui.ColorWhite|gocui.AttrBold, gocui.ColorMagenta) + return nil +} + +func (s *State) NewScreen(title string, isgroup bool) { + s.Screens = append(s.Screens, Screen{Title: title, IsGroup: isgroup, IsWelcome: (len(s.Screens) == 0)}) +} + +func (s *State) GetScreen(num int) *Screen { + if len(s.Screens) > num { + return &s.Screens[num] + } + return &s.Screens[0] +} + +func (s *State) PreviousScreen() { + if len(s.Screens) > 1 { + if s.CurrentScreen == 0 { + s.CurrentScreen = len(s.Screens) - 1 + } else { + s.CurrentScreen -= 1 + } + } +} + +func (s *State) GotoScreen(num int) { + if len(s.Screens) > num { + s.CurrentScreen = num + s.Render() + } + +} + +func (s *State) NextScreen() { + if len(s.Screens) == 1 || s.CurrentScreen == len(s.Screens)-1 { + s.CurrentScreen = 0 + } else { + s.CurrentScreen += 1 + } +} + +func String(c int, str string) string { + return fmt.Sprintf("\x1b[38;5;%dm%s\x1b[0m", c, str) +} + +func (s *State) RenderMenu(g *gocui.Gui, v *gocui.View, sc Screen) { + + v.Clear() + v.SetCursor(0, 0) + v.SetOrigin(0, 0) + + colorGroupChat := color.New(color.FgMagenta, color.Bold).Sprintf + colorPersonalChat := color.New(color.FgGreen, color.Bold).Sprintf + colorMenu := color.New(color.FgYellow, color.Bold).Sprintf + colorNumber := color.New(color.FgWhite).Sprintf + colorSelected := color.New(color.FgGreen, color.Bold).Sprintf + + var group string + if sc.IsGroup { + group = colorGroupChat("G") + } else if !sc.IsWelcome { + group = colorPersonalChat("P") + } else { + group = colorMenu("W") + } + + var screenSelection string + + for i := 0; i < s.CurrentScreen; i++ { + screenSelection += colorNumber("%d ", i) + } + screenSelection += colorSelected("*%d:%s ", s.CurrentScreen, sc.Title) + + for i := s.CurrentScreen + 1; i < len(s.Screens); i++ { + screenSelection += colorNumber("%d ", i) + } + + v.Write([]byte(fmt.Sprintf("⣿ %s [%s] ⡇%s", + colorMenu("MENU"), + group, + screenSelection))) + +}*/ diff --git a/ux/main.go b/ux/main.go new file mode 100644 index 0000000..412148c --- /dev/null +++ b/ux/main.go @@ -0,0 +1,57 @@ +package main + +import ( + //"fmt" + "git.mascherari.press/cwtch/ui" + "github.com/jroimartin/gocui" + "log" +) + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +var layout ui.Layout + +func cursorDown(g *gocui.Gui, v *gocui.View) error { + layout.MoveDown() + // layout.Position = 2 + layout.Render(g) + return nil +} + +func cursorUp(g *gocui.Gui, v *gocui.View) error { + layout.MoveUp() + // layout.Position = 3 + return layout.Render(g) +} + +func main() { + g, err := gocui.NewGui(gocui.OutputNormal) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + g.Cursor = false + layout.Title = "Cwtch" + layout.Position = 1 + layout.Elements = []ui.Element{{"Alice"}, {"Bob"}, {"Carol"}, {"Malory"}} + + g.SetManagerFunc(layout.Setup) + + if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + //return err + } + if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + //return err + } + + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } +}