commit
5d7bdce118
50 changed files with 3864 additions and 0 deletions
@ -0,0 +1,9 @@ |
|||
# Cwtch |
|||
|
|||
## Minimal Viable Product Feature Set |
|||
|
|||
* Connecting Dialog |
|||
* Profiles |
|||
* Server Setup |
|||
* Verification |
|||
* Peer-2-Peer Chat |
@ -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) { |
|||
|
|||
} |
@ -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() { |
|||
} |
@ -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) |
|||
} |
@ -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() |
|||
} |
@ -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) |
|||
|
|||
} |
@ -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) |
|||
} |
|||
} |
|||
} |
@ -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) |
|||
|
|||
} |
@ -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() |
|||
} |
@ -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) |
|||
|
|||
} |
@ -0,0 +1 @@ |
|||
{"Profile":{"Name":"sarah","Ed25519PublicKey":"QUqkM0hmJ6UnfLFirkIqC0ZzxNVbMc55ePtpihk4QP4=","Contacts":{},"Ed25519PrivateKey":"B3gR0WdoGfGLONyV6Mq685ltyAE3coKo39Z6FtjOg8pBSqQzSGYnpSd8sWKuQioLRnPE1Vsxznl4+2mKGThA/g==","OnionPrivateKey":null,"Groups":{}},"PendingContacts":null,"PendingInvites":{}} |
@ -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) |
|||
} |
|||
} |
@ -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() |
|||
} |
|||
} |
|||
} |
@ -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, "" |
|||
} |
@ -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) |
|||
} |
@ -0,0 +1,13 @@ |
|||
package model |
|||
|
|||
import ( |
|||
"time" |
|||
) |
|||
|
|||
type Message struct { |
|||
Timestamp time.Time |
|||
PeerID string |
|||
Message string |
|||
Signature []byte |
|||
Verified bool |
|||
} |
@ -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 |
|||
} |
@ -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":{}} |
@ -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) |
|||
} |
|||
} |
@ -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. |
|||
|