Compare commits

..

2 Commits

Author SHA1 Message Date
Dan Ballard 91e1cbb97b bump libricochet go and bine 2020-01-22 14:35:08 -05:00
Dan Ballard 632aaa6382 testing windows bine hide 2020-01-21 23:34:43 -05:00
92 changed files with 4371 additions and 3146 deletions

View File

@ -13,9 +13,9 @@ pipeline:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- go get -u golang.org/x/lint/golint
- export GO111MODULE=on
- go mod vendor
- go get -u golang.org/x/lint/golint
quality:
image: golang
when:
@ -41,7 +41,9 @@ pipeline:
branch: master
event: [ push, pull_request ]
commands:
- go test -race -v cwtch.im/cwtch/testing/
- ./tor -f ./torrc
- sleep 15
- go test -v cwtch.im/cwtch/testing/
notify-email:
image: drillster/drone-email
host: build.openprivacy.ca

1
.gitignore vendored
View File

@ -9,6 +9,5 @@ server/app/messages
.reviewboardrc
/vendor/
/testing/tor/
/storage/*/testing/
/storage/testing/
/testing/storage/

View File

@ -7,10 +7,10 @@ import (
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/storage"
"cwtch.im/tapir/primitives"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"io/ioutil"
"os"
"path"
@ -52,7 +52,6 @@ type Application interface {
GetPrimaryBus() event.Manager
GetEventBus(onion string) event.Manager
QueryACNStatus()
QueryACNVersion()
ShutdownPeer(string)
Shutdown()
@ -91,7 +90,7 @@ func (ac *applicationCore) CreatePeer(name string) (*model.Profile, error) {
_, exists := ac.eventBuses[profile.Onion]
if exists {
return nil, fmt.Errorf("error: profile for onion %v already exists", profile.Onion)
return nil, fmt.Errorf("Error: profile for onion %v already exists", profile.Onion)
}
eventBus := event.NewEventManager()
@ -119,22 +118,22 @@ func (app *application) CreateTaggedPeer(name string, password string, tag strin
app.storage[profile.Onion] = profileStore
pc := app.storage[profile.Onion].GetProfileCopy(true)
p := peer.FromProfile(pc)
p.Init(app.eventBuses[profile.Onion])
peer := peer.FromProfile(pc)
peer.Init(app.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
blockedPeers := profile.BlockedPeers()
// TODO: Would be nice if ProtocolEngine did not need to explicitly be given the Private Key.
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], profile.GetContacts(), blockedPeers)
app.peers[profile.Onion] = p
app.peers[profile.Onion] = peer
app.engines[profile.Onion] = engine
if tag != "" {
p.SetAttribute(AttributeTag, tag)
peer.SetAttribute(AttributeTag, tag)
}
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.True}))
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion}))
}
// CreatePeer creates a new Peer with the given name and required accessories (eventbus, storage, protocol engine)
@ -178,7 +177,7 @@ func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProfileFn LoadProfileFn) error {
files, err := ioutil.ReadDir(path.Join(ac.directory, "profiles"))
if err != nil {
return fmt.Errorf("error: cannot read profiles directory: %v", err)
return fmt.Errorf("Error: cannot read profiles directory: %v", err)
}
for _, file := range files {
@ -214,15 +213,15 @@ func (app *application) LoadProfiles(password string) {
peer := peer.FromProfile(profile)
peer.Init(app.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
blockedPeers := profile.BlockedPeers()
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], profile.GetContacts(), blockedPeers)
app.appmutex.Lock()
app.peers[profile.Onion] = peer
app.storage[profile.Onion] = profileStore
app.engines[profile.Onion] = engine
app.appmutex.Unlock()
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.False}))
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion}))
count++
})
if count == 0 {
@ -247,12 +246,10 @@ func (ac *applicationCore) GetEventBus(onion string) event.Manager {
func (app *application) getACNStatusHandler() func(int, string) {
return func(progress int, status string) {
progStr := strconv.Itoa(progress)
app.peerLock.Lock()
app.appBus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
app.appBus.Publish(event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status))
for _, bus := range app.eventBuses {
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
bus.Publish(event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status))
}
app.peerLock.Unlock()
}
}
@ -261,11 +258,6 @@ func (app *application) QueryACNStatus() {
app.getACNStatusHandler()(prog, status)
}
func (app *application) QueryACNVersion() {
version := app.acn.GetVersion()
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (app *application) ShutdownPeer(onion string) {
app.appmutex.Lock()

View File

@ -1,7 +1,7 @@
package app
import "cwtch.im/cwtch/event"
import "git.openprivacy.ca/openprivacy/log"
import "git.openprivacy.ca/openprivacy/libricochet-go/log"
const (
// DestApp should be used as a destination for IPC messages that are for the application itself an not a peer

View File

@ -6,7 +6,7 @@ import (
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/storage"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"path"
"strconv"
"sync"
@ -45,8 +45,7 @@ func (ac *applicationClient) handleEvent(ev *event.Event) {
key := ev.Data[event.Key]
salt := ev.Data[event.Salt]
reload := ev.Data[event.Status] == event.StorageRunning
created := ev.Data[event.Created]
ac.newPeer(localID, key, salt, reload, created)
ac.newPeer(localID, key, salt, reload)
case event.DeletePeer:
onion := ev.Data[event.Identity]
ac.handleDeletedPeer(onion)
@ -56,14 +55,12 @@ func (ac *applicationClient) handleEvent(ev *event.Event) {
ac.appBus.Publish(*ev)
case event.ACNStatus:
ac.appBus.Publish(*ev)
case event.ACNVersion:
ac.appBus.Publish(*ev)
case event.ReloadDone:
ac.appBus.Publish(*ev)
}
}
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool, created string) {
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool) {
var keyBytes [32]byte
var saltBytes [128]byte
copy(keyBytes[:], key)
@ -86,13 +83,13 @@ func (ac *applicationClient) newPeer(localID, key, salt string, reload bool, cre
peer := peer.FromProfile(profile)
peer.Init(eventBus)
ac.peerLock.Lock()
defer ac.peerLock.Unlock()
ac.acmutex.Lock()
defer ac.acmutex.Unlock()
ac.peers[profile.Onion] = peer
ac.eventBuses[profile.Onion] = eventBus
npEvent := event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: created})
npEvent := event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion})
if reload {
npEvent.Data[event.Status] = event.StorageRunning
npEvent.Data[event.Status] = "running"
}
ac.appBus.Publish(npEvent)
@ -149,11 +146,6 @@ func (ac *applicationClient) QueryACNStatus() {
ac.bridge.Write(&message)
}
func (ac *applicationClient) QueryACNVersion() {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNVersion, map[event.Field]string{})}
ac.bridge.Write(&message)
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (ac *applicationClient) ShutdownPeer(onion string) {
ac.acmutex.Lock()

View File

@ -6,9 +6,9 @@ import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/storage"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"path"
"strconv"
"sync"
@ -67,7 +67,6 @@ func (as *applicationService) handleEvent(ev *event.Event) {
for _, storage := range as.storage {
peerMsg := *storage.GetNewPeerMessage()
peerMsg.Data[event.Status] = event.StorageRunning
peerMsg.Data[event.Created] = event.False
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
as.bridge.Write(&message)
}
@ -85,9 +84,6 @@ func (as *applicationService) handleEvent(ev *event.Event) {
case event.GetACNStatus:
prog, status := as.acn.GetBootstrapStatus()
as.getACNStatusHandler()(prog, status)
case event.GetACNVersion:
version := as.acn.GetVersion()
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNVersion, event.Data, version)})
case event.ShutdownPeer:
onion := ev.Data[event.Identity]
as.ShutdownPeer(onion)
@ -111,16 +107,15 @@ func (as *applicationService) createPeer(name, password, tag string) {
profileStore := storage.CreateProfileWriterStore(as.eventBuses[profile.Onion], path.Join(as.directory, "profiles", profile.LocalID), password, profile)
peerAuthorizations := profile.ContactsAuthorizations()
blockedPeers := profile.BlockedPeers()
// TODO: Would be nice if ProtocolEngine did not need to explicitly be given the Private Key.
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], peerAuthorizations)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], profile.GetContacts(), blockedPeers)
as.storage[profile.Onion] = profileStore
as.engines[profile.Onion] = engine
peerMsg := *profileStore.GetNewPeerMessage()
peerMsg.Data[event.Created] = event.True
peerMsg.Data[event.Status] = event.StorageNew
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
as.bridge.Write(&message)
@ -131,16 +126,15 @@ func (as *applicationService) loadProfiles(password string) {
as.applicationCore.LoadProfiles(password, false, func(profile *model.Profile, profileStore storage.ProfileStore) {
as.eventBuses[profile.Onion] = event.IPCEventManagerFrom(as.bridge, profile.Onion, as.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
blockedPeers := profile.BlockedPeers()
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], peerAuthorizations)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], profile.GetContacts(), blockedPeers)
as.asmutex.Lock()
as.storage[profile.Onion] = profileStore
as.engines[profile.Onion] = engine
as.asmutex.Unlock()
peerMsg := *profileStore.GetNewPeerMessage()
peerMsg.Data[event.Created] = event.False
peerMsg.Data[event.Status] = event.StorageNew
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
as.bridge.Write(&message)
@ -155,11 +149,9 @@ func (as *applicationService) loadProfiles(password string) {
func (as *applicationService) getACNStatusHandler() func(int, string) {
return func(progress int, status string) {
progStr := strconv.Itoa(progress)
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status)})
as.applicationCore.coremutex.Lock()
defer as.applicationCore.coremutex.Unlock()
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status)})
for _, bus := range as.eventBuses {
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
bus.Publish(event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status))
}
}
}

View File

@ -2,8 +2,8 @@ package app
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"sync"
"cwtch.im/cwtch/app/plugins"
@ -11,7 +11,6 @@ import (
)
type appletPeers struct {
peerLock sync.Mutex
peers map[string]peer.CwtchPeer
launched bool // bit hacky, place holder while we transition to full multi peer support and a better api
}
@ -47,17 +46,13 @@ func (ap *appletPeers) init() {
// LaunchPeers starts each peer Listening and connecting to peers and groups
func (ap *appletPeers) LaunchPeers() {
log.Debugf("appletPeers LaunchPeers\n")
ap.peerLock.Lock()
defer ap.peerLock.Unlock()
if ap.launched {
return
}
for pid, p := range ap.peers {
log.Debugf("Launching %v\n", pid)
for _, p := range ap.peers {
p.Listen()
log.Debugf("done Listen() for %v\n", pid)
p.StartPeersConnections()
log.Debugf("done StartPeersConnections() for %v\n", pid)
p.StartGroupConnections()
}
ap.launched = true
}
@ -65,11 +60,8 @@ func (ap *appletPeers) LaunchPeers() {
// ListPeers returns a map of onions to their profile's Name
func (ap *appletPeers) ListPeers() map[string]string {
keys := map[string]string{}
ap.peerLock.Lock()
defer ap.peerLock.Unlock()
for k, p := range ap.peers {
keys[k] = p.GetOnion()
keys[k] = p.GetProfile().Name
}
return keys
}
@ -113,7 +105,7 @@ func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event
pluginsinf, _ := ap.plugins.Load(peerid)
peerPlugins := pluginsinf.([]plugins.Plugin)
newp := plugins.Get(id, bus, acn, peerid)
newp := plugins.Get(id, bus, acn)
newp.Start()
peerPlugins = append(peerPlugins, newp)
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)

View File

@ -7,21 +7,21 @@ import (
"cwtch.im/cwtch/protocol/connections"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"os"
"time"
)
func waitForPeerGroupConnection(peer peer.CwtchPeer, groupID string) error {
for {
group := peer.GetGroup(groupID)
if group != nil {
state, _ := peer.GetGroupState(groupID)
_, ok := peer.GetProfile().Groups[groupID]
if ok {
state := peer.GetGroupState(groupID)
if state == connections.FAILED {
return errors.New("Connection to group " + groupID + " failed!")
}
if state != connections.AUTHENTICATED {
fmt.Printf("peer %v waiting to authenticate with group %v 's server, current state: %v\n", peer.GetOnion(), groupID, connections.ConnectionStateName[state])
fmt.Printf("peer %v waiting to authenticate with group %v 's server, current state: %v\n", peer.GetProfile().Onion, groupID, connections.ConnectionStateName[state])
time.Sleep(time.Second * 10)
continue
}
@ -41,7 +41,7 @@ func main() {
serverAddr := os.Args[1]
acn, err := tor.NewTorACN(".", "")
acn, err := connectivity.StartTor(".", "")
if err != nil {
fmt.Printf("Could not start tor: %v\n", err)
os.Exit(1)
@ -70,7 +70,7 @@ func main() {
timeout := 1 * time.Second
timeElapsed := 0 * time.Second
for {
_, err := botPeer.SendMessageToGroupTracked(groupID, timeout.String())
err := botPeer.SendMessageToGroup(groupID, timeout.String())
if err != nil {
fmt.Printf("Sent to group on server %v failed at interval %v of total %v with: %v\n", serverAddr, timeout, timeElapsed, err)
os.Exit(1)

625
app/cli/main.go Normal file
View File

@ -0,0 +1,625 @@
package main
import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
peer2 "cwtch.im/cwtch/peer"
"bytes"
"cwtch.im/cwtch/model"
"fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"github.com/c-bata/go-prompt"
"golang.org/x/crypto/ssh/terminal"
"os"
"os/user"
"path"
"strings"
"syscall"
"time"
)
var app app2.Application
var peer peer2.CwtchPeer
var group *model.Group
var groupFollowBreakChan chan bool
var prmpt string
var suggestionsBase = []prompt.Suggest{
{Text: "/new-profile", Description: "create a new profile"},
{Text: "/load-profiles", Description: "loads profiles with a password"},
{Text: "/list-profiles", Description: "list active profiles"},
{Text: "/select-profile", Description: "selects an active profile to use"},
{Text: "/help", Description: "print list of commands"},
{Text: "/quit", Description: "quit cwtch"},
}
var suggestionsSelectedProfile = []prompt.Suggest{
{Text: "/info", Description: "show user info"},
{Text: "/list-contacts", Description: "retrieve a list of contacts"},
{Text: "/list-groups", Description: "retrieve a list of groups"},
{Text: "/new-group", Description: "create a new group on a server"},
{Text: "/select-group", Description: "selects a group to follow"},
{Text: "/unselect-group", Description: "stop following the current group"},
{Text: "/invite", Description: "invite a new contact"},
{Text: "/invite-to-group", Description: "invite an existing contact to join an existing group"},
{Text: "/accept-invite", Description: "accept the invite of a group"},
/*{Text: "/list-servers", Description: "retrieve a list of servers and their connection status"},
{Text: "/list-peers", Description: "retrieve a list of peers and their connection status"},*/
{Text: "/export-group", Description: "export a group invite: prints as a string"},
{Text: "/trust", Description: "trust a peer"},
{Text: "/block", Description: "block a peer - you will no longer see messages or connect to this peer"},
}
var suggestions = suggestionsBase
var usages = map[string]string{
"/new-profile": "/new-profile [name]",
"/load-profiles": "/load-profiles",
"/list-profiles": "",
"/select-profile": "/select-profile [onion]",
"/quit": "",
/* "/list-servers": "",
"/list-peers": "",*/
"/list-contacts": "",
"/list-groups": "",
"/select-group": "/select-group [groupid]",
"/unselect-group": "",
"/export-group": "/export-group [groupid]",
"/info": "",
"/send": "/send [groupid] [message]",
"/timeline": "/timeline [groupid]",
"/accept-invite": "/accept-invite [groupid]",
"/invite": "/invite [peerid]",
"/invite-to-group": "/invite-to-group [groupid] [peerid]",
"/new-group": "/new-group [server]",
"/help": "",
"/trust": "/trust [peerid]",
"/block": "/block [peerid]",
}
func printMessage(m model.Message) {
p := peer.GetContact(m.PeerID)
name := "unknown"
if p != nil {
name = p.Name
} else if peer.GetProfile().Onion == m.PeerID {
name = peer.GetProfile().Name
}
fmt.Printf("%v %v (%v): %v\n", m.Timestamp, name, m.PeerID, m.Message)
}
func startGroupFollow() {
groupFollowBreakChan = make(chan bool)
go func() {
for {
l := len(group.Timeline.GetMessages())
select {
case <-time.After(1 * time.Second):
if group == nil {
return
}
gms := group.Timeline.GetMessages()
if len(gms) != l {
fmt.Printf("\n")
for ; l < len(gms); l++ {
printMessage(gms[l])
}
fmt.Printf(prmpt)
}
case <-groupFollowBreakChan:
return
}
}
}()
}
func stopGroupFollow() {
if group != nil {
groupFollowBreakChan <- true
group = nil
}
}
func completer(d prompt.Document) []prompt.Suggest {
var s []prompt.Suggest
if d.FindStartOfPreviousWord() == 0 {
return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true)
}
w := d.CurrentLine()
// Suggest a profile id
if strings.HasPrefix(w, "/select-profile") {
s = []prompt.Suggest{}
peerlist := app.ListPeers()
for onion, peername := range peerlist {
s = append(s, prompt.Suggest{Text: onion, Description: peername})
}
}
if peer == nil {
return s
}
// Suggest groupid
if /*strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") ||*/ strings.HasPrefix(w, "/export-group") || strings.HasPrefix(w, "/select-group") {
s = []prompt.Suggest{}
groups := peer.GetGroups()
for _, groupID := range groups {
group := peer.GetGroup(groupID)
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
// Suggest unaccepted group
if strings.HasPrefix(w, "/accept-invite") {
s = []prompt.Suggest{}
groups := peer.GetGroups()
for _, groupID := range groups {
group := peer.GetGroup(groupID)
if group.Accepted == false {
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
}
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
// suggest groupid AND peerid
if strings.HasPrefix(w, "/invite-to-group") {
if d.FindStartOfPreviousWordWithSpace() == 0 {
s = []prompt.Suggest{}
groups := peer.GetGroups()
for _, groupID := range groups {
group := peer.GetGroup(groupID)
if group.Owner == "self" {
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
}
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
s = []prompt.Suggest{}
contacts := peer.GetContacts()
for _, onion := range contacts {
contact := peer.GetContact(onion)
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
// Suggest contact onion / peerid
if strings.HasPrefix(w, "/block") || strings.HasPrefix(w, "/trust") || strings.HasPrefix(w, "/invite") {
s = []prompt.Suggest{}
contacts := peer.GetContacts()
for _, onion := range contacts {
contact := peer.GetContact(onion)
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
return s
}
func handleAppEvents(em event.Manager) {
queue := event.NewQueue()
em.Subscribe(event.NewPeer, queue)
em.Subscribe(event.PeerError, queue)
for {
ev := queue.Next()
switch ev.EventType {
case event.NewPeer:
onion := ev.Data[event.Identity]
p := app.GetPeer(onion)
app.LaunchPeers()
fmt.Printf("\nLoaded profile %v (%v)\n", p.GetProfile().Name, p.GetProfile().Onion)
suggestions = append(suggestionsBase, suggestionsSelectedProfile...)
profiles := app.ListPeers()
fmt.Printf("\n%v profiles active now\n", len(profiles))
fmt.Printf("You should run `select-profile` to use a profile or `list-profiles` to view loaded profiles\n")
case event.PeerError:
err := ev.Data[event.Error]
fmt.Printf("\nError creating profile: %v\n", err)
}
}
}
func main() {
cwtch :=
`
#, #'
@@@@@@:
@@@@@@.
@'@@+#' @@@@+
''''''@ #+@ :
@''''+;+' . '
@''@' :+' , ; ##, +'
,@@ ;' #'#@''. #''@''#
# ''''''#:,,#'''''@
: @''''@ :+'''@
' @;+'@ @'#
.:# '#..# '# @
@@@@@@
@@@@@@
'@@@@
@# . .
+++, #'@+'@
''', ''''''#
.#+# ''', @'''+,
@''# ''', .#@
:; '@''# .;. ''', ' : ;. ,
@+'''@ '+'+ @++ @+'@+''''+@ #+'''#: ''';#''+@ @@@@ @@@@@@@@@ :@@@@#
#''''''# +''. +'': +'''''''''+ @'''''''# '''+'''''@ @@@@ @@@@@@@@@@@@@@@@:
@'''@@'''@ @''# ,'''@ ''+ @@''+#+ :'''@@+''' ''''@@'''' @@@@ @@@@@@@@@@@@@@@@@
'''# @''# +''@ @'''# ;''@ +''+ @''@ ,+'', '''@ #'''. @@@@ @@@@ '@@@# @@@@
;''' @@; '''# #'@'' @''@ @''+ +''# .@@ ''', '''. @@@@ @@@ @@@ .@@@
@''# #'' ''#''#@''. #''# '''. '''. +'', @@@@ @@@ @@@ @@@
@''# @''@'' #'@+'+ #''# '''. ''', +'', +@@@.@@@ @@@@ @@@, @@@ ,@@@
;''+ @, +''@'# @'+''@ @''# +''; '+ ''', +'', @@@@@@@@# @@@@ @@@. .@@@ .@@@
'''# ++'+ ''''@ ,''''# #''' @''@ '@''+ ''', ''', @@@@@@@@: @@@@ @@@; .@@@' ;@@@
@'''@@'''@ #'''. +'''' ;'''#@ :'''#@+''+ ''', ''', @@@@@@# @@@@ @@@+ ,@@@. @@@@
#''''''# @''+ @''+ +'''' @'''''''# ''', ''', #@@@. @@@@ @@@+ @@@ @@@@
@+''+@ '++@ ;++@ '#''@ ##'''@: +++, +++, :@ @@@@ @@@' @@@ '@@@
:' ' '`
fmt.Printf("%v\n\n", cwtch)
quit := false
usr, err := user.Current()
if err != nil {
log.Errorf("\nError: could not load current user: %v\n", err)
os.Exit(1)
}
acn, err := connectivity.StartTor(path.Join(usr.HomeDir, ".cwtch"), "")
if err != nil {
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)
}
app = app2.NewApp(acn, path.Join(usr.HomeDir, ".cwtch"))
go handleAppEvents(app.GetPrimaryBus())
if err != nil {
log.Errorf("Error initializing application: %v", err)
os.Exit(1)
}
log.SetLevel(log.LevelDebug)
fmt.Printf("\nWelcome to Cwtch!\n")
fmt.Printf("If this if your first time you should create a profile by running `/new-profile`\n")
fmt.Printf("`/load-profiles` will prompt you for a password and load profiles from storage\n")
fmt.Printf("`/help` will show you other available commands\n")
fmt.Printf("There is full [TAB] completion support\n\n")
var history []string
for !quit {
prmpt = "cwtch> "
if group != nil {
prmpt = fmt.Sprintf("cwtch %v (%v) [%v] say> ", peer.GetProfile().Name, peer.GetProfile().Onion, group.GroupID)
} else if peer != nil {
prmpt = fmt.Sprintf("cwtch %v (%v)> ", peer.GetProfile().Name, peer.GetProfile().Onion)
}
text := prompt.Input(prmpt, completer, prompt.OptionSuggestionBGColor(prompt.Purple),
prompt.OptionDescriptionBGColor(prompt.White),
prompt.OptionPrefixTextColor(prompt.White),
prompt.OptionInputTextColor(prompt.Purple),
prompt.OptionHistory(history))
commands := strings.Split(text[0:], " ")
history = append(history, text)
if peer == nil {
if commands[0] != "/help" && commands[0] != "/quit" && commands[0] != "/new-profile" && commands[0] != "/load-profiles" && commands[0] != "/select-profile" && commands[0] != "/list-profiles" {
fmt.Printf("Profile needs to be set\n")
continue
}
}
// Send
if group != nil && !strings.HasPrefix(commands[0], "/") {
err := peer.SendMessageToGroup(group.GroupID, text)
if err != nil {
fmt.Printf("Error sending message: %v\n", err)
}
}
switch commands[0] {
case "/quit":
quit = true
case "/new-profile":
if len(commands) == 2 {
name := strings.Trim(commands[1], " ")
if name == "" {
fmt.Printf("Error creating profile, usage: %v\n", usages[commands[0]])
break
}
fmt.Print("** WARNING: PASSWORDS CANNOT BE RECOVERED! **\n")
password := ""
failcount := 0
for ; failcount < 3; failcount++ {
fmt.Print("Enter a password to encrypt the profile: ")
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
if string(bytePassword) == "" {
fmt.Print("\nBlank password not allowed.")
continue
}
fmt.Print("\nRe-enter password: ")
bytePassword2, _ := terminal.ReadPassword(int(syscall.Stdin))
if bytes.Equal(bytePassword, bytePassword2) {
password = string(bytePassword)
break
} else {
fmt.Print("\nPASSWORDS DIDN'T MATCH! Try again.\n")
}
}
if failcount >= 3 {
fmt.Printf("Error creating profile for %v: Your password entries must match!\n", name)
} else {
app.CreatePeer(name, password)
}
} else {
fmt.Printf("Error creating New Profile, usage: %s\n", usages[commands[0]])
}
case "/load-profiles":
fmt.Print("Enter a password to decrypt the profile: ")
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Printf("\nError loading profiles: %v\n", err)
continue
}
app.LoadProfiles(string(bytePassword))
if err == nil {
} else {
fmt.Printf("\nError loading profiles: %v\n", err)
}
case "/list-profiles":
peerlist := app.ListPeers()
for onion, peername := range peerlist {
fmt.Printf(" %v\t%v\n", onion, peername)
}
case "/select-profile":
if len(commands) == 2 {
p := app.GetPeer(commands[1])
if p == nil {
fmt.Printf("Error: profile '%v' does not exist\n", commands[1])
} else {
stopGroupFollow()
peer = p
suggestions = append(suggestionsBase, suggestionsSelectedProfile...)
}
// Auto cwtchPeer / Join Server
// TODO There are some privacy implications with this that we should
// think over.
for _, name := range p.GetProfile().GetContacts() {
profile := p.GetContact(name)
if profile.Trusted && !profile.Blocked {
p.PeerWithOnion(profile.Onion)
}
}
for _, groupid := range p.GetGroups() {
group := p.GetGroup(groupid)
if group.Accepted || group.Owner == "self" {
p.JoinServer(group.GroupServer)
}
}
} else {
fmt.Printf("Error selecting profile, usage: %s\n", usages[commands[0]])
}
case "/info":
if peer != nil {
fmt.Printf("Address cwtch:%v\n", peer.GetProfile().Onion)
} else {
fmt.Printf("Profile needs to be set\n")
}
case "/invite":
if len(commands) == 2 {
fmt.Printf("Inviting cwtch:%v\n", commands[1])
peer.PeerWithOnion(commands[1])
} else {
fmt.Printf("Error inviting peer, usage: %s\n", usages[commands[0]])
}
/*case "/list-peers":
peers := peer.GetPeers()
for p, s := range peers {
fmt.Printf("Name: %v Status: %v\n", p, connections.ConnectionStateName[s])
}
case "/list-servers":
servers := peer.GetServers()
for s, st := range servers {
fmt.Printf("Name: %v Status: %v\n", s, connections.ConnectionStateName[st])
}*/
case "/list-contacts":
contacts := peer.GetContacts()
for _, onion := range contacts {
c := peer.GetContact(onion)
fmt.Printf("Name: %v Onion: %v Trusted: %v\n", c.Name, c.Onion, c.Trusted)
}
case "/list-groups":
for _, gid := range peer.GetGroups() {
g := peer.GetGroup(gid)
fmt.Printf("Group Id: %v Owner: %v Accepted:%v\n", gid, g.Owner, g.Accepted)
}
case "/trust":
if len(commands) == 2 {
peer.TrustPeer(commands[1])
} else {
fmt.Printf("Error trusting peer, usage: %s\n", usages[commands[0]])
}
case "/block":
if len(commands) == 2 {
peer.BlockPeer(commands[1])
} else {
fmt.Printf("Error blocking peer, usage: %s\n", usages[commands[0]])
}
case "/accept-invite":
if len(commands) == 2 {
groupID := commands[1]
err := peer.AcceptInvite(groupID)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
group := peer.GetGroup(groupID)
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
peer.JoinServer(group.GroupServer)
}
}
} else {
fmt.Printf("Error accepting invite, usage: %s\n", usages[commands[0]])
}
case "/invite-to-group":
if len(commands) == 3 {
fmt.Printf("Inviting %v to %v\n", commands[1], commands[2])
err := peer.InviteOnionToGroup(commands[2], commands[1])
if err != nil {
fmt.Printf("Error: %v\n", err)
}
} else {
fmt.Printf("Error inviting peer to group, usage: %s\n", usages[commands[0]])
}
case "/new-group":
if len(commands) == 2 && commands[1] != "" {
fmt.Printf("Setting up a new group on server:%v\n", commands[1])
id, _, err := peer.StartGroup(commands[1])
if err == nil {
fmt.Printf("New Group [%v] created for server %v\n", id, commands[1])
group := peer.GetGroup(id)
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
peer.JoinServer(group.GroupServer)
}
} else {
fmt.Printf("Error creating new group: %v", err)
}
} else {
fmt.Printf("Error creating a new group, usage: %s\n", usages[commands[0]])
}
case "/select-group":
if len(commands) == 2 {
g := peer.GetGroup(commands[1])
if g == nil {
fmt.Printf("Error: group %s not found!\n", commands[1])
} else {
stopGroupFollow()
group = g
fmt.Printf("--------------- %v ---------------\n", group.GroupID)
gms := group.Timeline.GetMessages()
max := 20
if len(gms) < max {
max = len(gms)
}
for i := len(gms) - max; i < len(gms); i++ {
printMessage(gms[i])
}
fmt.Printf("------------------------------\n")
startGroupFollow()
}
} else {
fmt.Printf("Error selecting a group, usage: %s\n", usages[commands[0]])
}
case "/unselect-group":
stopGroupFollow()
case "/export-group":
if len(commands) == 2 {
group := peer.GetGroup(commands[1])
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
invite, _ := peer.ExportGroup(commands[1])
fmt.Printf("Invite: %v\n", invite)
}
} else {
fmt.Printf("Error exporting group, usage: %s\n", usages[commands[0]])
}
case "/import-group":
if len(commands) == 2 {
err := peer.ImportGroup(commands[1])
if err != nil {
fmt.Printf("Error importing group: %v\n", err)
} else {
fmt.Printf("Imported group!\n")
}
} else {
fmt.Printf("%v", commands)
fmt.Printf("Error importing group, usage: %s\n", usages[commands[0]])
}
case "/help":
for _, command := range suggestions {
fmt.Printf("%-18s%-56s%s\n", command.Text, command.Description, usages[command.Text])
}
case "/sendlots":
if len(commands) == 2 {
group := peer.GetGroup(commands[1])
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
for i := 0; i < 100; i++ {
fmt.Printf("Sending message: %v\n", i)
err := peer.SendMessageToGroup(commands[1], fmt.Sprintf("this is message %v", i))
if err != nil {
fmt.Printf("could not send message %v because %v\n", i, err)
}
}
fmt.Printf("Waiting 5 seconds for message to process...\n")
time.Sleep(time.Second * 5)
timeline := group.GetTimeline()
totalLatency := time.Duration(0)
maxLatency := time.Duration(0)
totalMessages := 0
for i := 0; i < 100; i++ {
found := false
for _, m := range timeline {
if m.Message == fmt.Sprintf("this is message %v", i) && m.PeerID == peer.GetProfile().Onion {
found = true
latency := m.Received.Sub(m.Timestamp)
fmt.Printf("Latency for Message %v was %v\n", i, latency)
totalLatency = totalLatency + latency
if maxLatency < latency {
maxLatency = latency
}
totalMessages++
}
}
if !found {
fmt.Printf("message %v was never received\n", i)
}
}
fmt.Printf("Average Latency for %v messages was: %vms\n", totalMessages, time.Duration(int64(totalLatency)/int64(totalMessages)))
fmt.Printf("Max Latency for %v messages was: %vms\n", totalMessages, maxLatency)
}
}
}
}
app.Shutdown()
acn.Close()
os.Exit(0)
}

View File

@ -3,7 +3,7 @@ package main
import (
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
//"bufio"
//"cwtch.im/cwtch/storage"

View File

@ -4,8 +4,8 @@ import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/utils"
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
"path"
)
@ -16,7 +16,7 @@ func main() {
log.AddEverythingFromPattern("peer/alice")
log.SetLevel(log.LevelDebug)
acn, err := tor.NewTorACN(path.Join(".", ".cwtch"), "")
acn, err := connectivity.StartTor(path.Join(".", ".cwtch"), "")
if err != nil {
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)
@ -26,7 +26,7 @@ func main() {
app.CreatePeer("alice", "be gay, do crimes")
alice := utils.WaitGetPeer(app, "alice")
app.LaunchPeers()
eventBus := app.GetEventBus(alice.GetOnion())
eventBus := app.GetEventBus(alice.GetProfile().Onion)
queue := event.NewQueue()
eventBus.Subscribe(event.NewMessageFromPeer, queue)

View File

@ -3,8 +3,8 @@ package main
import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/utils"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
"path"
"time"
@ -15,7 +15,7 @@ func main() {
// System Boilerplate, We need Tor Up and Running
log.AddEverythingFromPattern("peer/bob")
log.SetLevel(log.LevelDebug)
acn, err := tor.NewTorACN(path.Join(".", ".cwtch"), "")
acn, err := connectivity.StartTor(path.Join(".", ".cwtch"), "")
if err != nil {
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)

View File

@ -3,7 +3,6 @@ package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/log"
"sync"
"time"
)
@ -31,66 +30,50 @@ type contactRetry struct {
bus event.Manager
queue event.Queue
networkUp bool
running bool
breakChan chan bool
onion string
lastCheck time.Time
connections sync.Map //[string]*contact
}
// NewConnectionRetry returns a Plugin that when started will retry connecting to contacts with a backoff timing
func NewConnectionRetry(bus event.Manager, onion string) Plugin {
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool), connections: sync.Map{}, networkUp: false, onion: onion}
func NewConnectionRetry(bus event.Manager) Plugin {
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool), connections: sync.Map{}, networkUp: false}
return cr
}
func (cr *contactRetry) Start() {
if !cr.running {
go cr.run()
} else {
log.Errorf("Attempted to start Contact Retry plugin twice for %v", cr.onion)
}
go cr.run()
}
func (cr *contactRetry) run() {
cr.running = true
cr.bus.Subscribe(event.PeerStateChange, cr.queue)
cr.bus.Subscribe(event.ACNStatus, cr.queue)
cr.bus.Subscribe(event.ServerStateChange, cr.queue)
for {
if time.Since(cr.lastCheck) > tickTime {
cr.retryDisconnected()
cr.lastCheck = time.Now()
}
select {
case e := <-cr.queue.OutChan():
switch e.EventType {
case event.PeerStateChange:
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
state := connections.ConnectionStateToType[e.Data[event.ConnectionState]]
peer := e.Data[event.RemotePeer]
cr.handleEvent(peer, state, peerConn)
case event.ServerStateChange:
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
state := connections.ConnectionStateToType[e.Data[event.ConnectionState]]
server := e.Data[event.GroupServer]
cr.handleEvent(server, state, serverConn)
case event.ACNStatus:
prog := e.Data[event.Progress]
if prog == "100" && !cr.networkUp {
prog := e.Data[event.Progreess]
if prog == "100" && cr.networkUp == false {
cr.networkUp = true
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
p.ticks = 0
p.backoff = 1
if p.ctype == peerConn {
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
}
if p.ctype == serverConn {
cr.bus.Publish(event.NewEvent(event.RetryServerRequest, map[event.Field]string{event.GroupServer: p.id}))
}
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
return true
})
} else if prog != "100" {
@ -99,40 +82,34 @@ func (cr *contactRetry) run() {
}
case <-time.After(tickTime):
continue
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
if p.state == connections.DISCONNECTED {
p.ticks++
if p.ticks == p.backoff {
p.ticks = 0
if cr.networkUp {
if p.ctype == peerConn {
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
} else {
cr.bus.Publish(event.NewEventList(event.JoinServer, event.GroupServer, p.id))
}
}
}
}
return true
})
case <-cr.breakChan:
cr.running = false
return
}
}
}
func (cr *contactRetry) retryDisconnected() {
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
if p.state == connections.DISCONNECTED {
p.ticks++
if p.ticks >= p.backoff {
p.ticks = 0
if cr.networkUp {
if p.ctype == peerConn {
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
}
if p.ctype == serverConn {
cr.bus.Publish(event.NewEvent(event.RetryServerRequest, map[event.Field]string{event.GroupServer: p.id}))
}
}
}
}
return true
})
}
func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState, ctype connectionType) {
if _, exists := cr.connections.Load(id); !exists {
p := &contact{id: id, state: connections.DISCONNECTED, backoff: 0, ticks: 0, ctype: ctype}
p := &contact{id: id, state: connections.DISCONNECTED, backoff: 1, ticks: 0, ctype: ctype}
cr.connections.Store(id, p)
return
}
@ -141,9 +118,7 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
p := pinf.(*contact)
if state == connections.DISCONNECTED || state == connections.FAILED || state == connections.KILLED {
p.state = connections.DISCONNECTED
if p.backoff == 0 {
p.backoff = 1
} else if p.backoff < maxBakoff {
if p.backoff < maxBakoff {
p.backoff *= 2
}
p.ticks = 0
@ -151,11 +126,10 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
p.state = state
} else if state == connections.AUTHENTICATED {
p.state = state
p.backoff = 0
p.backoff = 1
}
}
func (cr *contactRetry) Shutdown() {
cr.breakChan <- true
}

View File

@ -3,25 +3,19 @@ package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/policies"
"sync"
"time"
)
// NetworkCheckError is a status for when the NetworkCheck Plugin has had an error making an out going connection indicating it may be offline
const NetworkCheckError = "Error"
// NetworkCheckSuccess is a status for when the NetworkCheck Plugin has had a successful message from a peer, indicating it is online right now
const NetworkCheckSuccess = "Success"
// networkCheck is a convenience plugin for testing high level availability of onion services
type networkCheck struct {
bus event.Manager
queue event.Queue
acn connectivity.ACN
onionsToCheck sync.Map // onion:string => true:bool
onionsToCheck []string
breakChan chan bool
running bool
offline bool
@ -40,15 +34,12 @@ func (nc *networkCheck) Start() {
func (nc *networkCheck) run() {
nc.running = true
nc.offline = true
nc.bus.Subscribe(event.ProtocolEngineStartListen, nc.queue)
nc.bus.Subscribe(event.NewMessageFromPeer, nc.queue)
nc.bus.Subscribe(event.PeerAcknowledgement, nc.queue)
nc.bus.Subscribe(event.EncryptedGroupMessage, nc.queue)
nc.bus.Subscribe(event.PeerStateChange, nc.queue)
nc.bus.Subscribe(event.ServerStateChange, nc.queue)
nc.bus.Subscribe(event.NewGetValMessageFromPeer, nc.queue)
nc.bus.Subscribe(event.NewRetValMessageFromPeer, nc.queue)
var lastMessageReceived time.Time
for {
select {
@ -61,26 +52,18 @@ func (nc *networkCheck) run() {
// and then we will wait a minute and check the connection for the first time (the onion should be up)
// under normal operating circumstances
case event.ProtocolEngineStartListen:
if _, exists := nc.onionsToCheck.Load(e.Data[event.Onion]); !exists {
log.Debugf("initiating connection check for %v", e.Data[event.Onion])
nc.onionsToCheck.Store(e.Data[event.Onion], true)
if time.Since(lastMessageReceived) > time.Minute {
nc.selfTest()
}
}
log.Debugf("initiating connection check for %v", e.Data[event.Onion])
nc.onionsToCheck = append(nc.onionsToCheck, e.Data[event.Onion])
case event.PeerStateChange:
fallthrough
case event.ServerStateChange:
// if we successfully connect / authenticated to a remote server / peer then we obviously have internet
connectionState := e.Data[event.ConnectionState]
nc.offlineLock.Lock()
if connectionState == connections.ConnectionStateName[connections.AUTHENTICATED] || connectionState == connections.ConnectionStateName[connections.CONNECTED] {
if nc.offline && (connectionState == connections.ConnectionStateName[connections.AUTHENTICATED] || connectionState == connections.ConnectionStateName[connections.CONNECTED]) {
lastMessageReceived = time.Now()
if nc.offline {
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: NetworkCheckSuccess}))
nc.offline = false
}
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: "Success"}))
nc.offline = false
}
nc.offlineLock.Unlock()
default:
@ -91,15 +74,17 @@ func (nc *networkCheck) run() {
lastMessageReceived = time.Now()
nc.offlineLock.Lock()
if nc.offline {
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: NetworkCheckSuccess}))
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: "Success"}))
nc.offline = false
}
nc.offlineLock.Unlock()
}
case <-time.After(tickTime):
// if we haven't received an action in the last minute...kick off a set of testing
if time.Since(lastMessageReceived) > time.Minute {
nc.selfTest()
if time.Now().Sub(lastMessageReceived) > time.Minute {
for _, onion := range nc.onionsToCheck {
go nc.checkConnection(onion)
}
}
}
}
@ -113,23 +98,11 @@ func (nc *networkCheck) Shutdown() {
}
}
func (nc *networkCheck) selfTest() {
nc.onionsToCheck.Range(func(key, val interface{}) bool {
go nc.checkConnection(key.(string))
return true
})
}
//
func (nc *networkCheck) checkConnection(onion string) {
prog, _ := nc.acn.GetBootstrapStatus()
if prog != 100 {
return
}
// we want to definitively time these actions out faster than tor will, because these onions should definitely be
// online
ClientTimeout := TimeoutPolicy(time.Second * 60)
ClientTimeout := policies.TimeoutPolicy(time.Second * 60)
err := ClientTimeout.ExecuteAction(func() error {
conn, _, err := nc.acn.Open(onion)
if err == nil {
@ -141,35 +114,12 @@ func (nc *networkCheck) checkConnection(onion string) {
defer nc.offlineLock.Unlock()
// regardless of the outcome we want to report a status to let anyone who might care know that we did do a check
if err != nil {
log.Debugf("publishing network error for %v -- %v\n", onion, err)
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: err.Error(), event.Status: NetworkCheckError}))
nc.offline = true
log.Debugf("publishing network error for %v", onion)
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: err.Error(), event.Status: "Error"}))
nc.offline = false
} else {
log.Debugf("publishing network success for %v", onion)
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: "", event.Status: NetworkCheckSuccess}))
nc.offline = false
}
}
// TODO we might want to reuse this, but for now it is only used by this plugin so it can live here
// TimeoutPolicy is an interface for enforcing common timeout patterns
type TimeoutPolicy time.Duration
// ExecuteAction runs a function and returns an error if it hasn't returned
// by the time specified by TimeoutPolicy
func (tp *TimeoutPolicy) ExecuteAction(action func() error) error {
c := make(chan error)
go func() {
c <- action()
}()
tick := time.NewTicker(time.Duration(*tp))
select {
case <-tick.C:
return fmt.Errorf("ActionTimedOutError")
case err := <-c:
return err
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: "", event.Status: "Success"}))
nc.offline = true
}
}

View File

@ -2,7 +2,7 @@ package plugins
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
)
// PluginID is used as an ID for signaling plugin activities
@ -21,10 +21,10 @@ type Plugin interface {
}
// Get is a plugin factory for the requested plugin
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) Plugin {
func Get(id PluginID, bus event.Manager, acn connectivity.ACN) Plugin {
switch id {
case CONNECTIONRETRY:
return NewConnectionRetry(bus, onion)
return NewConnectionRetry(bus)
case NETWORKCHECK:
return NewNetworkCheck(bus, acn)
}

View File

@ -2,7 +2,6 @@ package utils
import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/peer"
"time"
)
@ -12,14 +11,13 @@ import (
// however for small utility use, this function which polls the app until the peer is created
// may fill that usecase better
func WaitGetPeer(app app2.Application, name string) peer.CwtchPeer {
for {
for id := range app.ListPeers() {
peer := app.GetPeer(id)
localName, _ := peer.GetAttribute(attr.GetLocalScope("name"))
if localName == name {
return peer
for true {
for id, n := range app.ListPeers() {
if n == name {
return app.GetPeer(id)
}
}
time.Sleep(100 * time.Millisecond)
}
return nil
}

View File

@ -3,16 +3,17 @@
package bridge
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"encoding/base64"
"encoding/binary"
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
"os"
"sync"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"syscall"
"time"
"cwtch.im/cwtch/event"
"encoding/json"
"os"
"sync"
)
/* pipeBridge creates a pair of named pipes
@ -75,37 +76,23 @@ func NewPipeBridgeService(inFilename, outFilename string) event.IPCBridge {
return pb
}
func (pb *pipeBridge) setState(state connections.ConnectionState) {
pb.lock.Lock()
defer pb.lock.Unlock()
pb.state = state
}
func (pb *pipeBridge) getState() connections.ConnectionState {
pb.lock.Lock()
defer pb.lock.Unlock()
return pb.state
}
func (pb *pipeBridge) connectionManager() {
for pb.getState() != connections.KILLED {
for pb.state != connections.KILLED {
log.Debugf("clientConnManager loop start init\n")
pb.setState(connections.CONNECTING)
pb.state = connections.CONNECTING
var err error
log.Debugf("%v open file infile\n", pb.name)
pb.in, err = os.OpenFile(pb.infile, os.O_RDWR, 0600)
if err != nil {
pb.setState(connections.DISCONNECTED)
pb.state = connections.DISCONNECTED
continue
}
log.Debugf("%v open file outfile\n", pb.name)
pb.out, err = os.OpenFile(pb.outfile, os.O_RDWR, 0600)
if err != nil {
pb.setState(connections.DISCONNECTED)
pb.state = connections.DISCONNECTED
continue
}
@ -169,7 +156,10 @@ func (pb *pipeBridge) threeShakeClient() bool {
if string(resp) == synack {
stop <- true
err := pb.writeString([]byte(ack))
return err == nil
if err != nil {
return false
}
return true
}
}
}
@ -177,12 +167,12 @@ func (pb *pipeBridge) threeShakeClient() bool {
func (pb *pipeBridge) handleConns() {
if !pb.threeShake() {
pb.setState(connections.FAILED)
pb.state = connections.FAILED
pb.closeReset()
return
}
pb.setState(connections.AUTHENTICATED)
pb.state = connections.AUTHENTICATED
pb.closedChan = make(chan bool, 5)
@ -193,8 +183,8 @@ func (pb *pipeBridge) handleConns() {
<-pb.closedChan
log.Debugf("handleConns <-closedChan (%v)\n", pb.name)
if pb.getState() != connections.KILLED {
pb.setState(connections.FAILED)
if pb.state != connections.KILLED {
pb.state = connections.FAILED
}
pb.closeReset()
log.Debugf("handleConns done for %v, exit\n", pb.name)
@ -206,7 +196,7 @@ func (pb *pipeBridge) closeReset() {
close(pb.read)
pb.write.Close()
if pb.getState() != connections.KILLED {
if pb.state != connections.KILLED {
pb.read = make(chan event.IPCMessage, maxBufferSize)
pb.write = newInfiniteChannel()
}
@ -229,7 +219,7 @@ func (pb *pipeBridge) handleWrite() {
} else {
log.Debugf("handleWrite <- message: %v\n", message)
}
if pb.getState() == connections.AUTHENTICATED {
if pb.state == connections.AUTHENTICATED {
encMessage := &event.IPCMessage{Dest: message.Dest, Message: event.Event{EventType: message.Message.EventType, EventID: message.Message.EventID, Data: make(map[event.Field]string)}}
for k, v := range message.Message.Data {
encMessage.Message.Data[k] = base64.StdEncoding.EncodeToString([]byte(v))
@ -286,7 +276,7 @@ func (pb *pipeBridge) Read() (*event.IPCMessage, bool) {
log.Debugf("Read() %v...\n", pb.name)
var ok = false
var message event.IPCMessage
for !ok && pb.getState() != connections.KILLED {
for !ok && pb.state != connections.KILLED {
message, ok = <-pb.read
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("Read %v: %v %v ...\n", pb.name, message.Dest, message.Message.EventType)
@ -294,7 +284,7 @@ func (pb *pipeBridge) Read() (*event.IPCMessage, bool) {
log.Debugf("Read %v: %v\n", pb.name, message)
}
}
return &message, pb.getState() != connections.KILLED
return &message, pb.state != connections.KILLED
}
func (pb *pipeBridge) Write(message *event.IPCMessage) {
@ -308,7 +298,7 @@ func (pb *pipeBridge) Write(message *event.IPCMessage) {
}
func (pb *pipeBridge) Shutdown() {
log.Debugf("pb.Shutdown() for %v currently in state: %v\n", pb.name, connections.ConnectionStateName[pb.getState()])
log.Debugf("pb.Shutdown() for %v currently in state: %v\n", pb.name, connections.ConnectionStateName[pb.state])
pb.state = connections.KILLED
pb.closedChan <- true
log.Debugf("Done Shutdown for %v\n", pb.name)

View File

@ -2,7 +2,7 @@ package bridge
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
"testing"
"time"
@ -93,7 +93,7 @@ func stableService(t *testing.T, in, out string, done chan bool) {
return
}
if message1.Message.EventType != event.NewPeer {
t.Errorf("Wrong message received, expected NewPeer\n")
t.Errorf("Wrong message recieved, expected NewPeer\n")
done <- true
return
}
@ -107,7 +107,7 @@ func stableService(t *testing.T, in, out string, done chan bool) {
return
}
if message2.Message.EventType != event.DeleteContact {
t.Errorf("Wrong message received, expected DeleteContact, got %v\n", message2)
t.Errorf("Wrong message recieved, expected DeleteContact, got %v\n", message2)
done <- true
return
}

View File

@ -19,14 +19,11 @@ const (
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
RetryPeerRequest = Type("RetryPeerRequest")
// RetryServerRequest
// Asks CwtchPeer to retry a server connection...
// GroupServer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
RetryServerRequest = Type("RetryServerRequest")
// RemotePeer
// Authorization(model.peer.Auth_...)
SetPeerAuthorization = Type("UpdatePeerAuthorization")
// Blocking Events both Block and Unblock have the same structure
// attributes:
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
BlockPeer = Type("BlockPeer")
UnblockPeer = Type("UnblockPeer")
// Turn on/off blocking of unknown peers (if peers aren't in the contact list then they will be autoblocked
BlockUnknownPeers = Type("BlockUnknownPeers")
@ -35,9 +32,6 @@ const (
// GroupServer
JoinServer = Type("JoinServer")
// attributes GroupServer - the onion of the server to leave
LeaveServer = Type("LeaveServer")
ProtocolEngineStartListen = Type("ProtocolEngineStartListen")
ProtocolEngineStopped = Type("ProtocolEngineStopped")
@ -48,16 +42,10 @@ const (
// TimestampReceived [eg time.Now().Format(time.RFC3339Nano)]
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"]
// GroupInvite: [eg "torv3....."]
// Imported
NewGroupInvite = Type("NewGroupInvite")
// Inform the UI about a new group
// GroupID: groupID (allows them to fetch from the peer)
NewGroup = Type("NewGroup")
// GroupID
AcceptGroupInvite = Type("AcceptGroupInvite")
RejectGroupInvite = Type("RejectGroupInvite")
SendMessageToGroup = Type("SendMessagetoGroup")
@ -66,10 +54,6 @@ const (
//TimestampReceived, TimestampSent, Data(Message), GroupID, Signature, PreviousSignature, RemotePeer
NewMessageFromGroup = Type("NewMessageFromGroup")
// Sent if a Group Key is detected as being used outside of expected parameters (e.g. with tampered signatures)
// GroupID: The ID of the Group that is presumed compromised
GroupCompromised = Type("GroupCompromised")
// an error was encountered trying to send a particular Message to a group
// attributes:
// GroupServer: The server the Message was sent to
@ -80,42 +64,12 @@ const (
SendMessageToPeer = Type("SendMessageToPeer")
NewMessageFromPeer = Type("NewMessageFromPeer")
// RemotePeer, scope, path
NewGetValMessageFromPeer = Type("NewGetValMessageFromPeer")
// RemotePeer, val, exists
SendRetValMessageToPeer = Type("SendRetValMessageToPeer")
// RemotePeer, scope, val
SendGetValMessageToPeer = Type("SendGetValMessageToPeer")
// RemotePeer, scope, path, data, exists
NewRetValMessageFromPeer = Type("NewRetValMessageFromPeer")
// Peer acknowledges a previously sent message
// attributes
// EventID: The original event id that the peer is responding too.
// RemotePeer: The peer associated with the acknowledgement
PeerAcknowledgement = Type("PeerAcknowledgement")
// Like PeerAcknowledgement but with message index instead of event ID
// attributes
// Index: The original index of the message that the peer is responding too.
// RemotePeer: The peer associated with the acknowledgement
IndexedAcknowledgement = Type("IndexedAcknowledgement")
// Like PeerAcknowledgement but with message index instead of event ID
// attributes
// Index: The original index of the message that the peer is responding too.
// RemotePeer: The peer associated with the acknowledgement
IndexedFailure = Type("IndexedFailure")
// UpdateMessageFlags will change the flags associated with a given message.
// Handle
// Message Index
// Flags
UpdateMessageFlags = Type("UpdateMessageFlags")
// attributes:
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"]
// Error: string describing the error
@ -126,7 +80,6 @@ const (
// a peer contact has been added
// attributes:
// RemotePeer [eg ""]
// Authorization
PeerCreated = Type("PeerCreated")
// Password, NewPassword
@ -188,8 +141,8 @@ const (
// ProfileName, Password, Data(tag)
CreatePeer = Type("CreatePeer")
// app: Identity(onion), Created(bool)
// service -> client: Identity(localId), Password, [Status(new/default=blank || from reload='running')], Created(bool)
// service -> client: Identity(localId), Password, [Status(new/default=blank || from reload='running')]
// app -> Key, Salt
NewPeer = Type("NewPeer")
// Identity(onion)
@ -221,25 +174,17 @@ const (
// Error(err)
AppError = Type("AppError")
GetACNStatus = Type("GetACNStatus")
GetACNVersion = Type("GetACNVersion")
GetACNStatus = Type("GetACNStatus")
// Progress, Status
ACNStatus = Type("ACNStatus")
// Data
ACNVersion = Type("ACNVersion")
// Network Status
// Status: Success || Error
// Error: Description of the Error
// Onion: the local onion we attempt to check
NetworkStatus = Type("NetworkError")
// Notify the UI that a Server has been added
// Onion = Server Onion
ServerCreated = Type("ServerAdded")
// For debugging. Allows test to emit a Syn and get a response Ack(eventID) when the subsystem is done processing a queue
Syn = Type("Syn")
Ack = Type("Ack")
@ -263,50 +208,27 @@ const (
Identity = Field("Identity")
GroupID = Field("GroupID")
GroupServer = Field("GroupServer")
ServerTokenY = Field("ServerTokenY")
ServerTokenOnion = Field("ServerTokenOnion")
GroupInvite = Field("GroupInvite")
GroupID = Field("GroupID")
GroupServer = Field("GroupServer")
GroupInvite = Field("GroupInvite")
ProfileName = Field("ProfileName")
Password = Field("Password")
NewPassword = Field("NewPassword")
Created = Field("Created")
ConnectionState = Field("ConnectionState")
Key = Field("Key")
Data = Field("Data")
Scope = Field("Scope")
Path = Field("Path")
Exists = Field("Exists")
Key = Field("Key")
Data = Field("Data")
Salt = Field("Salt")
Error = Field("Error")
Progress = Field("Progress")
Progreess = Field("Progress")
Status = Field("Status")
EventID = Field("EventID")
EventContext = Field("EventContext")
Index = Field("Index")
// Handle denotes a contact handle of any type.
Handle = Field("Handle")
// Flags denotes a set of message flags
Flags = Field("Flags")
Authorization = Field("Authorization")
KeyBundle = Field("KeyBundle")
// Indicate whether an event was triggered by a user import
Imported = Field("Imported")
Source = Field("Source")
)
// Defining Common errors
@ -325,27 +247,4 @@ const (
ContextAck = "im.cwtch.acknowledgement"
ContextInvite = "im.cwtch.invite"
ContextRaw = "im.cwtch.raw"
ContextGetVal = "im.cwtch.getVal"
ContextRetVal = "im.cwtch.retVal"
)
// Define Default Attribute Keys
const (
SaveHistoryKey = "SavePeerHistory"
)
// Define Default Attribute Values
const (
// Save History has 3 distinct states. By default we don't save history (DefaultDeleteHistory), if the peer confirms this
// we change to DeleteHistoryConfirmed, if they confirm they want to save then this becomes SaveHistoryConfirmed
// We use this distinction between default and confirmed to drive UI
DeleteHistoryDefault = "DefaultDeleteHistory"
SaveHistoryConfirmed = "SaveHistory"
DeleteHistoryConfirmed = "DeleteHistoryConfirmed"
)
// Bool strings
const (
True = "true"
False = "false"
)

View File

@ -1,8 +1,6 @@
package event
import (
"sync"
)
import "sync"
type queue struct {
infChan infiniteChannel
@ -21,7 +19,7 @@ type simpleQueue struct {
// the event.Manager.
type Queue interface {
Publish(event Event)
Next() Event
Next() *Event
Shutdown()
OutChan() <-chan Event
Len() int
@ -54,9 +52,9 @@ func (sq *simpleQueue) Len() int {
}
// Next returns the next available event from the front of the queue
func (sq *simpleQueue) Next() Event {
func (sq *simpleQueue) Next() *Event {
event := <-sq.eventChannel
return event
return &event
}
// Shutdown closes our eventChannel
@ -85,9 +83,9 @@ func (iq *queue) OutChan() <-chan Event {
}
// Out returns the next available event from the front of the queue
func (iq *queue) Next() Event {
func (iq *queue) Next() *Event {
event := <-iq.infChan.Out()
return event
return &event
}
func (iq *queue) Len() int {

View File

@ -1,41 +1,20 @@
package event
import (
"crypto/rand"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"math"
"math/big"
"os"
"runtime"
"strings"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"sync"
)
// Event is the core struct type passed around between various subsystems. Events consist of a type which can be
// filtered on, an event ID for tracing and a map of Fields to string values.
// Event is a structure which binds a given set of data to an Type
type Event struct {
EventType Type
EventID string
Data map[Field]string
}
// GetRandNumber is a helper function which returns a random integer, this is
// currently mostly used to generate messageids
func GetRandNumber() *big.Int {
num, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
// If we can't generate random numbers then panicking is probably
// the best option.
if err != nil {
panic(err.Error())
}
return num
}
// NewEvent creates a new event object with a unique ID and the given type and data.
func NewEvent(eventType Type, data map[Field]string) Event {
return Event{EventType: eventType, EventID: GetRandNumber().String(), Data: data}
return Event{EventType: eventType, EventID: utils.GetRandNumber().String(), Data: data}
}
// NewEventList creates a new event object with a unique ID and the given type and data supplied in a list format and composed into a map of Type:string
@ -48,17 +27,16 @@ func NewEventList(eventType Type, args ...interface{}) Event {
data[key] = val
}
}
return Event{EventType: eventType, EventID: GetRandNumber().String(), Data: data}
return Event{EventType: eventType, EventID: utils.GetRandNumber().String(), Data: data}
}
// Manager is an Event Bus which allows subsystems to subscribe to certain EventTypes and publish others.
type manager struct {
subscribers map[Type][]Queue
events chan []byte
events chan Event
mapMutex sync.Mutex
internal chan bool
closed bool
trace bool
}
// Manager is an interface for an event bus
@ -80,12 +58,9 @@ func NewEventManager() Manager {
// Initialize sets up the Manager.
func (em *manager) initialize() {
em.subscribers = make(map[Type][]Queue)
em.events = make(chan []byte)
em.events = make(chan Event)
em.internal = make(chan bool)
em.closed = false
_, em.trace = os.LookupEnv("CWTCH_EVENT_SOURCE")
go em.eventBus()
}
@ -99,27 +74,8 @@ func (em *manager) Subscribe(eventType Type, queue Queue) {
// Publish takes an Event and sends it to the internal eventBus where it is distributed to all Subscribers
func (em *manager) Publish(event Event) {
if event.EventType != "" && !em.closed {
// Debug Events for Tracing, locked behind an environment variable
// for now.
if em.trace {
pc, _, _, _ := runtime.Caller(1)
funcName := runtime.FuncForPC(pc).Name()
lastSlash := strings.LastIndexByte(funcName, '/')
if lastSlash < 0 {
lastSlash = 0
}
lastDot := strings.LastIndexByte(funcName[lastSlash:], '.') + lastSlash
event.Data[Source] = fmt.Sprintf("%v.%v", funcName[:lastDot], funcName[lastDot+1:])
}
// Deep Copy the Event...
eventJSON, err := json.Marshal(event)
if err != nil {
log.Errorf("Error serializing event: %v", event)
}
em.events <- eventJSON
if event.EventType != "" && em.closed != true {
em.events <- event
}
}
@ -131,21 +87,13 @@ func (em *manager) PublishLocal(event Event) {
// eventBus is an internal function that is used to distribute events to all subscribers
func (em *manager) eventBus() {
for {
eventJSON := <-em.events
event := <-em.events
// In the case on an empty event. Teardown the Queue
if len(eventJSON) == 0 {
log.Errorf("Received zero length event")
if event.EventType == "" {
break
}
var event Event
err := json.Unmarshal(eventJSON, &event)
if err != nil {
log.Errorf("Error on Deep Copy: %v %v", eventJSON, err)
}
// maps aren't thread safe
em.mapMutex.Lock()
subscribers := em.subscribers[event.EventType]
@ -153,10 +101,7 @@ func (em *manager) eventBus() {
// Send the event to any subscribers to that event type
for _, subscriber := range subscribers {
// Deep Copy for Each Subscriber
var eventCopy Event
json.Unmarshal(eventJSON, &eventCopy)
subscriber.Publish(eventCopy)
subscriber.Publish(event)
}
}
@ -166,7 +111,7 @@ func (em *manager) eventBus() {
// Shutdown triggers, and waits for, the internal eventBus goroutine to finish
func (em *manager) Shutdown() {
em.events <- []byte{}
em.events <- Event{}
em.closed = true
// wait for eventBus to finish
<-em.internal

View File

@ -1,7 +1,7 @@
package event
import (
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"sync"
"testing"
"time"

27
go.mod
View File

@ -1,17 +1,18 @@
module cwtch.im/cwtch
go 1.14
require (
git.openprivacy.ca/cwtch.im/tapir v0.4.3
git.openprivacy.ca/openprivacy/connectivity v1.4.4
git.openprivacy.ca/openprivacy/log v1.0.2
github.com/gtank/ristretto255 v0.1.2
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/struCoder/pidusage v0.1.3
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/tools v0.1.2 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
cwtch.im/tapir v0.1.14
git.openprivacy.ca/openprivacy/libricochet-go v1.0.9-0.20200122193341-ddac73106710
github.com/c-bata/go-prompt v0.2.3
github.com/golang/protobuf v1.3.2
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 // indirect
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect
github.com/struCoder/pidusage v0.1.2
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 // indirect
)
go 1.13

165
go.sum
View File

@ -1,110 +1,91 @@
git.openprivacy.ca/cwtch.im/tapir v0.3.1 h1:+d1dHyPvZ8JmdfFe/oXWJPardzflRIhcdILtkeArkW8=
git.openprivacy.ca/cwtch.im/tapir v0.3.1/go.mod h1:q6RMI/TQvRN8SCtRY3GryOawMcB0uG6NjP6M77oSMx8=
git.openprivacy.ca/cwtch.im/tapir v0.3.2 h1:thLWqqY1LkirWFcy9Tg6NgWeYbvo9xBm+s2XVnCIvpY=
git.openprivacy.ca/cwtch.im/tapir v0.3.2/go.mod h1:q6RMI/TQvRN8SCtRY3GryOawMcB0uG6NjP6M77oSMx8=
git.openprivacy.ca/cwtch.im/tapir v0.3.3 h1:Q7F8JijgOMMYSy3IdZl7+r6qkWckEWV1+EY7q6MAkVs=
git.openprivacy.ca/cwtch.im/tapir v0.3.3/go.mod h1:ZMg9Jzh0n3Os2aSF4z+bx/n8WBCJBN7KCQESXperYts=
git.openprivacy.ca/cwtch.im/tapir v0.3.4 h1:g7yZkfz/vWr/t2tFXa/t0Ebr/w665uIKpxpCZ3lIPCo=
git.openprivacy.ca/cwtch.im/tapir v0.3.4/go.mod h1:+Niy2AHhQC351ZTtfhC0uLjViCICyOxCJZsIlGKKNAU=
git.openprivacy.ca/cwtch.im/tapir v0.3.5 h1:AlqAhluY4ivznGoHh37Khyxy0u9IbtYskP93wgtmYx8=
git.openprivacy.ca/cwtch.im/tapir v0.3.5/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
git.openprivacy.ca/cwtch.im/tapir v0.4.0 h1:clG8uORt0NKEhT4P+Dpw1pzyUuYzYBMevGqn2pciKk8=
git.openprivacy.ca/cwtch.im/tapir v0.4.0/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
git.openprivacy.ca/cwtch.im/tapir v0.4.1 h1:9LMpQX41IzecNNlRc1FZKXHg6wlFss679tFsa3vzb3Y=
git.openprivacy.ca/cwtch.im/tapir v0.4.1/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
git.openprivacy.ca/cwtch.im/tapir v0.4.2 h1:bxMWZnVJXX4dqqOFS7ELW4iFkVL4GS8wiRkjRv5rJe8=
git.openprivacy.ca/cwtch.im/tapir v0.4.2/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
git.openprivacy.ca/cwtch.im/tapir v0.4.3 h1:sctSfUXHDIqaHfJPDl+5lHtmoEJolQiHTcHZGAe5Qc4=
git.openprivacy.ca/cwtch.im/tapir v0.4.3/go.mod h1:10qEaib5x021zgyZ/97JKWsEpedH5+Vfy2CvB2V+08E=
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/connectivity v1.4.0 h1:c7AANUCrlA4hIqXxIGDOWMtSe8CpDleD1877PShScbM=
git.openprivacy.ca/openprivacy/connectivity v1.4.0/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
git.openprivacy.ca/openprivacy/connectivity v1.4.1 h1:zoM+j7PFj8mQeUCNiDNMe7Uq9dhcJDOhaZcSANfeDL4=
git.openprivacy.ca/openprivacy/connectivity v1.4.1/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
git.openprivacy.ca/openprivacy/connectivity v1.4.2 h1:rQFIjWunLlRmXL5Efsv+7+1cA70T6Uza6RCy2PRm9zc=
git.openprivacy.ca/openprivacy/connectivity v1.4.2/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
git.openprivacy.ca/openprivacy/connectivity v1.4.3 h1:i2Ad/U9FlL9dKr2bhRck7lJ8NoWyGtoEfUwoCyMT0fU=
git.openprivacy.ca/openprivacy/connectivity v1.4.3/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
git.openprivacy.ca/openprivacy/connectivity v1.4.4 h1:11M3akVCyy/luuhMpZTM1r9Jayl7IHD944Bxsn2FDpU=
git.openprivacy.ca/openprivacy/connectivity v1.4.4/go.mod h1:JVRCIdL+lAG6ohBFWiKeC/MN42nnC0sfFszR9XG6vPQ=
git.openprivacy.ca/openprivacy/log v1.0.1/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
git.openprivacy.ca/openprivacy/log v1.0.2 h1:HLP4wsw4ljczFAelYnbObIs821z+jgMPCe8uODPnGQM=
git.openprivacy.ca/openprivacy/log v1.0.2/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
cwtch.im/tapir v0.1.14 h1:lg+reZNT998l++4Q4RQBLXYv3ukqWffhI0Wed9RSjuA=
cwtch.im/tapir v0.1.14/go.mod h1:QwERb982YIes9UOxDqIthm1HZ1xy0YQetD2+XxDbg9Y=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.4 h1:GWLMJ5jBSIC/gFXzdbbeVz7fIAn2FTgW8+wBci6/3Ek=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.4/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.8 h1:HVoyxfivFaEtkfO5K3piD6oq6MQB1qGF5IB2EYXeCW8=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.8/go.mod h1:6I+vO9Aagv3/yUWv+e7A57H8tgXgR67FCjfSj9Pp970=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.9-0.20200122042825-b8f1a255fc60 h1:L9IbWW/Mf0rhEC1M9pPBYUZvdU5VrDQCU0iYLK2maew=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.9-0.20200122042825-b8f1a255fc60/go.mod h1:e8njwG7xhHv1V0vBzZMZn143BVrGiYx9w9dXj5kVg+w=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.9-0.20200122193341-ddac73106710 h1:chhDO51AVBd1UUKW261ajHu7MiQdWWJ89bZnExCp8J0=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.9-0.20200122193341-ddac73106710/go.mod h1:JCw+R8dGMiqcDVvbqcZykniuRvad5tjbSLe4VaoJIUk=
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2s=
github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g=
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dballard/bine v0.0.0-20200122042525-8b3483ce130e h1:eEJdfVWrT8xpQbgexDQdyHtJBo82nEGK/pbgTkci93E=
github.com/dballard/bine v0.0.0-20200122042525-8b3483ce130e/go.mod h1:2BCjjHAFKmIMpg+dLpCXICQB3UC+eMIkBhPwfOQRZ0I=
github.com/dballard/bine v0.0.0-20200122193120-a40095f982a8 h1:VOWn1e0matU7u6szFQFzj49ppDvYfpNkxwEit2sCUdM=
github.com/dballard/bine v0.0.0-20200122193120-a40095f982a8/go.mod h1:2BCjjHAFKmIMpg+dLpCXICQB3UC+eMIkBhPwfOQRZ0I=
github.com/dvyukov/go-fuzz v0.0.0-20191022152526-8cb203812681/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 h1:smQbSzmT3EHl4EUwtFwFGmGIpiYgIiiPeVv1uguIQEE=
github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/struCoder/pidusage v0.1.3 h1:pZcSa6asBE38TJtW0Nui6GeCjLTpaT/jAnNP7dUTLSQ=
github.com/struCoder/pidusage v0.1.3/go.mod h1:pWBlW3YuSwRl6h7R5KbvA4N8oOqe9LjaKW5CwT1SPjI=
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/struCoder/pidusage v0.1.2 h1:fFPTThlcWFQyizv3xKs5Lyq1lpG5lZ36arEGNhWz2Vs=
github.com/struCoder/pidusage v0.1.2/go.mod h1:pWBlW3YuSwRl6h7R5KbvA4N8oOqe9LjaKW5CwT1SPjI=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b h1:Ib/yptP38nXZFMwqWSip+OKuMP9OkyDe3p+DssP8n9w=
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50=
golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108175616-46f5a7f28bf0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,60 +0,0 @@
package attr
import (
"strings"
)
/*
Scope model for peer attributes and requests
A local peer "Alice" has a PublicScope that is queryable by getVal requests.
By default, for now, other scopes are private, of which we here define SettingsScope
Alice's peer structs of remote peers such as "Bob" keep the queried
PublicScope values in the PeerScope, which can be overridden by the same named
values stored in the LocalScope.
*/
// scopes for attributes
const (
// on a peer, local and peer supplied data
LocalScope = "local"
PeerScope = "peer"
// on a local profile, public data and private settings
PublicScope = "public"
SettingsScope = "settings"
)
// Separator for scope and the rest of path
const Separator = "."
// GetPublicScope takes a path and attaches the pubic scope to it
func GetPublicScope(path string) string {
return PublicScope + Separator + path
}
// GetSettingsScope takes a path and attaches the settings scope to it
func GetSettingsScope(path string) string {
return SettingsScope + Separator + path
}
// GetLocalScope takes a path and attaches the local scope to it
func GetLocalScope(path string) string {
return LocalScope + Separator + path
}
// GetPeerScope takes a path and attaches the peer scope to it
func GetPeerScope(path string) string {
return PeerScope + Separator + path
}
// GetScopePath take a full path and returns the scope and the scope-less path
func GetScopePath(fullPath string) (string, string) {
parts := strings.SplitN(fullPath, Separator, 1)
if len(parts) != 2 {
return "", ""
}
return parts[0], parts[1]
}

View File

@ -1,15 +0,0 @@
package model
// Error models some common errors that need to be handled by applications that use Cwtch
type Error string
// Error is the error interface
func (e Error) Error() string {
return string(e)
}
// Error definitions
const (
InvalidEd25519PublicKey = Error("InvalidEd25519PublicKey")
InconsistentKeyBundleError = Error("InconsistentKeyBundleError")
)

View File

@ -1,59 +1,45 @@
package model
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha512"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/groups"
"encoding/base32"
"encoding/base64"
"encoding/hex"
"encoding/json"
"cwtch.im/cwtch/protocol"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"github.com/golang/protobuf/proto"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"io"
"strings"
"sync"
"time"
)
// CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date
const CurrentGroupVersion = 3
// GroupInvitePrefix identifies a particular string as being a serialized group invite.
const GroupInvitePrefix = "torv3"
// Group defines and encapsulates Cwtch's conception of group chat. Which are sessions
// tied to a server under a given group key. Each group has a set of Messages.
type Group struct {
// GroupID is now derived from the GroupKey and the GroupServer
GroupID string
SignedGroupID []byte
GroupKey [32]byte
GroupServer string
Timeline Timeline `json:"-"`
Accepted bool
Owner string
IsCompromised bool
InitialMessage []byte
Attributes map[string]string
lock sync.Mutex
LocalID string
State string `json:"-"`
UnacknowledgedMessages []Message
Version int
unacknowledgedMessages []Message
}
// NewGroup initializes a new group associated with a given CwtchServer
func NewGroup(server string) (*Group, error) {
group := new(Group)
group.Version = CurrentGroupVersion
group.LocalID = GenerateRandomID()
group.Accepted = true // we are starting a group, so we assume we want to connect to it...
if !tor.IsValidHostname(server) {
return nil, errors.New("server is not a valid v3 onion")
if utils.IsValidHostname(server) == false {
return nil, errors.New("Server is not a valid v3 onion")
}
group.GroupServer = server
@ -71,65 +57,64 @@ func NewGroup(server string) (*Group, error) {
return nil, err
}
copy(group.GroupKey[:], groupKey[:])
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
// and key.
group.GroupID = deriveGroupID(groupKey[:], server)
group.Owner = "self"
group.Attributes = make(map[string]string)
// By default we set the "name" of the group to a random string, we can override this later, but to simplify the
// codes around invite, we assume that this is always set.
group.Attributes[attr.GetLocalScope("name")] = group.GroupID
return group, nil
}
// CheckGroup returns true only if the ID of the group is cryptographically valid.
func (g *Group) CheckGroup() bool {
return g.GroupID == deriveGroupID(g.GroupKey[:], g.GroupServer)
// SignGroup adds a signature to the group.
func (g *Group) SignGroup(signature []byte) {
g.SignedGroupID = signature
copy(g.Timeline.SignedGroupID[:], g.SignedGroupID)
}
// deriveGroupID hashes together the key and the hostname to create a bound identifier that can later
// be referenced and checked by profiles when they receive invites and messages.
func deriveGroupID(groupKey []byte, serverHostname string) string {
data, _ := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
pubkey := data[0:ed25519.PublicKeySize]
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New))
}
// Compromised should be called if we detect a groupkey leak
// Compromised should be called if we detect a a groupkey leak.
func (g *Group) Compromised() {
g.IsCompromised = true
}
// Invite generates a invitation that can be sent to a cwtch peer
func (g *Group) Invite() (string, error) {
// GetInitialMessage returns the first message of the group, if one was sent with the invite.
func (g *Group) GetInitialMessage() []byte {
g.lock.Lock()
defer g.lock.Unlock()
return g.InitialMessage
}
gci := &groups.GroupInvite{
GroupID: g.GroupID,
GroupName: g.Attributes[attr.GetLocalScope("name")],
SharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
// Invite generates a invitation that can be sent to a cwtch peer
func (g *Group) Invite(initialMessage []byte) ([]byte, error) {
if g.SignedGroupID == nil {
return nil, errors.New("group isn't signed")
}
invite, err := json.Marshal(gci)
serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite))
return serializedInvite, err
g.InitialMessage = initialMessage[:]
gci := &protocol.GroupChatInvite{
GroupName: g.GroupID,
GroupSharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
SignedGroupId: g.SignedGroupID[:],
InitialMessage: initialMessage[:],
}
invite, err := proto.Marshal(gci)
return invite, err
}
// AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte) Message {
func (g *Group) AddSentMessage(message *protocol.DecryptedGroupMessage, sig []byte) Message {
g.lock.Lock()
defer g.lock.Unlock()
timelineMessage := Message{
Message: message.Text,
Timestamp: time.Unix(int64(message.Timestamp), 0),
Message: message.GetText(),
Timestamp: time.Unix(int64(message.GetTimestamp()), 0),
Received: time.Unix(0, 0),
Signature: sig,
PeerID: message.Onion,
PreviousMessageSig: message.PreviousMessageSig,
PeerID: message.GetOnion(),
PreviousMessageSig: message.GetPreviousMessageSig(),
ReceivedByServer: false,
}
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages, timelineMessage)
g.unacknowledgedMessages = append(g.unacknowledgedMessages, timelineMessage)
return timelineMessage
}
@ -140,10 +125,10 @@ func (g *Group) ErrorSentMessage(sig []byte, error string) bool {
var message *Message
// Delete the message from the unack'd buffer if it exists
for i, unAckedMessage := range g.UnacknowledgedMessages {
for i, unAckedMessage := range g.unacknowledgedMessages {
if compareSignatures(unAckedMessage.Signature, sig) {
message = &unAckedMessage
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...)
message.Error = error
g.Timeline.Insert(message)
@ -154,29 +139,28 @@ func (g *Group) ErrorSentMessage(sig []byte, error string) bool {
}
// AddMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*Message, bool) {
func (g *Group) AddMessage(message *protocol.DecryptedGroupMessage, sig []byte) (*Message, bool) {
g.lock.Lock()
defer g.lock.Unlock()
// Delete the message from the unack'd buffer if it exists
for i, unAckedMessage := range g.UnacknowledgedMessages {
for i, unAckedMessage := range g.unacknowledgedMessages {
if compareSignatures(unAckedMessage.Signature, sig) {
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...)
break
}
}
timelineMessage := &Message{
Message: message.Text,
Timestamp: time.Unix(int64(message.Timestamp), 0),
Message: message.GetText(),
Timestamp: time.Unix(int64(message.GetTimestamp()), 0),
Received: time.Now(),
Signature: sig,
PeerID: message.Onion,
PreviousMessageSig: message.PreviousMessageSig,
PeerID: message.GetOnion(),
PreviousMessageSig: message.GetPreviousMessageSig(),
ReceivedByServer: true,
Error: "",
Acknowledged: true,
}
seen := g.Timeline.Insert(timelineMessage)
@ -187,17 +171,17 @@ func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*
func (g *Group) GetTimeline() (timeline []Message) {
g.lock.Lock()
defer g.lock.Unlock()
return append(g.Timeline.GetMessages(), g.UnacknowledgedMessages...)
return append(g.Timeline.GetMessages(), g.unacknowledgedMessages...)
}
//EncryptMessage takes a message and encrypts the message under the group key.
func (g *Group) EncryptMessage(message *groups.DecryptedGroupMessage) ([]byte, error) {
func (g *Group) EncryptMessage(message *protocol.DecryptedGroupMessage) ([]byte, error) {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
return nil, err
}
wire, err := json.Marshal(message)
wire, err := proto.Marshal(message)
if err != nil {
return nil, err
}
@ -207,14 +191,14 @@ func (g *Group) EncryptMessage(message *groups.DecryptedGroupMessage) ([]byte, e
// DecryptMessage takes a ciphertext and returns true and the decrypted message if the
// cipher text can be successfully decrypted,else false.
func (g *Group) DecryptMessage(ciphertext []byte) (bool, *groups.DecryptedGroupMessage) {
func (g *Group) DecryptMessage(ciphertext []byte) (bool, *protocol.DecryptedGroupMessage) {
if len(ciphertext) > 24 {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &g.GroupKey)
if ok {
dm := &groups.DecryptedGroupMessage{}
err := json.Unmarshal(decrypted, dm)
dm := &protocol.DecryptedGroupMessage{}
err := proto.Unmarshal(decrypted, dm)
if err == nil {
return true, dm
}
@ -237,41 +221,3 @@ func (g *Group) GetAttribute(name string) (value string, exists bool) {
value, exists = g.Attributes[name]
return
}
// ValidateInvite takes in a serialized invite and returns the invite structure if it is cryptographically valid
// and an error if it is not
func ValidateInvite(invite string) (*groups.GroupInvite, error) {
// We prefix invites for groups with torv3
if strings.HasPrefix(invite, GroupInvitePrefix) {
data, err := base64.StdEncoding.DecodeString(invite[len(GroupInvitePrefix):])
if err == nil {
// First attempt to unmarshal the json...
var gci groups.GroupInvite
err := json.Unmarshal(data, &gci)
if err == nil {
// Validate the Invite by first checking that the server is a valid v3 onion
if !tor.IsValidHostname(gci.ServerHost) {
return nil, errors.New("server is not a valid v3 onion")
}
// Validate the length of the shared key...
if len(gci.SharedKey) != 32 {
return nil, errors.New("key length is not 32 bytes")
}
// Derive the servers public key (we can ignore the error checking here because it's already been
// done by IsValidHostname, and check that we derive the same groupID...
derivedGroupID := deriveGroupID(gci.SharedKey, gci.ServerHost)
if derivedGroupID != gci.GroupID {
return nil, errors.New("group id is invalid")
}
// Replace the original with the derived, this should be a no-op at this point but defense in depth...
gci.GroupID = derivedGroupID
return &gci, nil
}
}
}
return nil, errors.New("invite has invalid structure")
}

View File

@ -1,44 +1,25 @@
package model
import (
"crypto/sha256"
"cwtch.im/cwtch/protocol/groups"
"strings"
"sync"
"cwtch.im/cwtch/protocol"
"github.com/golang/protobuf/proto"
"testing"
"time"
)
func TestGroup(t *testing.T) {
g, _ := NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
dgm := &groups.DecryptedGroupMessage{
Onion: "onion",
Text: "Hello World!",
Timestamp: uint64(time.Now().Unix()),
SignedGroupID: []byte{},
dgm := &protocol.DecryptedGroupMessage{
Onion: proto.String("onion"),
Text: proto.String("Hello World!"),
Timestamp: proto.Int32(int32(time.Now().Unix())),
SignedGroupId: []byte{},
PreviousMessageSig: []byte{},
Padding: []byte{},
}
invite, err := g.Invite()
if err != nil {
t.Fatalf("error creating group invite: %v", err)
}
validatedInvite, err := ValidateInvite(invite)
if err != nil {
t.Fatalf("error validating group invite: %v", err)
}
if validatedInvite.GroupID != g.GroupID {
t.Fatalf("after validate group invite id should be identical to original: %v", err)
}
encMessage, _ := g.EncryptMessage(dgm)
ok, message := g.DecryptMessage(encMessage)
if !ok || message.Text != "Hello World!" {
if !ok || message.GetText() != "Hello World!" {
t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
return
}
@ -56,60 +37,3 @@ func TestGroupErr(t *testing.T) {
t.Errorf("Group Setup Should Have Failed")
}
}
// Test various group invite validation failures...
func TestGroupValidation(t *testing.T) {
group := &Group{
GroupID: "",
GroupKey: [32]byte{},
GroupServer: "",
Timeline: Timeline{},
Accepted: false,
IsCompromised: false,
Attributes: nil,
lock: sync.Mutex{},
LocalID: "",
State: "",
UnacknowledgedMessages: nil,
Version: 0,
}
invite, _ := group.Invite()
_, err := ValidateInvite(invite)
if err == nil {
t.Fatalf("Group with empty group id should have been an error")
}
t.Logf("Error: %v", err)
// Generate a valid group but replace the group server...
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
invite, _ = group.Invite()
_, err = ValidateInvite(invite)
if err == nil {
t.Fatalf("Group with empty group id should have been an error")
}
t.Logf("Error: %v", err)
// Generate a valid group but replace the group key...
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group.GroupKey = sha256.Sum256([]byte{})
invite, _ = group.Invite()
_, err = ValidateInvite(invite)
if err == nil {
t.Fatalf("Group with different group key should have errored")
}
t.Logf("Error: %v", err)
// mangle the invite
_, err = ValidateInvite(strings.ReplaceAll(invite, GroupInvitePrefix, ""))
if err == nil {
t.Fatalf("Group with different group key should have errored")
}
t.Logf("Error: %v", err)
}

View File

@ -1,103 +0,0 @@
package model
import (
"crypto/ed25519"
"encoding/base32"
"encoding/json"
"errors"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"strings"
)
// KeyType provides a wrapper for a generic public key type identifier (could be an onion address, a zcash address etc.)
type KeyType string
const (
// BundleType - the attribute under which the signed server bundle is stored...
BundleType = KeyType("server_key_bundle")
// KeyTypeServerOnion - a cwtch address
KeyTypeServerOnion = KeyType("bulletin_board_onion") // bulletin board
// KeyTypeTokenOnion - a cwtch peer with a PoW based token protocol
KeyTypeTokenOnion = KeyType("token_service_onion")
//KeyTypePrivacyPass - a privacy pass based token server
KeyTypePrivacyPass = KeyType("privacy_pass_public_key")
)
// Key provides a wrapper for a generic public key identifier (could be an onion address, a zcash address etc.)
type Key string
// KeyBundle manages a collection of related keys for various different services.
type KeyBundle struct {
Keys map[KeyType]Key
Signature []byte
}
// NewKeyBundle creates a new KeyBundle initialized with no keys.
func NewKeyBundle() *KeyBundle {
keyBundle := new(KeyBundle)
keyBundle.Keys = make(map[KeyType]Key)
return keyBundle
}
// HasKeyType returns true if the bundle has a public key of a given type.
func (kb *KeyBundle) HasKeyType(keytype KeyType) bool {
_, exists := kb.Keys[keytype]
return exists
}
// GetKey retrieves a key with a given type from the bundle
func (kb *KeyBundle) GetKey(keytype KeyType) (Key, error) {
key, exists := kb.Keys[keytype]
if exists {
return key, nil
}
return "", errors.New("no such key")
}
// Serialize produces a json encoded byte array.
func (kb KeyBundle) Serialize() []byte {
// json.Marshal sorts map keys
bundle, _ := json.Marshal(kb)
return bundle
}
// Sign allows a server to authenticate a key bundle by signing it (this uses the tapir identity interface)
func (kb *KeyBundle) Sign(identity primitives.Identity) {
kb.Signature = identity.Sign(kb.Serialize())
}
// DeserializeAndVerify takes in a json formatted bundle and only returns a valid key bundle
// if it has been signed by the server.
func DeserializeAndVerify(bundle []byte) (*KeyBundle, error) {
keyBundle := new(KeyBundle)
err := json.Unmarshal(bundle, &keyBundle)
if err == nil {
signature := keyBundle.Signature
keyBundle.Signature = nil
serverKey, _ := keyBundle.GetKey(KeyTypeServerOnion)
// We have to do convert the encoded key to a format that can be used to verify the signature
var decodedPub []byte
decodedPub, err = base32.StdEncoding.DecodeString(strings.ToUpper(string(serverKey)))
if err == nil && len(decodedPub) == 35 {
if ed25519.Verify(decodedPub[:32], keyBundle.Serialize(), signature) { // == true
return keyBundle, nil
}
}
err = InvalidEd25519PublicKey
}
return nil, err
}
// AttributeBundle returns a map that can be used as part of a peer attribute bundle
func (kb *KeyBundle) AttributeBundle() map[string]string {
ab := make(map[string]string)
for k, v := range kb.Keys {
ab[string(k)] = string(v)
}
return ab
}

View File

@ -1,64 +0,0 @@
package model
import (
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"testing"
)
func TestDeserializeAndVerify(t *testing.T) {
server, _ := primitives.InitializeEphemeralIdentity()
serverKeyBundle := NewKeyBundle()
serverKeyBundle.Keys[KeyTypeServerOnion] = Key(server.Hostname())
serverKeyBundle.Keys[KeyTypePrivacyPass] = Key("random 1")
serverKeyBundle.Keys[KeyTypeTokenOnion] = Key("random 2")
serverKeyBundle.Sign(server)
//eyeball keys are sorted
t.Logf("%s", serverKeyBundle.Serialize())
serialize := serverKeyBundle.Serialize()
newKeyBundle, err := DeserializeAndVerify(serialize)
if err != nil {
t.Fatalf("Key Bundle did not Deserialize %v", err)
}
if newKeyBundle.Keys[KeyTypeServerOnion] != Key(server.Hostname()) {
t.Fatalf("Key Bundle did not Serialize Correctly Actual: %v Expected: %v", newKeyBundle, serverKeyBundle)
}
}
func TestDeserializeAndVerifyMaliciousSignShouldFail(t *testing.T) {
server, _ := primitives.InitializeEphemeralIdentity()
maliciousServer, _ := primitives.InitializeEphemeralIdentity()
serverKeyBundle := NewKeyBundle()
serverKeyBundle.Keys[KeyTypeServerOnion] = Key(server.Hostname())
// This time we sign with a malicious server
serverKeyBundle.Sign(maliciousServer)
serialize := serverKeyBundle.Serialize()
newKeyBundle, err := DeserializeAndVerify(serialize)
if err == nil {
t.Fatalf("Key Bundle did Deserialize (it should have failed): %v", newKeyBundle)
}
}
func TestDeserializeAndVerifyUnsignedShouldFail(t *testing.T) {
server, _ := primitives.InitializeEphemeralIdentity()
serverKeyBundle := NewKeyBundle()
serverKeyBundle.Keys[KeyTypeServerOnion] = Key(server.Hostname())
// This time we don't sign
// serverKeyBundle.Sign(server)
serialize := serverKeyBundle.Serialize()
newKeyBundle, err := DeserializeAndVerify(serialize)
if err == nil {
t.Fatalf("Key Bundle did Deserialize (it should have failed): %v", newKeyBundle)
}
}

View File

@ -26,8 +26,6 @@ type Message struct {
ReceivedByServer bool // messages sent to a server
Acknowledged bool // peer to peer
Error string `json:",omitempty"`
// Application specific flags, useful for storing small amounts of metadata
Flags uint64
}
// MessageBaseSize is a rough estimate of the base number of bytes the struct uses before strings are populated
@ -37,11 +35,12 @@ func compareSignatures(a []byte, b []byte) bool {
if len(a) != len(b) {
return false
}
int d = 0
for i := range a {
d := d | (a[i] ^ b[i])
if a[i] != b[i] {
return false
}
}
return d == 0
return true
}
// GetMessages returns a copy of the entire timeline
@ -104,6 +103,7 @@ func (t *Timeline) Less(i, j int) bool {
}
// Sort sorts the timeline in a canonical order.
// TODO: There is almost definitely a more efficient way of doing things that involve not calling this method on every timeline load.
func (t *Timeline) Sort() {
t.lock.Lock()
defer t.lock.Unlock()

View File

@ -16,7 +16,7 @@ func TestMessagePadding(t *testing.T) {
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite)
sarah.ProcessInvite(string(invite), alice.Onion)
group := alice.GetGroup(gid)
@ -47,12 +47,9 @@ func TestTranscriptConsistency(t *testing.T) {
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
// The lightest weight server entry possible (usually we would import a key bundle...)
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite)
sarah.ProcessInvite(string(invite), alice.Onion)
group := alice.GetGroup(gid)

View File

@ -2,14 +2,13 @@ package model
import (
"crypto/rand"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/protocol"
"encoding/base32"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"github.com/golang/protobuf/proto"
"golang.org/x/crypto/ed25519"
"io"
"path/filepath"
@ -18,31 +17,19 @@ import (
"time"
)
// Authorization is a type determining client assigned authorization to a peer
type Authorization string
const (
// AuthUnknown is an initial state for a new unseen peer
AuthUnknown Authorization = "unknown"
// AuthApproved means the client has approved the peer, it can send messages to us, perform GetVals, etc
AuthApproved Authorization = "approved"
// AuthBlocked means the client has blocked the peer, it's messages and connections should be rejected
AuthBlocked Authorization = "blocked"
)
// PublicProfile is a local copy of a CwtchIdentity
type PublicProfile struct {
Name string
Ed25519PublicKey ed25519.PublicKey
Authorization Authorization
DeprecatedBlocked bool `json:"Blocked"`
Trusted bool
Blocked bool
Onion string
Attributes map[string]string
Timeline Timeline `json:"-"`
LocalID string // used by storage engine
State string `json:"-"`
lock sync.Mutex
UnacknowledgedMessages map[string]int
unacknowledgedMessages map[string]Message
}
// Profile encapsulates all the attributes necessary to be a Cwtch Peer.
@ -68,7 +55,7 @@ func (p *PublicProfile) init() {
if p.Attributes == nil {
p.Attributes = make(map[string]string)
}
p.UnacknowledgedMessages = make(map[string]int)
p.unacknowledgedMessages = make(map[string]Message)
p.LocalID = GenerateRandomID()
}
@ -79,12 +66,6 @@ func (p *PublicProfile) SetAttribute(name string, value string) {
p.Attributes[name] = value
}
// IsServer returns true if the profile is associated with a server.
func (p *PublicProfile) IsServer() (isServer bool) {
_, isServer = p.GetAttribute(string(KeyTypeServerOnion))
return
}
// GetAttribute returns the value of a value set with SetCustomAttribute. If no such value has been set exists is set to false.
func (p *PublicProfile) GetAttribute(name string) (value string, exists bool) {
p.lock.Lock()
@ -101,7 +82,7 @@ func GenerateNewProfile(name string) *Profile {
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
p.Ed25519PublicKey = pub
p.Ed25519PrivateKey = priv
p.Onion = tor.GetTorV3Hostname(pub)
p.Onion = utils.GetTorV3Hostname(pub)
p.Contacts = make(map[string]*PublicProfile)
p.Contacts[p.Onion] = &p.PublicProfile
@ -113,33 +94,13 @@ func GenerateNewProfile(name string) *Profile {
func (p *Profile) AddContact(onion string, profile *PublicProfile) {
p.lock.Lock()
profile.init()
// We expect callers to verify addresses before we get to this point, so if this isn't a
// valid address this is a noop.
if tor.IsValidHostname(onion) {
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
if err == nil {
profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32])
p.Contacts[onion] = profile
}
}
// TODO: More Robust V3 Onion Handling
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32])
p.Contacts[onion] = profile
p.lock.Unlock()
}
// UpdateMessageFlags updates the flags stored with a message
func (p *Profile) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
p.lock.Lock()
defer p.lock.Unlock()
if contact, exists := p.Contacts[handle]; exists {
if len(contact.Timeline.Messages) > mIdx {
contact.Timeline.Messages[mIdx].Flags = flags
}
} else if group, exists := p.Groups[handle]; exists {
if len(group.Timeline.Messages) > mIdx {
group.Timeline.Messages[mIdx].Flags = flags
}
}
}
// DeleteContact deletes a peer contact
func (p *Profile) DeleteContact(onion string) {
p.lock.Lock()
@ -172,11 +133,10 @@ func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt strin
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
message := &Message{PeerID: p.Onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: false}
if contact.UnacknowledgedMessages == nil {
contact.UnacknowledgedMessages = make(map[string]int)
if contact.unacknowledgedMessages == nil {
contact.unacknowledgedMessages = make(map[string]Message)
}
contact.Timeline.Insert(message)
contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1
contact.unacknowledgedMessages[eventID] = *message
return message
}
return nil
@ -199,47 +159,45 @@ func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, s
}
// ErrorSentMessageToPeer sets a sent message's error message and removes it from the unacknowledged list
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) int {
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
message, ok := contact.unacknowledgedMessages[eventID]
if ok {
contact.Timeline.Messages[mIdx].Error = error
delete(contact.UnacknowledgedMessages, eventID)
return mIdx
message.Error = error
contact.Timeline.Insert(&message) // TODO: do we want a non timeline.Insert way to handle errors
delete(contact.unacknowledgedMessages, eventID)
}
}
return -1
}
// AckSentMessageToPeer sets mesage to a peer as acknowledged
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) int {
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
message, ok := contact.unacknowledgedMessages[eventID]
if ok {
contact.Timeline.Messages[mIdx].Acknowledged = true
delete(contact.UnacknowledgedMessages, eventID)
return mIdx
message.Acknowledged = true
contact.Timeline.Insert(&message)
delete(contact.unacknowledgedMessages, eventID)
}
}
return -1
}
// AddGroupSentMessageError searches matching groups for the message by sig and marks it as an error
func (p *Profile) AddGroupSentMessageError(groupID string, signature []byte, error string) {
p.lock.Lock()
defer p.lock.Unlock()
group, exists := p.Groups[groupID]
if exists {
group.ErrorSentMessage(signature, error)
func (p *Profile) AddGroupSentMessageError(groupServer string, signature string, error string) {
for _, group := range p.Groups {
if group.GroupServer == groupServer {
if group.ErrorSentMessage([]byte(signature), error) {
break
}
}
}
}
@ -280,38 +238,64 @@ func (p *Profile) GetContacts() []string {
return keys
}
// SetContactAuthorization sets the authoirization level of a peer
func (p *Profile) SetContactAuthorization(onion string, auth Authorization) (err error) {
// BlockPeer blocks a contact
func (p *Profile) BlockPeer(onion string) (err error) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
contact.Authorization = auth
contact.Blocked = true
} else {
err = errors.New("peer does not exist")
}
return
}
// GetContactAuthorization returns the contact's authorization level
func (p *Profile) GetContactAuthorization(onion string) Authorization {
// UnblockPeer unblocks a contact
func (p *Profile) UnblockPeer(onion string) (err error) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
return contact.Authorization
contact.Blocked = false
} else {
err = errors.New("peer does not exist")
}
return AuthUnknown
return
}
// ContactsAuthorizations calculates a list of Peers who are at the supplied auth levels
func (p *Profile) ContactsAuthorizations(authorizationFilter ...Authorization) map[string]Authorization {
authorizations := map[string]Authorization{}
// BlockedPeers calculates a list of Peers who have been Blocked.
func (p *Profile) BlockedPeers() []string {
blockedPeers := []string{}
for _, contact := range p.GetContacts() {
c, _ := p.GetContact(contact)
authorizations[c.Onion] = c.Authorization
if c.Blocked {
blockedPeers = append(blockedPeers, c.Onion)
}
}
return authorizations
return blockedPeers
}
// TrustPeer sets a contact to trusted
func (p *Profile) TrustPeer(onion string) (err error) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
contact.Trusted = true
} else {
err = errors.New("peer does not exist")
}
return
}
// IsBlocked returns true if the contact has been blocked, false otherwise
func (p *Profile) IsBlocked(onion string) bool {
contact, ok := p.GetContact(onion)
if ok {
return contact.Blocked
}
return false
}
// GetContact returns a contact if the profile has it
@ -322,34 +306,20 @@ func (p *Profile) GetContact(onion string) (*PublicProfile, bool) {
return contact, ok
}
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, ciphertext and signature.
// The goal of this function is 2-fold:
// 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least
// knows the senders private key)
// 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't
// matter if we actually received this message from the server or from a hybrid protocol, all that matters is
// that the sender and receivers agree that this message was intended for the group
// The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at
// https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups
// on two different servers with the same key and then forwards messages between them to convince the parties in
// each group that they are actually in one big group (with the intent to later censor and/or selectively send messages
// to each group).
func (p *Profile) VerifyGroupMessage(onion string, groupID string, ciphertext []byte, signature []byte) bool {
// VerifyGroupMessage confirms the authenticity of a message given an onion, message and signature.
func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, timestamp int32, ciphertext []byte, signature []byte) bool {
group := p.GetGroup(groupID)
if group == nil {
return false
}
// We use our group id, a known reference server and the ciphertext of the message.
m := groupID + group.GroupServer + string(ciphertext)
// If the message is ostensibly from us then we check it against our public key...
if onion == p.Onion {
m := groupID + group.GroupServer + string(ciphertext)
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
}
// Otherwise we derive the public key from the sender and check it against that.
m := groupID + group.GroupServer + string(ciphertext)
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
if err == nil && len(decodedPub) >= 32 {
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
@ -365,13 +335,22 @@ func (p *Profile) SignMessage(message string) []byte {
// StartGroup when given a server, creates a new Group under this profile and returns the group id an a precomputed
// invite which can be sent on the wire.
func (p *Profile) StartGroup(server string) (groupID string, invite string, err error) {
func (p *Profile) StartGroup(server string) (groupID string, invite []byte, err error) {
return p.StartGroupWithMessage(server, []byte{})
}
// StartGroupWithMessage when given a server, and an initial message creates a new Group under this profile and returns the group id an a precomputed
// invite which can be sent on the wire.
func (p *Profile) StartGroupWithMessage(server string, initialMessage []byte) (groupID string, invite []byte, err error) {
group, err := NewGroup(server)
if err != nil {
return "", "", err
return "", nil, err
}
groupID = group.GroupID
invite, err = group.Invite()
group.Owner = p.Onion
signedGroupID := p.SignMessage(groupID + server)
group.SignGroup(signedGroupID)
invite, err = group.Invite(initialMessage)
p.lock.Lock()
defer p.lock.Unlock()
p.Groups[group.GroupID] = group
@ -386,35 +365,33 @@ func (p *Profile) GetGroup(groupID string) (g *Group) {
return
}
// ProcessInvite validates a group invite and adds a new group invite to the profile if it is valid.
// returns the new group ID on success, error on fail.
func (p *Profile) ProcessInvite(invite string) (string, error) {
gci, err := ValidateInvite(invite)
// ProcessInvite adds a new group invite to the profile. returns the new group ID
func (p *Profile) ProcessInvite(invite string, peerHostname string) (string, error) {
var gci protocol.GroupChatInvite
err := proto.Unmarshal([]byte(invite), &gci)
if err == nil {
if server, exists := p.GetContact(gci.ServerHost); !exists || !server.IsServer() {
return "", fmt.Errorf("unknown server. a server key bundle needs to be imported before this group can be verified")
}
group := new(Group)
group.Version = CurrentGroupVersion
group.GroupID = gci.GroupID
group.GroupID = gci.GetGroupName()
group.LocalID = GenerateRandomID()
copy(group.GroupKey[:], gci.SharedKey[:])
group.GroupServer = gci.ServerHost
group.SignedGroupID = gci.GetSignedGroupId()
copy(group.GroupKey[:], gci.GetGroupSharedKey()[:])
group.GroupServer = gci.GetServerHost()
group.InitialMessage = gci.GetInitialMessage()[:]
group.Accepted = false
group.Owner = peerHostname
group.Attributes = make(map[string]string)
group.Attributes[attr.GetLocalScope("name")] = gci.GroupName
p.AddGroup(group)
return gci.GroupID, nil
return group.GroupID, nil
}
return "", err
}
// AddGroup is a convenience method for adding a group to a profile.
func (p *Profile) AddGroup(group *Group) {
p.lock.Lock()
defer p.lock.Unlock()
_, exists := p.Groups[group.GroupID]
if !exists {
p.lock.Lock()
defer p.lock.Unlock()
p.Groups[group.GroupID] = group
}
}
@ -425,7 +402,7 @@ func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool,
for _, group := range p.Groups {
success, dgm := group.DecryptMessage(ciphertext)
if success {
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, ciphertext, signature)
verified := p.VerifyGroupMessage(dgm.GetOnion(), group.GroupID, dgm.GetText(), dgm.GetTimestamp(), ciphertext, signature)
// So we have a message that has a valid group key, but the signature can't be verified.
// The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious)
@ -445,11 +422,7 @@ func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool,
func getRandomness(arr *[]byte) {
if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil {
if err != nil {
// If we can't do randomness, just crash something is very very wrong and we are not going
// to resolve it here....
panic(err.Error())
}
utils.CheckError(err)
}
}
@ -465,27 +438,22 @@ func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte,
if group != nil {
timestamp := time.Now().Unix()
// Select the latest message from the timeline as a reference point.
var prevSig []byte
if len(group.Timeline.Messages) > 0 {
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
} else {
prevSig = []byte(group.GroupID)
prevSig = group.SignedGroupID
}
lenPadding := MaxGroupMessageLength - len(message)
padding := make([]byte, lenPadding)
getRandomness(&padding)
hexGroupID, err := hex.DecodeString(group.GroupID)
if err != nil {
return nil, nil, err
}
dm := &groups.DecryptedGroupMessage{
Onion: p.Onion,
Text: message,
SignedGroupID: hexGroupID,
Timestamp: uint64(timestamp),
dm := &protocol.DecryptedGroupMessage{
Onion: proto.String(p.Onion),
Text: proto.String(message),
SignedGroupId: group.SignedGroupID[:],
Timestamp: proto.Int32(int32(timestamp)),
PreviousMessageSig: prevSig,
Padding: padding[:],
}
@ -514,10 +482,6 @@ func (p *Profile) GetCopy(timeline bool) *Profile {
for groupID := range newp.Groups {
newp.Groups[groupID].Timeline = *p.Groups[groupID].Timeline.GetCopy()
}
for peerID := range newp.Contacts {
newp.Contacts[peerID].Timeline = *p.Contacts[peerID].Timeline.GetCopy()
}
}
return newp

View File

@ -31,9 +31,13 @@ func TestTrustPeer(t *testing.T) {
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
alice.SetContactAuthorization(sarah.Onion, AuthApproved)
if alice.GetContactAuthorization(sarah.Onion) != AuthApproved {
t.Errorf("peer should be approved")
alice.TrustPeer(sarah.Onion)
if alice.IsBlocked(sarah.Onion) {
t.Errorf("peer should not be blocked")
}
if alice.TrustPeer("") == nil {
t.Errorf("trusting a non existent peer should error")
}
}
@ -42,13 +46,13 @@ func TestBlockPeer(t *testing.T) {
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
alice.SetContactAuthorization(sarah.Onion, AuthBlocked)
if alice.GetContactAuthorization(sarah.Onion) != AuthBlocked {
t.Errorf("peer should be blocked")
alice.BlockPeer(sarah.Onion)
if !alice.IsBlocked(sarah.Onion) {
t.Errorf("peer should not be blocked")
}
if alice.SetContactAuthorization("", AuthUnknown) == nil {
t.Errorf("Seting Auth level of a non existent peer should error")
if alice.BlockPeer("") == nil {
t.Errorf("blocking a non existent peer should error")
}
}
@ -62,11 +66,9 @@ func TestRejectGroupInvite(t *testing.T) {
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
// The lightest weight server entry possible (usually we would import a key bundle...)
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite)
sarah.ProcessInvite(string(invite), alice.Onion)
group := alice.GetGroup(gid)
if len(sarah.Groups) == 1 {
if sarah.GetGroup(group.GroupID).Accepted {
@ -87,11 +89,8 @@ func TestProfileGroup(t *testing.T) {
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
// The lightest weight server entry possible (usually we would import a key bundle...)
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
sarah.ProcessInvite(invite)
gid, invite, _ := alice.StartGroupWithMessage("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", []byte("Hello World"))
sarah.ProcessInvite(string(invite), alice.Onion)
if len(sarah.GetGroups()) != 1 {
t.Errorf("sarah should only be in 1 group instead: %v", sarah.GetGroups())
}
@ -102,11 +101,17 @@ func TestProfileGroup(t *testing.T) {
alice.AttemptDecryption(c, s1)
gid2, invite2, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite2)
sarah.ProcessInvite(string(invite2), alice.Onion)
group2 := alice.GetGroup(gid2)
c2, s2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID)
alice.AttemptDecryption(c2, s2)
sarahGroup := sarah.GetGroup(group.GroupID)
im := sarahGroup.GetInitialMessage()
if string(im) != "Hello World" {
t.Errorf("Initial Message was not stored properly: %v", im)
}
_, _, err := sarah.EncryptMessageToGroup(string(make([]byte, MaxGroupMessageLength*2)), group2.GroupID)
if err == nil {
t.Errorf("Overly long message should have returned an error")
@ -114,10 +119,7 @@ func TestProfileGroup(t *testing.T) {
bob := GenerateNewProfile("bob")
bob.AddContact(alice.Onion, &alice.PublicProfile)
// The lightest weight server entry possible (usually we would import a key bundle...)
bob.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
bob.ProcessInvite(invite2)
bob.ProcessInvite(string(invite2), alice.Onion)
c3, s3, err := bob.EncryptMessageToGroup("Bobs Message", group2.GroupID)
if err == nil {
ok, _, message, _ := alice.AttemptDecryption(c3, s3)

View File

@ -3,192 +3,78 @@ package peer
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/connections"
"encoding/base32"
"encoding/base64"
"encoding/json"
"errors"
"git.openprivacy.ca/openprivacy/log"
"strconv"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"strings"
"sync"
"time"
)
const lastKnownSignature = "LastKnowSignature"
var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true,
event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeer: true,
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToPeerError: true, event.SendMessageToGroupError: true,
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true}
// DefaultEventsToHandle specifies which events will be subscribed to
// when a peer has its Init() function called
var DefaultEventsToHandle = []event.Type{
event.EncryptedGroupMessage,
event.NewMessageFromPeer,
event.PeerAcknowledgement,
event.NewGroupInvite,
event.PeerError,
event.SendMessageToGroupError,
event.NewGetValMessageFromPeer,
event.ProtocolEngineStopped,
event.RetryServerRequest,
}
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToGroupError: true}
// cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch cwtchPeer
type cwtchPeer struct {
Profile *model.Profile
mutex sync.Mutex
shutdown bool
listenStatus bool
Profile *model.Profile
mutex sync.Mutex
shutdown bool
queue event.Queue
eventBus event.Manager
}
func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
log.Debugf("Updating Flags for %v %v %v", handle, mIdx, flags)
cp.Profile.UpdateMessageFlags(handle, mIdx, flags)
cp.eventBus.Publish(event.NewEvent(event.UpdateMessageFlags, map[event.Field]string{event.Handle: handle, event.Index: strconv.Itoa(mIdx), event.Flags: strconv.FormatUint(flags, 2)}))
}
// BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname
// known to peer.
func (cp *cwtchPeer) BlockUnknownConnections() {
cp.eventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{}))
}
// AllowUnknownConnections will permit connections from unknown contacts.
func (cp *cwtchPeer) AllowUnknownConnections() {
cp.eventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{}))
}
// ReadContacts is a meta-interface intended to restrict callers to read-only access to contacts
type ReadContacts interface {
GetContacts() []string
GetContact(string) *model.PublicProfile
GetContactAttribute(string, string) (string, bool)
}
// ModifyContacts is a meta-interface intended to restrict callers to modify-only access to contacts
type ModifyContacts interface {
AddContact(nick, onion string, authorization model.Authorization)
SetContactAuthorization(string, model.Authorization) error
SetContactAttribute(string, string, string)
DeleteContact(string)
}
// AccessPeeringState provides access to functions relating to the underlying connections of a peer.
type AccessPeeringState interface {
GetPeerState(string) (connections.ConnectionState, bool)
}
// ModifyPeeringState is a meta-interface intended to restrict callers to modify-only access to connection peers
type ModifyPeeringState interface {
BlockUnknownConnections()
AllowUnknownConnections()
PeerWithOnion(string)
JoinServer(string) error
}
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
// and peers.
type ModifyContactsAndPeers interface {
ReadContacts
ModifyContacts
ModifyPeeringState
}
// ReadServers provides access to the servers
type ReadServers interface {
GetServers() []string
}
// ReadGroups provides read-only access to group state
type ReadGroups interface {
GetGroup(string) *model.Group
GetGroupState(string) (connections.ConnectionState, bool)
GetGroups() []string
GetGroupAttribute(string, string) (string, bool)
ExportGroup(string) (string, error)
}
// ModifyGroups provides write-only access add/edit/remove new groups
type ModifyGroups interface {
ImportGroup(string) (string, error)
StartGroup(string) (string, string, error)
AcceptInvite(string) error
RejectInvite(string)
DeleteGroup(string)
SetGroupAttribute(string, string, string)
}
// ModifyServers provides write-only access to servers
type ModifyServers interface {
AddServer(string) error
ResyncServer(onion string) error
}
// SendMessages enables a caller to sender messages to a contact
// Note:
type SendMessages interface {
SendGetValToPeer(string, string, string)
SendMessageToPeer(string, string) string
// TODO This should probably not be exposed
StoreMessage(onion string, messageTxt string, sent time.Time)
// TODO Extract once groups are stable
InviteOnionToGroup(string, string) error
}
// SendMessagesToGroup enables a caller to sender messages to a group
type SendMessagesToGroup interface {
SendMessageToGroupTracked(string, string) (string, error)
}
// ModifyMessages enables a caller to modify the messages in a timline
type ModifyMessages interface {
UpdateMessageFlags(string, int, uint64)
}
// CwtchPeer provides us with a way of testing systems built on top of cwtch without having to
// directly implement a cwtchPeer.
type CwtchPeer interface {
// Core Cwtch Peer Functions that should not be exposed to
// most functions
Init(event.Manager)
AutoHandleEvents(events []event.Type)
Listen()
StartPeersConnections()
StartServerConnections()
Shutdown()
PeerWithOnion(string)
InviteOnionToGroup(string, string) error
SendMessageToPeer(string, string) string
TrustPeer(string) error
BlockPeer(string) error
UnblockPeer(string) error
AcceptInvite(string) error
RejectInvite(string)
DeleteContact(string)
DeleteGroup(string)
JoinServer(string)
SendMessageToGroup(string, string) error
SendMessageToGroupTracked(string, string) (string, error)
GetProfile() *model.Profile
GetPeerState(string) connections.ConnectionState
StartGroup(string) (string, []byte, error)
ImportGroup(string) error
ExportGroup(string) (string, error)
GetGroup(string) *model.Group
GetGroupState(string) connections.ConnectionState
GetGroups() []string
AddContact(nick, onion string, trusted bool)
GetContacts() []string
GetContact(string) *model.PublicProfile
// Relating to local attributes
GetOnion() string
SetAttribute(string, string)
GetAttribute(string) (string, bool)
SetContactAttribute(string, string, string)
GetContactAttribute(string, string) (string, bool)
SetGroupAttribute(string, string, string)
GetGroupAttribute(string, string) (string, bool)
ReadContacts
ModifyContacts
AccessPeeringState
ModifyPeeringState
ReadGroups
ModifyGroups
ReadServers
ModifyServers
SendMessages
ModifyMessages
SendMessagesToGroup
Listen()
StartPeersConnections()
StartGroupConnections()
Shutdown()
}
// NewCwtchPeer creates and returns a new cwtchPeer with the given name.
@ -209,15 +95,11 @@ func FromProfile(profile *model.Profile) CwtchPeer {
// Init instantiates a cwtchPeer
func (cp *cwtchPeer) Init(eventBus event.Manager) {
cp.InitForEvents(eventBus, DefaultEventsToHandle)
}
func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) {
cp.queue = event.NewQueue()
go cp.eventHandler()
cp.eventBus = eventBus
cp.AutoHandleEvents(toBeHandled)
cp.AutoHandleEvents([]event.Type{event.EncryptedGroupMessage, event.NewMessageFromPeer, event.PeerAcknowledgement, event.PeerError, event.SendMessageToGroupError})
}
// AutoHandleEvents sets an event (if able) to be handled by this peer
@ -231,331 +113,199 @@ func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) {
}
}
// ImportGroup initializes a group from an imported source rather than a peer invite
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
gid, err := cp.Profile.ProcessInvite(exportedInvite)
if err == nil {
cp.eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.GroupID: gid, event.GroupInvite: exportedInvite}))
// ImportGroup intializes a group from an imported source rather than a peer invite
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (err error) {
if strings.HasPrefix(exportedInvite, "torv3") {
data, err := base64.StdEncoding.DecodeString(exportedInvite[5:])
if err == nil {
cp.eventBus.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{
event.GroupInvite: string(data),
}))
} else {
log.Errorf("error decoding group invite: %v", err)
}
return nil
}
return gid, err
return errors.New("unsupported exported group type")
}
// ExportGroup serializes a group invite so it can be given offline
func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
group := cp.Profile.GetGroup(groupID)
if group != nil {
return group.Invite()
invite, err := group.Invite(group.GetInitialMessage())
if err == nil {
exportedInvite := "torv3" + base64.StdEncoding.EncodeToString(invite)
return exportedInvite, err
}
}
return "", errors.New("group id could not be found")
}
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
func (cp *cwtchPeer) StartGroup(server string) (string, string, error) {
cp.mutex.Lock()
groupID, invite, err := cp.Profile.StartGroup(server)
cp.mutex.Unlock()
func (cp *cwtchPeer) StartGroup(server string) (string, []byte, error) {
return cp.StartGroupWithMessage(server, []byte{})
}
// StartGroupWithMessage create a new group linked to the given server and returns the group ID, an invite or an error.
func (cp *cwtchPeer) StartGroupWithMessage(server string, initialMessage []byte) (groupID string, invite []byte, err error) {
groupID, invite, err = cp.Profile.StartGroupWithMessage(server, initialMessage)
if err == nil {
group := cp.GetGroup(groupID)
jsobj, err := json.Marshal(group)
if err == nil {
cp.eventBus.Publish(event.NewEvent(event.GroupCreated, map[event.Field]string{
event.GroupID: groupID,
event.GroupServer: group.GroupServer,
event.GroupInvite: invite,
// Needed for Storage Engine...
event.Data: string(jsobj),
}))
}
} else {
log.Errorf("error creating group: %v", err)
}
return groupID, invite, err
return
}
// GetGroups returns an unordered list of all group IDs.
func (cp *cwtchPeer) GetGroups() []string {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.Profile.GetGroups()
}
// GetGroup returns a pointer to a specific group, nil if no group exists.
func (cp *cwtchPeer) GetGroup(groupID string) *model.Group {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.Profile.GetGroup(groupID)
}
func (cp *cwtchPeer) AddContact(nick, onion string, authorization model.Authorization) {
func (cp *cwtchPeer) AddContact(nick, onion string, trusted bool) {
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
pp := &model.PublicProfile{Name: nick, Ed25519PublicKey: decodedPub, Authorization: authorization, Onion: onion, Attributes: map[string]string{"nick": nick}}
pp := &model.PublicProfile{Name: nick, Ed25519PublicKey: decodedPub, Trusted: trusted, Blocked: false, Onion: onion, Attributes: map[string]string{"nick": nick}}
cp.Profile.AddContact(onion, pp)
pd, _ := json.Marshal(pp)
cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
event.Data: string(pd),
event.RemotePeer: onion,
}))
cp.eventBus.Publish(event.NewEventList(event.SetPeerAuthorization, event.RemotePeer, onion, event.Authorization, string(authorization)))
// Default to Deleting Peer History
cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, event.SaveHistoryKey, event.DeleteHistoryDefault))
}
// AddServer takes in a serialized server specification (a bundle of related keys) and adds a contact for the
// server assuming there are no errors and the contact doesn't already exist.
// TODO in the future this function should also integrate with a trust provider to validate the key bundle.
func (cp *cwtchPeer) AddServer(serverSpecification string) error {
// This confirms that the server did at least sign the bundle
keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification))
if err != nil {
return err
}
log.Debugf("Got new key bundle %v", keyBundle.Serialize())
// TODO if the key bundle is incomplete then error out. In the future we may allow servers to attest to new
// keys or subsets of keys, but for now they must commit only to a complete set of keys required for Cwtch Groups
// (that way we can be assured that the keybundle we store is a valid one)
if !keyBundle.HasKeyType(model.KeyTypeTokenOnion) || !keyBundle.HasKeyType(model.KeyTypeServerOnion) || !keyBundle.HasKeyType(model.KeyTypePrivacyPass) {
return errors.New("keybundle is incomplete")
}
if keyBundle.HasKeyType(model.KeyTypeServerOnion) {
onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion)
onion := string(onionKey)
// Add the contact if we don't already have it
if cp.GetContact(onion) == nil {
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
ab := keyBundle.AttributeBundle()
pp := &model.PublicProfile{Name: onion, Ed25519PublicKey: decodedPub, Authorization: model.AuthUnknown, Onion: onion, Attributes: ab}
// The only part of this function that actually modifies the profile...
cp.mutex.Lock()
cp.Profile.AddContact(onion, pp)
cp.mutex.Unlock()
pd, _ := json.Marshal(pp)
// Sync the Storage Engine
cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
event.Data: string(pd),
event.RemotePeer: onion,
}))
}
// At this point we know the server exists
server := cp.GetContact(onion)
ab := keyBundle.AttributeBundle()
// Check server bundle for consistency if we have different keys stored than in the tofu bundle then we
// abort...
for k, v := range ab {
val, exists := server.GetAttribute(k)
if exists {
if val != v {
// this is inconsistent!
return model.InconsistentKeyBundleError
}
}
// we haven't seen this key associated with the server before
}
// Store the key bundle for the server so we can reconstruct a tofubundle invite
cp.SetContactAttribute(onion, string(model.BundleType), serverSpecification)
// If we have gotten to this point we can assume this is a safe key bundle signed by the
// server with no conflicting keys. So we are going to publish all the keys
for k, v := range ab {
log.Debugf("Server (%v) has %v key %v", onion, k, v)
cp.SetContactAttribute(onion, k, v)
}
return nil
}
return err
}
// GetContacts returns an unordered list of onions
func (cp *cwtchPeer) GetContacts() []string {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.Profile.GetContacts()
}
// GetServers returns an unordered list of servers
func (cp *cwtchPeer) GetServers() []string {
contacts := cp.Profile.GetContacts()
var servers []string
for _, contact := range contacts {
if cp.GetContact(contact).IsServer() {
servers = append(servers, contact)
}
}
return servers
}
// GetContact returns a given contact, nil is no such contact exists
func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
cp.mutex.Lock()
defer cp.mutex.Unlock()
contact, _ := cp.Profile.GetContact(onion)
return contact
}
func (cp *cwtchPeer) GetOnion() string {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.Profile.Onion
}
func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if peer, ok := cp.Profile.Contacts[onion]; ok {
return connections.ConnectionStateToType()[peer.State], true
}
return connections.DISCONNECTED, false
// GetProfile returns the profile associated with this cwtchPeer.
func (cp *cwtchPeer) GetProfile() *model.Profile {
return cp.Profile
}
func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if group, ok := cp.Profile.Groups[groupid]; ok {
return connections.ConnectionStateToType()[group.State], true
}
return connections.DISCONNECTED, false
func (cp *cwtchPeer) GetPeerState(onion string) connections.ConnectionState {
return connections.ConnectionStateToType[cp.Profile.Contacts[onion].State]
}
func (cp *cwtchPeer) GetGroupState(groupid string) connections.ConnectionState {
return connections.ConnectionStateToType[cp.Profile.Groups[groupid].State]
}
// PeerWithOnion is the entry point for cwtchPeer relationships
func (cp *cwtchPeer) PeerWithOnion(onion string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if _, exists := cp.Profile.GetContact(onion); !exists {
cp.AddContact(onion, onion, model.AuthApproved)
cp.AddContact(onion, onion, false)
}
cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion}))
}
// DeleteContact deletes a peer from the profile, storage, and handling
func (cp *cwtchPeer) DeleteContact(onion string) {
cp.mutex.Lock()
cp.Profile.DeleteContact(onion)
defer cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEventList(event.DeleteContact, event.RemotePeer, onion))
}
// DeleteGroup deletes a Group from the profile, storage, and handling
func (cp *cwtchPeer) DeleteGroup(groupID string) {
cp.mutex.Lock()
cp.Profile.DeleteGroup(groupID)
defer cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEventList(event.DeleteGroup, event.GroupID, groupID))
}
// InviteOnionToGroup kicks off the invite process
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
cp.mutex.Lock()
group := cp.Profile.GetGroup(groupid)
if group == nil {
cp.mutex.Unlock()
return errors.New("invalid group id")
}
invite, err := group.Invite()
cp.mutex.Unlock()
invite, err := group.Invite(group.InitialMessage)
if err == nil {
cp.SendMessageToPeer(onion, invite)
cp.eventBus.Publish(event.NewEvent(event.InvitePeerToGroup, map[event.Field]string{event.RemotePeer: onion, event.GroupInvite: string(invite)}))
}
return err
}
// JoinServer manages a new server connection with the given onion address
func (cp *cwtchPeer) JoinServer(onion string) error {
if cp.GetContact(onion) != nil {
tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
if yExists && onionExists {
signature, exists := cp.GetContactAttribute(onion, lastKnownSignature)
if !exists {
signature = base64.StdEncoding.EncodeToString([]byte{})
}
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
return nil
}
}
return errors.New("no keys found for server connection")
func (cp *cwtchPeer) JoinServer(onion string) {
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion}))
}
// ResyncServer completely tears down and resyncs a new server connection with the given onion address
func (cp *cwtchPeer) ResyncServer(onion string) error {
if cp.GetContact(onion) != nil {
tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
if yExists && onionExists {
signature := base64.StdEncoding.EncodeToString([]byte{})
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
return nil
}
}
return errors.New("no keys found for server connection")
// SendMessageToGroup attempts to sent the given message to the given group id.
// TODO: Deprecate in favour of SendMessageToGroupTracked
func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error {
_, err := cp.SendMessageToGroupTracked(groupid, message)
return err
}
// SendMessageToGroupTracked attempts to sent the given message to the given group id.
// SendMessageToGroup attempts to sent the given message to the given group id.
// It returns the signature of the message which can be used to identify it in any UX layer.
func (cp *cwtchPeer) SendMessageToGroupTracked(groupid string, message string) (string, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
group := cp.Profile.GetGroup(groupid)
if group == nil {
return "", errors.New("invalid group id")
}
ct, sig, err := cp.Profile.EncryptMessageToGroup(message, groupid)
if err == nil {
cp.eventBus.Publish(event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupID: groupid, event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)}))
cp.eventBus.Publish(event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupServer: group.GroupServer, event.Ciphertext: string(ct), event.Signature: string(sig)}))
}
return base64.StdEncoding.EncodeToString(sig), err
return string(sig), err
}
func (cp *cwtchPeer) SendMessageToPeer(onion string, message string) string {
event := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Data: message})
cp.mutex.Lock()
contact, _ := cp.Profile.GetContact(onion)
event.EventID = strconv.Itoa(contact.Timeline.Len())
cp.Profile.AddSentMessageToContactTimeline(onion, message, time.Now(), event.EventID)
cp.mutex.Unlock()
cp.eventBus.Publish(event)
cp.Profile.AddSentMessageToContactTimeline(onion, message, time.Now(), event.EventID)
return event.EventID
}
func (cp *cwtchPeer) SendGetValToPeer(onion string, scope string, path string) {
event := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, onion, event.Scope, scope, event.Path, path)
cp.eventBus.Publish(event)
// TrustPeer sets an existing peer relationship to trusted
func (cp *cwtchPeer) TrustPeer(peer string) error {
err := cp.Profile.TrustPeer(peer)
if err == nil {
cp.PeerWithOnion(peer)
}
return err
}
// BlockPeer blocks an existing peer relationship.
func (cp *cwtchPeer) SetContactAuthorization(peer string, authorization model.Authorization) error {
cp.mutex.Lock()
err := cp.Profile.SetContactAuthorization(peer, authorization)
cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.SetPeerAuthorization, map[event.Field]string{event.RemotePeer: peer, event.Authorization: string(authorization)}))
func (cp *cwtchPeer) BlockPeer(peer string) error {
err := cp.Profile.BlockPeer(peer)
cp.eventBus.Publish(event.NewEvent(event.BlockPeer, map[event.Field]string{event.RemotePeer: peer}))
return err
}
// UnblockPeer blocks an existing peer relationship.
func (cp *cwtchPeer) UnblockPeer(peer string) error {
err := cp.Profile.UnblockPeer(peer)
cp.eventBus.Publish(event.NewEvent(event.UnblockPeer, map[event.Field]string{event.RemotePeer: peer}))
return err
}
// AcceptInvite accepts a given existing group invite
func (cp *cwtchPeer) AcceptInvite(groupID string) error {
cp.mutex.Lock()
err := cp.Profile.AcceptInvite(groupID)
cp.mutex.Unlock()
if err != nil {
return err
}
@ -567,48 +317,39 @@ func (cp *cwtchPeer) AcceptInvite(groupID string) error {
// RejectInvite rejects a given group invite.
func (cp *cwtchPeer) RejectInvite(groupID string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
cp.Profile.RejectInvite(groupID)
cp.eventBus.Publish(event.NewEvent(event.RejectGroupInvite, map[event.Field]string{event.GroupID: groupID}))
}
// Listen makes the peer open a listening port to accept incoming connections (and be detactably online)
func (cp *cwtchPeer) Listen() {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if !cp.listenStatus {
log.Infof("cwtchPeer Listen sending ProtocolEngineStartListen\n")
cp.listenStatus = true
cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: cp.Profile.Onion}))
}
// else protocol engine is already listening
log.Debugf("cwtchPeer Listen sending ProtocolEngineStartListen\n")
cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: cp.Profile.Onion}))
}
// StartPeersConnections attempts to connect to peer connections
// StartGroupConnections attempts to connect to all group servers (thus initiating reconnect attempts in the conectionsmanager)
func (cp *cwtchPeer) StartPeersConnections() {
for _, contact := range cp.GetContacts() {
if !cp.GetContact(contact).IsServer() {
cp.PeerWithOnion(contact)
}
cp.PeerWithOnion(contact)
}
}
// StartServerConnections attempts to connect to all server connections
func (cp *cwtchPeer) StartServerConnections() {
for _, contact := range cp.GetContacts() {
if cp.GetContact(contact).IsServer() {
cp.JoinServer(contact)
// StartPeerConnections attempts to connect to all peers (thus initiating reconnect attempts in the conectionsmanager)
func (cp *cwtchPeer) StartGroupConnections() {
joinedServers := map[string]bool{}
for _, groupID := range cp.GetGroups() {
// Only send a join server packet if we haven't joined this server yet...
group := cp.GetGroup(groupID)
if joined := joinedServers[groupID]; group.Accepted && !joined {
log.Infof("Join Server %v (%v)\n", group.GroupServer, joined)
cp.JoinServer(group.GroupServer)
joinedServers[group.GroupServer] = true
}
}
}
// SetAttribute sets an attribute for this profile and emits an event
func (cp *cwtchPeer) SetAttribute(key string, val string) {
cp.mutex.Lock()
cp.Profile.SetAttribute(key, val)
defer cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{
event.Key: key,
event.Data: val,
@ -617,23 +358,11 @@ func (cp *cwtchPeer) SetAttribute(key string, val string) {
// GetAttribute gets an attribute for the profile
func (cp *cwtchPeer) GetAttribute(key string) (string, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if val, exists := cp.Profile.GetAttribute(key); exists {
return val, true
}
if key == attr.GetLocalScope("name") {
return cp.Profile.Name, true
}
return "", false
return cp.Profile.GetAttribute(key)
}
// SetContactAttribute sets an attribute for the indicated contact and emits an event
func (cp *cwtchPeer) SetContactAttribute(onion string, key string, val string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if contact, ok := cp.Profile.GetContact(onion); ok {
contact.SetAttribute(key, val)
cp.eventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{
@ -646,20 +375,14 @@ func (cp *cwtchPeer) SetContactAttribute(onion string, key string, val string) {
// GetContactAttribute gets an attribute for the indicated contact
func (cp *cwtchPeer) GetContactAttribute(onion string, key string) (string, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if contact, ok := cp.Profile.GetContact(onion); ok {
if val, exists := contact.GetAttribute(key); exists {
return val, true
}
return contact.GetAttribute(key)
}
return "", false
}
// SetGroupAttribute sets an attribute for the indicated group and emits an event
func (cp *cwtchPeer) SetGroupAttribute(gid string, key string, val string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if group := cp.Profile.GetGroup(gid); group != nil {
group.SetAttribute(key, val)
cp.eventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{
@ -672,155 +395,59 @@ func (cp *cwtchPeer) SetGroupAttribute(gid string, key string, val string) {
// GetGroupAttribute gets an attribute for the indicated group
func (cp *cwtchPeer) GetGroupAttribute(gid string, key string) (string, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if group := cp.Profile.GetGroup(gid); group != nil {
if val, exists := group.GetAttribute(key); exists {
return val, true
}
return group.GetAttribute(key)
}
return "", false
}
// Shutdown kills all connections and cleans up all goroutines for the peer
func (cp *cwtchPeer) Shutdown() {
cp.mutex.Lock()
defer cp.mutex.Unlock()
cp.shutdown = true
cp.queue.Shutdown()
}
func (cp *cwtchPeer) StoreMessage(onion string, messageTxt string, sent time.Time) {
if cp.GetContact(onion) == nil {
cp.AddContact(onion, onion, model.AuthUnknown)
}
cp.mutex.Lock()
cp.Profile.AddMessageToContactTimeline(onion, messageTxt, sent)
cp.mutex.Unlock()
}
// eventHandler process events from other subsystems
func (cp *cwtchPeer) eventHandler() {
for {
ev := cp.queue.Next()
switch ev.EventType {
/***** Default auto handled events *****/
case event.ProtocolEngineStopped:
cp.mutex.Lock()
cp.listenStatus = false
log.Infof("Protocol engine for %v has stopped listening", cp.Profile.Onion)
cp.mutex.Unlock()
case event.EncryptedGroupMessage:
// If successful, a side effect is the message is added to the group's timeline
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
// SECURITY NOTE: A malicious server could insert posts such that everyone always has a different lastKnownSignature
// However the server can always replace **all** messages in an attempt to track users
// This is mitigated somewhat by resync events which do wipe things entire.
// The security of cwtch groups are also not dependent on the servers inability to uniquely tag connections (as long as
// it learns nothing else about each connection).
cp.SetContactAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature])
cp.mutex.Lock()
ok, groupID, message, seen := cp.Profile.AttemptDecryption(ciphertext, signature)
cp.mutex.Unlock()
ok, groupID, message, seen := cp.Profile.AttemptDecryption([]byte(ev.Data[event.Ciphertext]), []byte(ev.Data[event.Signature]))
if ok && !seen {
cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.TimestampReceived: message.Received.Format(time.RFC3339Nano), event.TimestampSent: message.Timestamp.Format(time.RFC3339Nano), event.Data: message.Message, event.GroupID: groupID, event.Signature: base64.StdEncoding.EncodeToString(message.Signature), event.PreviousSignature: base64.StdEncoding.EncodeToString(message.PreviousMessageSig), event.RemotePeer: message.PeerID}))
cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.TimestampReceived: message.Received.Format(time.RFC3339Nano), event.TimestampSent: message.Timestamp.Format(time.RFC3339Nano), event.Data: message.Message, event.GroupID: groupID, event.Signature: string(message.Signature), event.PreviousSignature: string(message.PreviousMessageSig), event.RemotePeer: message.PeerID}))
}
// The group has been compromised
if !ok && groupID != "" {
if cp.Profile.GetGroup(groupID).IsCompromised {
cp.eventBus.Publish(event.NewEvent(event.GroupCompromised, map[event.Field]string{event.GroupID: groupID}))
}
}
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
cp.StoreMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
cp.Profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
case event.PeerAcknowledgement:
cp.mutex.Lock()
idx := cp.Profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
edata := ev.Data
edata[event.Index] = strconv.Itoa(idx)
cp.eventBus.Publish(event.NewEvent(event.IndexedAcknowledgement, edata))
cp.mutex.Unlock()
cp.Profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
case event.SendMessageToGroupError:
cp.mutex.Lock()
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
cp.Profile.AddGroupSentMessageError(ev.Data[event.GroupID], signature, ev.Data[event.Error])
cp.mutex.Unlock()
cp.Profile.AddGroupSentMessageError(ev.Data[event.GroupServer], ev.Data[event.Signature], ev.Data[event.Error])
case event.SendMessageToPeerError:
cp.mutex.Lock()
idx := cp.Profile.ErrorSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID], ev.Data[event.Error])
edata := ev.Data
edata[event.Index] = strconv.Itoa(idx)
cp.eventBus.Publish(event.NewEvent(event.IndexedFailure, edata))
cp.mutex.Unlock()
case event.RetryServerRequest:
// Automated Join Server Request triggered by a plugin.
log.Debugf("profile received an automated retry event for %v", ev.Data[event.GroupServer])
cp.JoinServer(ev.Data[event.GroupServer])
case event.NewGetValMessageFromPeer:
onion := ev.Data[event.RemotePeer]
scope := ev.Data[event.Scope]
path := ev.Data[event.Path]
log.Debugf("NewGetValMessageFromPeer for %v%v from %v\n", scope, path, onion)
remotePeer := cp.GetContact(onion)
if remotePeer != nil && remotePeer.Authorization == model.AuthApproved {
if scope == attr.PublicScope {
val, exists := cp.GetAttribute(attr.GetPublicScope(path))
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)})
resp.EventID = ev.EventID
if exists {
resp.Data[event.Data] = val
} else {
resp.Data[event.Data] = ""
}
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
cp.eventBus.Publish(resp)
}
}
cp.Profile.ErrorSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID], ev.Data[event.Error])
/***** Non default but requestable handlable events *****/
case event.NewRetValMessageFromPeer:
onion := ev.Data[event.RemotePeer]
scope := ev.Data[event.Scope]
path := ev.Data[event.Path]
val := ev.Data[event.Data]
exists, _ := strconv.ParseBool(ev.Data[event.Exists])
log.Debugf("NewRetValMessageFromPeer %v %v%v %v %v\n", onion, scope, path, exists, val)
if exists {
if scope == attr.PublicScope {
cp.SetContactAttribute(onion, attr.GetPeerScope(path), val)
}
}
case event.NewGroupInvite:
cp.Profile.ProcessInvite(ev.Data[event.GroupInvite], ev.Data[event.RemotePeer])
case event.PeerStateChange:
cp.mutex.Lock()
if _, exists := cp.Profile.Contacts[ev.Data[event.RemotePeer]]; exists {
cp.Profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState]
}
cp.mutex.Unlock()
case event.ServerStateChange:
cp.mutex.Lock()
// We update both the server contact status, as well as the groups the server belongs to
cp.Profile.Contacts[ev.Data[event.GroupServer]].State = ev.Data[event.ConnectionState]
// TODO deprecate this, the UI should consult the server contact entry instead (it's far more efficient)
for _, group := range cp.Profile.Groups {
if group.GroupServer == ev.Data[event.GroupServer] {
group.State = ev.Data[event.ConnectionState]
}
}
cp.mutex.Unlock()
default:
if ev.EventType != "" {

79
peer/cwtch_peer_test.go Normal file
View File

@ -0,0 +1,79 @@
package peer
import (
"testing"
)
// TODO: Rewrite these tests (and others) using the news event bus interface.
func TestCwtchPeerGenerate(t *testing.T) {
/**
alice := NewCwtchPeer("alice")
groupID, _, _ := alice.StartGroup("test.server")
exportedGroup, _ := alice.ExportGroup(groupID)
t.Logf("Exported Group: %v from %v", exportedGroup, alice.GetProfile().Onion)
importedGroupID, err := alice.ImportGroup(exportedGroup)
group := alice.GetGroup(importedGroupID)
t.Logf("Imported Group: %v, err := %v %v", group, err, importedGroupID)
*/
}
func TestTrustPeer(t *testing.T) {
/**
groupName := "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"
alice := NewCwtchPeer("alice")
aem := new(event.Manager)
aem.Initialize()
alice.Init(connectivity.LocalProvider(),aem)
defer alice.Shutdown()
bob := NewCwtchPeer("bob")
bem := new(event.Manager)
bem.Initialize()
bob.Init(connectivity.LocalProvider(), bem)
defer bob.Shutdown()
bobOnion := bob.GetProfile().Onion
aliceOnion := alice.GetProfile().Onion
groupID, _, err := alice.StartGroup(groupName)
if err != nil {
t.Error(err)
}
groupAlice := alice.GetGroup(groupID)
if groupAlice.GroupID != groupID {
t.Errorf("Alice should be part of group %v, got %v instead", groupID, groupAlice)
}
exportedGroup, err := alice.ExportGroup(groupID)
if err != nil {
t.Error(err)
}
err = alice.InviteOnionToGroup(bobOnion, groupID)
if err == nil {
t.Errorf("onion invitation should fail since alice does no trust bob")
}
err = alice.TrustPeer(bobOnion)
if err == nil {
t.Errorf("trust peer should fail since alice does not know about bob")
}
// bob adds alice contact by importing serialized group created by alice
_, err = bob.ImportGroup(exportedGroup)
if err != nil {
t.Error(err)
}
err = bob.TrustPeer(aliceOnion)
if err != nil {
t.Errorf("bob must be able to trust alice, got %v", err)
}
err = bob.InviteOnionToGroup(aliceOnion, groupID)
if err == nil {
t.Errorf("bob trusts alice but peer connection is not ready yet. should not be able to invite her to group, instead got: %v", err)
}
*/
}

View File

@ -0,0 +1,325 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: ControlChannel.proto
/*
Package protocol is a generated protocol buffer package.
It is generated from these files:
ControlChannel.proto
cwtch-profile.proto
group_message.proto
It has these top-level messages:
Packet
OpenChannel
ChannelResult
KeepAlive
EnableFeatures
FeaturesEnabled
CwtchPeerPacket
CwtchIdentity
GroupChatInvite
CwtchServerPacket
FetchMessage
GroupMessage
DecryptedGroupMessage
*/
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
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type ChannelResult_CommonError int32
const (
ChannelResult_GenericError ChannelResult_CommonError = 0
ChannelResult_UnknownTypeError ChannelResult_CommonError = 1
ChannelResult_UnauthorizedError ChannelResult_CommonError = 2
ChannelResult_BadUsageError ChannelResult_CommonError = 3
ChannelResult_FailedError ChannelResult_CommonError = 4
)
var ChannelResult_CommonError_name = map[int32]string{
0: "GenericError",
1: "UnknownTypeError",
2: "UnauthorizedError",
3: "BadUsageError",
4: "FailedError",
}
var ChannelResult_CommonError_value = map[string]int32{
"GenericError": 0,
"UnknownTypeError": 1,
"UnauthorizedError": 2,
"BadUsageError": 3,
"FailedError": 4,
}
func (x ChannelResult_CommonError) Enum() *ChannelResult_CommonError {
p := new(ChannelResult_CommonError)
*p = x
return p
}
func (x ChannelResult_CommonError) String() string {
return proto.EnumName(ChannelResult_CommonError_name, int32(x))
}
func (x *ChannelResult_CommonError) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(ChannelResult_CommonError_value, data, "ChannelResult_CommonError")
if err != nil {
return err
}
*x = ChannelResult_CommonError(value)
return nil
}
func (ChannelResult_CommonError) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{2, 0} }
type Packet struct {
// Must contain exactly one field
OpenChannel *OpenChannel `protobuf:"bytes,1,opt,name=open_channel,json=openChannel" json:"open_channel,omitempty"`
ChannelResult *ChannelResult `protobuf:"bytes,2,opt,name=channel_result,json=channelResult" json:"channel_result,omitempty"`
KeepAlive *KeepAlive `protobuf:"bytes,3,opt,name=keep_alive,json=keepAlive" json:"keep_alive,omitempty"`
EnableFeatures *EnableFeatures `protobuf:"bytes,4,opt,name=enable_features,json=enableFeatures" json:"enable_features,omitempty"`
FeaturesEnabled *FeaturesEnabled `protobuf:"bytes,5,opt,name=features_enabled,json=featuresEnabled" json:"features_enabled,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Packet) Reset() { *m = Packet{} }
func (m *Packet) String() string { return proto.CompactTextString(m) }
func (*Packet) ProtoMessage() {}
func (*Packet) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *Packet) GetOpenChannel() *OpenChannel {
if m != nil {
return m.OpenChannel
}
return nil
}
func (m *Packet) GetChannelResult() *ChannelResult {
if m != nil {
return m.ChannelResult
}
return nil
}
func (m *Packet) GetKeepAlive() *KeepAlive {
if m != nil {
return m.KeepAlive
}
return nil
}
func (m *Packet) GetEnableFeatures() *EnableFeatures {
if m != nil {
return m.EnableFeatures
}
return nil
}
func (m *Packet) GetFeaturesEnabled() *FeaturesEnabled {
if m != nil {
return m.FeaturesEnabled
}
return nil
}
type OpenChannel struct {
ChannelIdentifier *int32 `protobuf:"varint,1,req,name=channel_identifier,json=channelIdentifier" json:"channel_identifier,omitempty"`
ChannelType *string `protobuf:"bytes,2,req,name=channel_type,json=channelType" json:"channel_type,omitempty"`
proto.XXX_InternalExtensions `json:"-"`
XXX_unrecognized []byte `json:"-"`
}
func (m *OpenChannel) Reset() { *m = OpenChannel{} }
func (m *OpenChannel) String() string { return proto.CompactTextString(m) }
func (*OpenChannel) ProtoMessage() {}
func (*OpenChannel) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
var extRange_OpenChannel = []proto.ExtensionRange{
{Start: 100, End: 536870911},
}
func (*OpenChannel) ExtensionRangeArray() []proto.ExtensionRange {
return extRange_OpenChannel
}
func (m *OpenChannel) GetChannelIdentifier() int32 {
if m != nil && m.ChannelIdentifier != nil {
return *m.ChannelIdentifier
}
return 0
}
func (m *OpenChannel) GetChannelType() string {
if m != nil && m.ChannelType != nil {
return *m.ChannelType
}
return ""
}
type ChannelResult struct {
ChannelIdentifier *int32 `protobuf:"varint,1,req,name=channel_identifier,json=channelIdentifier" json:"channel_identifier,omitempty"`
Opened *bool `protobuf:"varint,2,req,name=opened" json:"opened,omitempty"`
CommonError *ChannelResult_CommonError `protobuf:"varint,3,opt,name=common_error,json=commonError,enum=protocol.ChannelResult_CommonError" json:"common_error,omitempty"`
proto.XXX_InternalExtensions `json:"-"`
XXX_unrecognized []byte `json:"-"`
}
func (m *ChannelResult) Reset() { *m = ChannelResult{} }
func (m *ChannelResult) String() string { return proto.CompactTextString(m) }
func (*ChannelResult) ProtoMessage() {}
func (*ChannelResult) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
var extRange_ChannelResult = []proto.ExtensionRange{
{Start: 100, End: 536870911},
}
func (*ChannelResult) ExtensionRangeArray() []proto.ExtensionRange {
return extRange_ChannelResult
}
func (m *ChannelResult) GetChannelIdentifier() int32 {
if m != nil && m.ChannelIdentifier != nil {
return *m.ChannelIdentifier
}
return 0
}
func (m *ChannelResult) GetOpened() bool {
if m != nil && m.Opened != nil {
return *m.Opened
}
return false
}
func (m *ChannelResult) GetCommonError() ChannelResult_CommonError {
if m != nil && m.CommonError != nil {
return *m.CommonError
}
return ChannelResult_GenericError
}
type KeepAlive struct {
ResponseRequested *bool `protobuf:"varint,1,req,name=response_requested,json=responseRequested" json:"response_requested,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *KeepAlive) Reset() { *m = KeepAlive{} }
func (m *KeepAlive) String() string { return proto.CompactTextString(m) }
func (*KeepAlive) ProtoMessage() {}
func (*KeepAlive) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func (m *KeepAlive) GetResponseRequested() bool {
if m != nil && m.ResponseRequested != nil {
return *m.ResponseRequested
}
return false
}
type EnableFeatures struct {
Feature []string `protobuf:"bytes,1,rep,name=feature" json:"feature,omitempty"`
proto.XXX_InternalExtensions `json:"-"`
XXX_unrecognized []byte `json:"-"`
}
func (m *EnableFeatures) Reset() { *m = EnableFeatures{} }
func (m *EnableFeatures) String() string { return proto.CompactTextString(m) }
func (*EnableFeatures) ProtoMessage() {}
func (*EnableFeatures) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
var extRange_EnableFeatures = []proto.ExtensionRange{
{Start: 100, End: 536870911},
}
func (*EnableFeatures) ExtensionRangeArray() []proto.ExtensionRange {
return extRange_EnableFeatures
}
func (m *EnableFeatures) GetFeature() []string {
if m != nil {
return m.Feature
}
return nil
}
type FeaturesEnabled struct {
Feature []string `protobuf:"bytes,1,rep,name=feature" json:"feature,omitempty"`
proto.XXX_InternalExtensions `json:"-"`
XXX_unrecognized []byte `json:"-"`
}
func (m *FeaturesEnabled) Reset() { *m = FeaturesEnabled{} }
func (m *FeaturesEnabled) String() string { return proto.CompactTextString(m) }
func (*FeaturesEnabled) ProtoMessage() {}
func (*FeaturesEnabled) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
var extRange_FeaturesEnabled = []proto.ExtensionRange{
{Start: 100, End: 536870911},
}
func (*FeaturesEnabled) ExtensionRangeArray() []proto.ExtensionRange {
return extRange_FeaturesEnabled
}
func (m *FeaturesEnabled) GetFeature() []string {
if m != nil {
return m.Feature
}
return nil
}
func init() {
proto.RegisterType((*Packet)(nil), "protocol.Packet")
proto.RegisterType((*OpenChannel)(nil), "protocol.OpenChannel")
proto.RegisterType((*ChannelResult)(nil), "protocol.ChannelResult")
proto.RegisterType((*KeepAlive)(nil), "protocol.KeepAlive")
proto.RegisterType((*EnableFeatures)(nil), "protocol.EnableFeatures")
proto.RegisterType((*FeaturesEnabled)(nil), "protocol.FeaturesEnabled")
proto.RegisterEnum("protocol.ChannelResult_CommonError", ChannelResult_CommonError_name, ChannelResult_CommonError_value)
}
func init() { proto.RegisterFile("ControlChannel.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 461 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x52, 0x4d, 0x8f, 0xd3, 0x30,
0x10, 0x25, 0xe9, 0xee, 0x92, 0x4e, 0xfa, 0x91, 0x9a, 0x5d, 0x30, 0xb7, 0x12, 0x2e, 0x15, 0x12,
0x3d, 0x54, 0x20, 0x21, 0x0e, 0x48, 0x4b, 0xd9, 0x22, 0xc4, 0x01, 0x64, 0xd1, 0x73, 0x64, 0x92,
0x29, 0x1b, 0x35, 0x6b, 0x1b, 0xc7, 0x05, 0x2d, 0xa7, 0xfe, 0x0e, 0xfe, 0x0c, 0x7f, 0x0d, 0xc5,
0x89, 0x9b, 0x14, 0x09, 0x09, 0x4e, 0xc9, 0x9b, 0xf7, 0xde, 0x8c, 0xfc, 0x66, 0xe0, 0x7c, 0x29,
0x85, 0xd1, 0xb2, 0x58, 0x5e, 0x73, 0x21, 0xb0, 0x98, 0x2b, 0x2d, 0x8d, 0x24, 0x81, 0xfd, 0xa4,
0xb2, 0x88, 0x7f, 0xf9, 0x70, 0xf6, 0x91, 0xa7, 0x5b, 0x34, 0xe4, 0x05, 0x0c, 0xa4, 0x42, 0x91,
0xa4, 0xb5, 0x94, 0x7a, 0x53, 0x6f, 0x16, 0x2e, 0x2e, 0xe6, 0x4e, 0x3b, 0xff, 0xa0, 0x50, 0x34,
0x7d, 0x58, 0x28, 0x5b, 0x40, 0x5e, 0xc1, 0xa8, 0x31, 0x25, 0x1a, 0xcb, 0x5d, 0x61, 0xa8, 0x6f,
0xbd, 0x0f, 0x5a, 0xaf, 0xf3, 0x59, 0x9a, 0x0d, 0xd3, 0x2e, 0x24, 0x0b, 0x80, 0x2d, 0xa2, 0x4a,
0x78, 0x91, 0x7f, 0x43, 0xda, 0xb3, 0xde, 0x7b, 0xad, 0xf7, 0x3d, 0xa2, 0xba, 0xac, 0x28, 0xd6,
0xdf, 0xba, 0x5f, 0x72, 0x09, 0x63, 0x14, 0xfc, 0x73, 0x81, 0xc9, 0x06, 0xb9, 0xd9, 0x69, 0x2c,
0xe9, 0x89, 0x35, 0xd2, 0xd6, 0x78, 0x65, 0x05, 0xab, 0x86, 0x67, 0x23, 0x3c, 0xc2, 0xe4, 0x0d,
0x44, 0xce, 0x9b, 0xd4, 0x54, 0x46, 0x4f, 0x6d, 0x8f, 0x87, 0x6d, 0x0f, 0xa7, 0xae, 0x7b, 0x65,
0x6c, 0xbc, 0x39, 0x2e, 0xc4, 0x39, 0x84, 0x9d, 0x60, 0xc8, 0x53, 0x20, 0x2e, 0x8b, 0x3c, 0x43,
0x61, 0xf2, 0x4d, 0x8e, 0x9a, 0x7a, 0x53, 0x7f, 0x76, 0xca, 0x26, 0x0d, 0xf3, 0xee, 0x40, 0x90,
0x47, 0x30, 0x70, 0x72, 0x73, 0xab, 0x90, 0xfa, 0x53, 0x7f, 0xd6, 0x67, 0x61, 0x53, 0xfb, 0x74,
0xab, 0xf0, 0x49, 0x10, 0x64, 0xd1, 0x7e, 0xbf, 0xdf, 0xfb, 0xf1, 0x4f, 0x1f, 0x86, 0x47, 0x41,
0xfe, 0xef, 0xb4, 0xfb, 0x70, 0x56, 0xed, 0x0d, 0x33, 0x3b, 0x27, 0x60, 0x0d, 0x22, 0x2b, 0x18,
0xa4, 0xf2, 0xe6, 0x46, 0x8a, 0x04, 0xb5, 0x96, 0xda, 0xae, 0x60, 0xb4, 0x78, 0xfc, 0x97, 0xf5,
0xcd, 0x97, 0x56, 0x7b, 0x55, 0x49, 0x59, 0x98, 0xb6, 0x20, 0x56, 0x10, 0x76, 0x38, 0x12, 0xc1,
0xe0, 0x2d, 0x0a, 0xd4, 0x79, 0x6a, 0x71, 0x74, 0x87, 0x9c, 0x43, 0xb4, 0x16, 0x5b, 0x21, 0xbf,
0x8b, 0xea, 0x69, 0x75, 0xd5, 0x23, 0x17, 0x30, 0x59, 0x0b, 0xbe, 0x33, 0xd7, 0x52, 0xe7, 0x3f,
0x30, 0xab, 0xcb, 0x3e, 0x99, 0xc0, 0xf0, 0x35, 0xcf, 0xd6, 0x25, 0xff, 0xd2, 0x28, 0x7b, 0x64,
0x0c, 0xe1, 0x8a, 0xe7, 0x85, 0xd3, 0x9c, 0x74, 0xc2, 0x79, 0x09, 0xfd, 0xc3, 0xa1, 0x54, 0xb9,
0x68, 0x2c, 0x95, 0x14, 0x25, 0x26, 0x1a, 0xbf, 0xee, 0xb0, 0x34, 0x98, 0xd9, 0x5c, 0x02, 0x36,
0x71, 0x0c, 0x73, 0x44, 0xfc, 0x0c, 0x46, 0xc7, 0xb7, 0x42, 0x28, 0xdc, 0x6d, 0x16, 0x4d, 0xbd,
0x69, 0x6f, 0xd6, 0x67, 0x0e, 0x76, 0x26, 0x3e, 0x87, 0xf1, 0x1f, 0xd7, 0xf1, 0x2f, 0xb6, 0xdf,
0x01, 0x00, 0x00, 0xff, 0xff, 0x9d, 0x32, 0x16, 0x1e, 0x93, 0x03, 0x00, 0x00,
}

View File

@ -0,0 +1,53 @@
syntax = "proto2";
package protocol;
message Packet {
// Must contain exactly one field
optional OpenChannel open_channel = 1;
optional ChannelResult channel_result = 2;
optional KeepAlive keep_alive = 3;
optional EnableFeatures enable_features = 4;
optional FeaturesEnabled features_enabled = 5;
}
message OpenChannel {
required int32 channel_identifier = 1; // Arbitrary unique identifier for this channel instance
required string channel_type = 2; // String identifying channel type; e.g. im.ricochet.chat
// It is valid to extend the OpenChannel message to add fields specific
// to the requested channel_type.
extensions 100 to max;
}
message ChannelResult {
required int32 channel_identifier = 1; // Matching the value from OpenChannel
required bool opened = 2; // If the channel is now open
enum CommonError {
GenericError = 0;
UnknownTypeError = 1;
UnauthorizedError = 2;
BadUsageError = 3;
FailedError = 4;
}
optional CommonError common_error = 3;
// As with OpenChannel, it is valid to extend this message with fields specific
// to the channel type.
extensions 100 to max;
}
message KeepAlive {
required bool response_requested = 1;
}
message EnableFeatures {
repeated string feature = 1;
extensions 100 to max;
}
message FeaturesEnabled {
repeated string feature = 1;
extensions 100 to max;
}

View File

@ -0,0 +1,74 @@
package connections
import (
"cwtch.im/cwtch/protocol"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"sync"
)
// Manager encapsulates all the logic necessary to manage outgoing peer and server connections.
type Manager struct {
serverConnections map[string]*PeerServerConnection
lock sync.Mutex
acn connectivity.ACN
}
// NewConnectionsManager creates a new instance of Manager.
func NewConnectionsManager(acn connectivity.ACN) *Manager {
m := new(Manager)
m.acn = acn
m.serverConnections = make(map[string]*PeerServerConnection)
return m
}
// ManageServerConnection creates a new ServerConnection for Host with the given callback handler.
// If there is an establish connection, it is replaced with a new one, assuming this came from
// a new JoinServer from a new Group being joined. If it is still connecting to a server, the second request will be abandonded
func (m *Manager) ManageServerConnection(host string, engine Engine, messageHandler func(string, *protocol.GroupMessage)) {
m.lock.Lock()
defer m.lock.Unlock()
psc, exists := m.serverConnections[host]
if exists {
if psc.GetState() == DISCONNECTED || psc.GetState() == CONNECTING || psc.GetState() == CONNECTED {
log.Infof("Already connecting to %v, abandoning fresh attempt\n", host)
return
}
}
newPsc := NewPeerServerConnection(engine, host)
newPsc.GroupMessageHandler = messageHandler
go newPsc.Run()
m.serverConnections[host] = newPsc
if exists {
log.Infof("Closing connection to %v, replacing with this one\n", host)
psc.Close()
}
}
// SetServerSynced is a helper for peerserver connections and engine to call when a Fetch is done to set the state of the connection to SYNCED
func (m *Manager) SetServerSynced(onion string) {
m.serverConnections[onion].setState(SYNCED)
}
// GetPeerServerConnectionForOnion safely returns a given host connection
func (m *Manager) GetPeerServerConnectionForOnion(host string) (psc *PeerServerConnection) {
m.lock.Lock()
psc = m.serverConnections[host]
m.lock.Unlock()
return
}
// Shutdown closes all connections under management (freeing their goroutines)
func (m *Manager) Shutdown() {
m.lock.Lock()
for onion, psc := range m.serverConnections {
psc.Close()
delete(m.serverConnections, onion)
}
m.lock.Unlock()
}

View File

@ -0,0 +1,11 @@
package connections
import (
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"testing"
)
func TestConnectionsManager(t *testing.T) {
// TODO We need to encapsulate connections behind a well defined interface for tesintg
NewConnectionsManager(connectivity.LocalProvider())
}

View File

@ -2,33 +2,33 @@ package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol/groups"
"encoding/base64"
"encoding/json"
"cwtch.im/cwtch/protocol"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"errors"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
"strconv"
"sync"
"time"
)
type engine struct {
queue event.Queue
queue event.Queue
connectionsManager *Manager
// Engine Attributes
identity primitives.Identity
acn connectivity.ACN
// Authorization list of contacts to authorization status
authorizations sync.Map // string(onion) => model.Authorization
// Engine State
started bool
// Blocklist
blocked sync.Map
// Block Unknown Contacts
blockUnknownContacts bool
@ -39,9 +39,6 @@ type engine struct {
// Nextgen Tapir Service
service tapir.Service
// Nextgen Tapir Service
ephemeralServices sync.Map // string(onion) => tapir.Service
// Required for listen(), inaccessible from identity
privateKey ed25519.PrivateKey
@ -50,8 +47,6 @@ type engine struct {
// Engine (ProtocolEngine) encapsulates the logic necessary to make and receive Cwtch connections.
// Note: ProtocolEngine doesn't have access to any information necessary to encrypt or decrypt GroupMessages
// Protocol Engine *can* associate Group Identifiers with Group Servers, although we don't currently make use of this fact
// other than to route errors back to the UI.
type Engine interface {
ACN() connectivity.ACN
EventManager() event.Manager
@ -59,7 +54,7 @@ type Engine interface {
}
// NewProtocolEngine initializes a new engine that runs Cwtch using the given parameters
func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateKey, acn connectivity.ACN, eventManager event.Manager, peerAuthorizations map[string]model.Authorization) Engine {
func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateKey, acn connectivity.ACN, eventManager event.Manager, knownPeers []string, blockedPeers []string) Engine {
engine := new(engine)
engine.identity = identity
engine.privateKey = privateKey
@ -67,6 +62,7 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
go engine.eventHandler()
engine.acn = acn
engine.connectionsManager = NewConnectionsManager(engine.acn)
// Init the Server running the Simple App.
engine.service = new(tor.BaseOnionService)
@ -79,20 +75,22 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
engine.eventManager.Subscribe(event.RetryPeerRequest, engine.queue)
engine.eventManager.Subscribe(event.InvitePeerToGroup, engine.queue)
engine.eventManager.Subscribe(event.JoinServer, engine.queue)
engine.eventManager.Subscribe(event.LeaveServer, engine.queue)
engine.eventManager.Subscribe(event.SendMessageToGroup, engine.queue)
engine.eventManager.Subscribe(event.SendMessageToPeer, engine.queue)
engine.eventManager.Subscribe(event.SendGetValMessageToPeer, engine.queue)
engine.eventManager.Subscribe(event.SendRetValMessageToPeer, engine.queue)
engine.eventManager.Subscribe(event.DeleteContact, engine.queue)
engine.eventManager.Subscribe(event.DeleteGroup, engine.queue)
engine.eventManager.Subscribe(event.SetPeerAuthorization, engine.queue)
engine.eventManager.Subscribe(event.BlockPeer, engine.queue)
engine.eventManager.Subscribe(event.UnblockPeer, engine.queue)
engine.eventManager.Subscribe(event.BlockUnknownPeers, engine.queue)
engine.eventManager.Subscribe(event.AllowUnknownPeers, engine.queue)
for peer, authorization := range peerAuthorizations {
engine.authorizations.Store(peer, authorization)
for _, peer := range knownPeers {
engine.blocked.Store(peer, false)
}
for _, peer := range blockedPeers {
engine.blocked.Store(peer, true)
}
return engine
}
@ -113,38 +111,29 @@ func (e *engine) eventHandler() {
case event.StatusRequest:
e.eventManager.Publish(event.Event{EventType: event.ProtocolEngineStatus, EventID: ev.EventID})
case event.PeerRequest:
if torProvider.IsValidHostname(ev.Data[event.RemotePeer]) {
if utils.IsValidHostname(ev.Data[event.RemotePeer]) {
e.blocked.Store(ev.Data[event.RemotePeer], false)
go e.peerWithOnion(ev.Data[event.RemotePeer])
}
case event.RetryPeerRequest:
// This event allows engine to treat (automated) retry peering requests differently to user-specified
// peer events
if torProvider.IsValidHostname(ev.Data[event.RemotePeer]) {
log.Debugf("Retrying Peer Request: %v", ev.Data[event.RemotePeer])
if utils.IsValidHostname(ev.Data[event.RemotePeer]) {
go e.peerWithOnion(ev.Data[event.RemotePeer])
}
case event.InvitePeerToGroup:
e.sendMessageToPeer(ev.EventID, ev.Data[event.RemotePeer], event.ContextInvite, []byte(ev.Data[event.GroupInvite]))
case event.JoinServer:
signature, err := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
if err != nil {
// will result in a full sync
signature = []byte{}
}
go e.peerWithTokenServer(ev.Data[event.GroupServer], ev.Data[event.ServerTokenOnion], ev.Data[event.ServerTokenY], signature)
case event.LeaveServer:
e.leaveServer(ev.Data[event.GroupServer])
e.joinServer(ev.Data[event.GroupServer])
case event.DeleteContact:
onion := ev.Data[event.RemotePeer]
// We remove this peer from out blocklist which will prevent them from contacting us if we have "block unknown peers" turned on.
e.authorizations.Delete(ev.Data[event.RemotePeer])
e.blocked.Delete(ev.Data[event.RemotePeer])
e.deleteConnection(onion)
case event.DeleteGroup:
// TODO: There isn't a way here to determine if other Groups are using a server connection...
case event.SendMessageToGroup:
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
go e.sendMessageToGroup(ev.Data[event.GroupID], ev.Data[event.GroupServer], ciphertext, signature)
e.sendMessageToGroup(ev.Data[event.GroupServer], []byte(ev.Data[event.Ciphertext]), []byte(ev.Data[event.Signature]))
case event.SendMessageToPeer:
// TODO: remove this passthrough once the UI is integrated.
context, ok := ev.Data[event.EventContext]
@ -155,28 +144,24 @@ func (e *engine) eventHandler() {
if err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: "peer is offline or the connection has yet to finalize"}))
}
case event.SendGetValMessageToPeer:
e.sendGetValToPeer(ev.EventID, ev.Data[event.RemotePeer], ev.Data[event.Scope], ev.Data[event.Path])
case event.SendRetValMessageToPeer:
e.sendRetValToPeer(ev.EventID, ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.Exists])
case event.SetPeerAuthorization:
auth := model.Authorization(ev.Data[event.Authorization])
e.authorizations.Store(ev.Data[event.RemotePeer], auth)
if auth == model.AuthBlocked {
connection, err := e.service.GetConnection(ev.Data[event.RemotePeer])
if connection != nil && err == nil {
connection.Close()
}
// Explicitly send a disconnected event (if we don't do this here then the UI can wait for a while before
// an ongoing Open() connection fails and so the user will see a blocked peer as still connecting (because
// there isn't an active connection and we are stuck waiting for tor to time out)
e.peerDisconnected(ev.Data[event.RemotePeer])
case event.UnblockPeer:
// We simply remove the peer from our blocklist
// The UI has the responsibility to reinitiate contact with the peer.
// (this should happen periodically in any case)
e.blocked.Store(ev.Data[event.RemotePeer], false)
case event.BlockPeer:
e.blocked.Store(ev.Data[event.RemotePeer], true)
connection, err := e.service.GetConnection(ev.Data[event.RemotePeer])
if connection != nil && err == nil {
connection.Close()
}
// Explicitly send a disconnected event (if we don't do this here then the UI can wait for a while before
// an ongoing Open() connection fails and so the user will see a blocked peer as still connecting (because
// there isn't an active connection and we are stuck waiting for tor to time out)
e.peerDisconnected(ev.Data[event.RemotePeer])
case event.AllowUnknownPeers:
log.Debugf("%v now allows unknown connections", e.identity.Hostname())
e.blockUnknownContacts = false
case event.BlockUnknownPeers:
log.Debugf("%v now forbids unknown connections", e.identity.Hostname())
e.blockUnknownContacts = true
case event.ProtocolEngineStartListen:
go e.listenFn()
@ -186,37 +171,21 @@ func (e *engine) eventHandler() {
}
}
func (e *engine) isBlocked(onion string) bool {
authorization, known := e.authorizations.Load(onion)
if !known {
// if we block unknown peers we will block this contact
return e.blockUnknownContacts
}
return authorization.(model.Authorization) == model.AuthBlocked
}
func (e *engine) isAllowed(onion string) bool {
authorization, known := e.authorizations.Load(onion)
if !known {
log.Errorf("attempted to lookup authorization of onion not in map...that should never happen")
return false
}
if e.blockUnknownContacts {
return authorization.(model.Authorization) == model.AuthApproved
}
return authorization.(model.Authorization) != model.AuthBlocked
}
func (e *engine) createPeerTemplate() *PeerApp {
peerAppTemplate := new(PeerApp)
peerAppTemplate.IsBlocked = e.isBlocked
peerAppTemplate.IsAllowed = e.isAllowed
peerAppTemplate.IsBlocked = func(onion string) bool {
blocked, known := e.blocked.Load(onion)
if !known {
// if we block unknown peers we will block this contact
return e.blockUnknownContacts
}
return blocked.(bool)
}
peerAppTemplate.MessageHandler = e.handlePeerMessage
peerAppTemplate.OnAcknowledgement = e.ignoreOnShutdown2(e.peerAck)
peerAppTemplate.OnAuth = e.ignoreOnShutdown(e.peerAuthed)
peerAppTemplate.OnConnecting = e.ignoreOnShutdown(e.peerConnecting)
peerAppTemplate.OnClose = e.ignoreOnShutdown(e.peerDisconnected)
peerAppTemplate.RetValHandler = e.handlePeerRetVal
return peerAppTemplate
}
@ -226,11 +195,13 @@ func (e *engine) listenFn() {
if !e.shuttingDown {
e.eventManager.Publish(event.NewEvent(event.ProtocolEngineStopped, map[event.Field]string{event.Identity: e.identity.Hostname(), event.Error: err.Error()}))
}
return
}
// Shutdown tears down the eventHandler goroutine
func (e *engine) Shutdown() {
e.shuttingDown = true
e.connectionsManager.Shutdown()
e.service.Shutdown()
e.queue.Shutdown()
}
@ -238,8 +209,8 @@ func (e *engine) Shutdown() {
// peerWithOnion is the entry point for cwtchPeer relationships
// needs to be run in a goroutine as will block on Open.
func (e *engine) peerWithOnion(onion string) {
log.Debugf("Called PeerWithOnion for %v", onion)
if !e.isBlocked(onion) {
blocked, known := e.blocked.Load(onion)
if known && !(blocked.(bool)) {
e.ignoreOnShutdown(e.peerConnecting)(onion)
connected, err := e.service.Connect(onion, e.createPeerTemplate())
@ -248,7 +219,7 @@ func (e *engine) peerWithOnion(onion string) {
if connected && err != nil {
conn, err := e.service.GetConnection(onion)
if err == nil {
if conn.HasCapability(cwtchCapability) {
if conn.HasCapability(applications.AuthCapability) {
e.ignoreOnShutdown(e.peerAuthed)(onion)
return
}
@ -262,55 +233,6 @@ func (e *engine) peerWithOnion(onion string) {
}
}
// peerWithTokenServer is the entry point for cwtchPeer - server relationships
// needs to be run in a goroutine as will block on Open.
func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, tokenServerY string, lastKnownSignature []byte) {
service, exists := e.ephemeralServices.Load(onion)
if exists {
connection := service.(*tor.BaseOnionService)
if conn, err := connection.GetConnection(onion); err == nil {
// We are already peered and synced so return...
// This will only not-trigger it lastKnownSignature has been wiped, which only happens when ResyncServer is called
// in CwtchPeer.
if !conn.IsClosed() && len(lastKnownSignature) != 0 {
return
}
// Otherwise...we are going to rebuild the connection(which will result in a bandwidth heavy resync)...
e.leaveServer(onion)
}
// Otherwise...let's reconnect
}
log.Debugf("Peering with Token Server %v %v", onion, tokenServerOnion)
e.ignoreOnShutdown(e.serverConnecting)(onion)
// Create a new ephemeral service for this connection
ephemeralService := new(tor.BaseOnionService)
eid, epk := primitives.InitializeEphemeralIdentity()
ephemeralService.Init(e.acn, epk, &eid)
Y := ristretto255.NewElement()
Y.UnmarshalText([]byte(tokenServerY))
connected, err := ephemeralService.Connect(onion, NewTokenBoardClient(e.acn, Y, tokenServerOnion, lastKnownSignature, e.receiveGroupMessage, e.serverSynced, e.serverDisconnected))
e.ephemeralServices.Store(onion, ephemeralService)
// If we are already connected...check if we are authed and issue an auth event
// (This allows the ui to be stateless)
if connected && err != nil {
conn, err := ephemeralService.GetConnection(onion)
if err == nil {
if conn.HasCapability(groups.CwtchServerSyncedCapability) {
e.ignoreOnShutdown(e.serverConnected)(onion)
return
}
}
}
// Only issue a disconnected error if we are disconnected (Connect will fail if a connection already exists)
if !connected && err != nil {
e.ignoreOnShutdown(e.serverDisconnected)(onion)
}
}
func (e *engine) ignoreOnShutdown(f func(string)) func(string) {
return func(x string) {
if !e.shuttingDown {
@ -328,10 +250,6 @@ func (e *engine) ignoreOnShutdown2(f func(string, string)) func(string, string)
}
func (e *engine) peerAuthed(onion string) {
_, known := e.authorizations.Load(onion)
if !known {
e.authorizations.Store(onion, model.AuthUnknown)
}
e.eventManager.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{
event.RemotePeer: string(onion),
event.ConnectionState: ConnectionStateName[AUTHENTICATED],
@ -345,34 +263,6 @@ func (e *engine) peerConnecting(onion string) {
}))
}
func (e *engine) serverConnecting(onion string) {
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: string(onion),
event.ConnectionState: ConnectionStateName[CONNECTING],
}))
}
func (e *engine) serverConnected(onion string) {
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: onion,
event.ConnectionState: ConnectionStateName[CONNECTED],
}))
}
func (e *engine) serverSynced(onion string) {
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: onion,
event.ConnectionState: ConnectionStateName[SYNCED],
}))
}
func (e *engine) serverDisconnected(onion string) {
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: onion,
event.ConnectionState: ConnectionStateName[DISCONNECTED],
}))
}
func (e *engine) peerAck(onion string, eventID string) {
e.eventManager.Publish(event.NewEvent(event.PeerAcknowledgement, map[event.Field]string{
event.EventID: eventID,
@ -389,7 +279,7 @@ func (e *engine) peerDisconnected(onion string) {
// sendMessageToPeer sends a message to a peer under a given context
func (e *engine) sendMessageToPeer(eventID string, onion string, context string, message []byte) error {
conn, err := e.service.WaitForCapabilityOrClose(onion, cwtchCapability)
conn, err := e.service.GetConnection(onion)
if err == nil {
peerApp, ok := (conn.App()).(*PeerApp)
if ok {
@ -401,27 +291,6 @@ func (e *engine) sendMessageToPeer(eventID string, onion string, context string,
return err
}
func (e *engine) sendGetValToPeer(eventID, onion, scope, path string) error {
log.Debugf("sendGetValMessage to peer %v %v%v\n", onion, scope, path)
getVal := peerGetVal{Scope: scope, Path: path}
message, err := json.Marshal(getVal)
if err != nil {
return err
}
return e.sendMessageToPeer(eventID, onion, event.ContextGetVal, message)
}
func (e *engine) sendRetValToPeer(eventID, onion, val, existsStr string) error {
log.Debugf("sendRetValMessage to peer %v (%v) %v %v\n", onion, eventID, val, existsStr)
exists, _ := strconv.ParseBool(existsStr)
retVal := peerRetVal{Val: val, Exists: exists}
message, err := json.Marshal(retVal)
if err != nil {
return err
}
return e.sendMessageToPeer(eventID, onion, event.ContextRetVal, message)
}
func (e *engine) deleteConnection(id string) {
conn, err := e.service.GetConnection(id)
if err == nil {
@ -430,86 +299,39 @@ func (e *engine) deleteConnection(id string) {
}
// receiveGroupMessage is a callback function that processes GroupMessages from a given server
func (e *engine) receiveGroupMessage(server string, gm *groups.EncryptedGroupMessage) {
func (e *engine) receiveGroupMessage(server string, gm *protocol.GroupMessage) {
// Publish Event so that a Profile Engine can deal with it.
// Note: This technically means that *multiple* Profile Engines could listen to the same ProtocolEngine!
e.eventManager.Publish(event.NewEvent(event.EncryptedGroupMessage, map[event.Field]string{event.GroupServer: server, event.Ciphertext: base64.StdEncoding.EncodeToString(gm.Ciphertext), event.Signature: base64.StdEncoding.EncodeToString(gm.Signature)}))
e.eventManager.Publish(event.NewEvent(event.EncryptedGroupMessage, map[event.Field]string{event.Ciphertext: string(gm.GetCiphertext()), event.Signature: string(gm.GetSignature())}))
}
// joinServer manages a new server connection with the given onion address
func (e *engine) joinServer(onion string) {
e.connectionsManager.ManageServerConnection(onion, e, e.receiveGroupMessage)
}
// sendMessageToGroup attempts to sent the given message to the given group id.
func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, sig []byte) {
es, ok := e.ephemeralServices.Load(server)
if es == nil || !ok {
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: "server-not-found", event.Signature: base64.StdEncoding.EncodeToString(sig)}))
return
func (e *engine) sendMessageToGroup(server string, ct []byte, sig []byte) {
psc := e.connectionsManager.GetPeerServerConnectionForOnion(server)
if psc == nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupServer: server, event.Signature: string(sig), event.Error: "server is offline or the connection has yet to finalize"}))
}
ephemeralService := es.(tapir.Service)
conn, err := ephemeralService.WaitForCapabilityOrClose(server, groups.CwtchServerSyncedCapability)
if err == nil {
tokenApp, ok := (conn.App()).(*TokenBoardClient)
if ok {
if spent, numtokens := tokenApp.Post(ct, sig); !spent {
// TODO: while this works for the spam guard, it won't work for other forms of payment...
// Make an -inline- payment, this will hold the goroutine
if err := tokenApp.MakePayment(); err == nil {
// This really shouldn't fail since we now know we have the required tokens...
if spent, _ := tokenApp.Post(ct, sig); !spent {
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: err.Error(), event.Signature: base64.StdEncoding.EncodeToString(sig)}))
}
} else {
// Broadast the token error
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: err.Error(), event.Signature: base64.StdEncoding.EncodeToString(sig)}))
}
} else if numtokens < 5 {
go tokenApp.MakePayment()
}
// regardless we return....
return
}
gm := &protocol.GroupMessage{
Ciphertext: ct,
Signature: sig,
}
err := psc.SendGroupMessage(gm)
if err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupServer: server, event.Signature: string(sig), event.Error: err.Error()}))
}
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: "server-connection-not-valid", event.Signature: base64.StdEncoding.EncodeToString(sig)}))
}
func (e *engine) handlePeerMessage(hostname string, eventID string, context string, message []byte) {
func (e *engine) handlePeerMessage(hostname string, context string, message []byte) {
log.Debugf("New message from peer: %v %v", hostname, context)
if context == event.ContextGetVal {
var getVal peerGetVal
err := json.Unmarshal(message, &getVal)
if err == nil {
ev := event.NewEventList(event.NewGetValMessageFromPeer, event.RemotePeer, hostname, event.Scope, getVal.Scope, event.Path, getVal.Path)
ev.EventID = eventID
e.eventManager.Publish(ev)
}
if context == event.ContextInvite {
e.eventManager.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: hostname, event.GroupInvite: string(message)}))
} else {
e.eventManager.Publish(event.NewEvent(event.NewMessageFromPeer, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: hostname, event.Data: string(message)}))
}
}
func (e *engine) handlePeerRetVal(hostname string, getValData, retValData []byte) {
var getVal peerGetVal
var retVal peerRetVal
err := json.Unmarshal(getValData, &getVal)
if err != nil {
log.Errorf("Unmarshalling our own getVal request: %v\n", err)
return
}
err = json.Unmarshal(retValData, &retVal)
if err != nil {
log.Errorf("Unmarshalling peer response to getVal request")
return
}
e.eventManager.Publish(event.NewEventList(event.NewRetValMessageFromPeer, event.RemotePeer, hostname, event.Scope, getVal.Scope, event.Path, getVal.Path, event.Exists, strconv.FormatBool(retVal.Exists), event.Data, retVal.Val))
}
func (e *engine) leaveServer(server string) {
es, ok := e.ephemeralServices.Load(server)
if ok {
ephemeralService := es.(tapir.Service)
ephemeralService.Shutdown()
e.ephemeralServices.Delete(server)
}
}

View File

@ -0,0 +1,104 @@
package fetch
import (
"cwtch.im/cwtch/protocol"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
)
// CwtchPeerFetchChannel is the peer implementation of the im.cwtch.server.fetch
// channel.
type CwtchPeerFetchChannel struct {
channel *channels.Channel
Handler CwtchPeerFetchChannelHandler
}
// CwtchPeerFetchChannelHandler should be implemented by peers to receive new messages.
type CwtchPeerFetchChannelHandler interface {
HandleGroupMessage(*protocol.GroupMessage)
HandleFetchDone()
}
// 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) {
cpfc.Handler.HandleFetchDone()
}
// 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
cpfc.FetchRequest()
}
}
}
// FetchRequest sends a FetchMessage to the Server.
func (cpfc *CwtchPeerFetchChannel) FetchRequest() error {
if cpfc.channel.Pending == false {
fm := &protocol.FetchMessage{}
csp := &protocol.CwtchServerPacket{
FetchMessage: fm,
}
packet, _ := proto.Marshal(csp)
cpfc.channel.SendMessage(packet)
} else {
return errors.New("channel isn't set up yet")
}
return nil
}
// 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.GetGroupMessage() != nil {
gm := csp.GetGroupMessage()
// We create a new go routine here to avoid leaking any information about processing time
// TODO Server can probably try to use this to DoS a peer
cpfc.Handler.HandleGroupMessage(gm)
}
}
}

View File

@ -0,0 +1,97 @@
package fetch
import (
"cwtch.im/cwtch/protocol"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
"time"
)
type TestHandler struct {
Received bool
Closed bool
}
func (th *TestHandler) HandleGroupMessage(m *protocol.GroupMessage) {
th.Received = true
}
func (th *TestHandler) HandleFetchDone() {
th.Closed = 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
channel.SendMessage = func([]byte) {}
channel.CloseChannel = func() {}
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"), Signature: []byte{}, Spamguard: []byte{},
},
}
packet, _ := proto.Marshal(csp)
pfc.Packet(packet)
time.Sleep(time.Second * 2)
if th.Received != true {
t.Errorf("group message should not have been received")
}
pfc.Closed(nil)
}

View File

@ -0,0 +1,84 @@
package listen
import (
"cwtch.im/cwtch/protocol"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
)
// CwtchPeerListenChannel is the peer implementation of im.cwtch.server.listen
type CwtchPeerListenChannel struct {
channel *channels.Channel
Handler CwtchPeerSendChannelHandler
}
// CwtchPeerSendChannelHandler is implemented by peers who want to listen to new messages
type CwtchPeerSendChannelHandler interface {
HandleGroupMessage(*protocol.GroupMessage)
}
// Type returns the type string for this channel, e.g. "im.ricochet.server.listen".
func (cplc *CwtchPeerListenChannel) Type() string {
return "im.cwtch.server.listen"
}
// Closed is called when the channel is closed for any reason.
func (cplc *CwtchPeerListenChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch server channels can only be opened by peers
func (cplc *CwtchPeerListenChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cplc *CwtchPeerListenChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cplc *CwtchPeerListenChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require no auth channels
func (cplc *CwtchPeerListenChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound - peers should never respond to open inbound requests from servers
func (cplc *CwtchPeerListenChannel) 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 server listen channel
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 confirms a previous open channel request
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 server packet received on this channel.
func (cplc *CwtchPeerListenChannel) Packet(data []byte) {
csp := &protocol.CwtchServerPacket{}
err := proto.Unmarshal(data, csp)
if err == nil {
if csp.GetGroupMessage() != nil {
gm := csp.GetGroupMessage()
cplc.Handler.HandleGroupMessage(gm)
}
}
}

View File

@ -0,0 +1,89 @@
package listen
import (
"cwtch.im/cwtch/protocol"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
"time"
)
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"), Signature: []byte{}, Spamguard: []byte{}},
}
packet, _ := proto.Marshal(csp)
pfc.Packet(packet)
// Wait for goroutine to run
time.Sleep(time.Second * 1)
if !th.Received {
t.Errorf("group message should have been received")
}
pfc.Closed(nil)
}

View File

@ -2,29 +2,22 @@ package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/openprivacy/log"
"sync"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
)
const cwtchCapability = tapir.Capability("cwtchCapability")
// PeerApp encapsulates the behaviour of a Cwtch Peer
type PeerApp struct {
applications.AuthApp
connection tapir.Connection
MessageHandler func(string, string, string, []byte)
RetValHandler func(string, []byte, []byte)
MessageHandler func(string, string, []byte)
IsBlocked func(string) bool
IsAllowed func(string) bool
OnAcknowledgement func(string, string)
OnAuth func(string)
OnClose func(string)
OnConnecting func(string)
getValRequests sync.Map // [string]string eventID:Data
}
// PeerMessage is an encapsulation that can be used by higher level applications
@ -34,38 +27,27 @@ type PeerMessage struct {
Data []byte // The serialized data packet.
}
type peerGetVal struct {
Scope, Path string
}
type peerRetVal struct {
Val string
Exists bool
}
// NewInstance should always return a new instantiation of the application.
func (pa *PeerApp) NewInstance() tapir.Application {
func (pa PeerApp) NewInstance() tapir.Application {
newApp := new(PeerApp)
newApp.MessageHandler = pa.MessageHandler
newApp.IsBlocked = pa.IsBlocked
newApp.IsAllowed = pa.IsAllowed
newApp.OnAcknowledgement = pa.OnAcknowledgement
newApp.OnAuth = pa.OnAuth
newApp.OnClose = pa.OnClose
newApp.OnConnecting = pa.OnConnecting
newApp.RetValHandler = pa.RetValHandler
return newApp
}
// Init is run when the connection is first started.
func (pa *PeerApp) Init(connection tapir.Connection) {
// First run the Authentication App
pa.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
pa.connection = connection
connection.SetCapability(cwtchCapability)
if pa.IsBlocked(connection.Hostname()) {
pa.connection.Close()
@ -75,39 +57,28 @@ func (pa *PeerApp) Init(connection tapir.Connection) {
go pa.listen()
}
} else {
// The auth protocol wasn't completed, we can safely shutdown the connection
connection.Close()
pa.OnClose(connection.Hostname())
}
}
func (pa *PeerApp) listen() {
func (pa PeerApp) listen() {
for {
message := pa.connection.Expect()
if len(message) == 0 {
log.Debugf("0 byte read, socket has likely failed. Closing the listen goroutine")
log.Errorf("0 byte read, socket has likely failed. Closing the listen goroutine")
pa.OnClose(pa.connection.Hostname())
return
}
var peerMessage PeerMessage
err := json.Unmarshal(message, &peerMessage)
if err == nil {
switch peerMessage.Context {
case event.ContextAck:
if peerMessage.Context == event.ContextAck {
pa.OnAcknowledgement(pa.connection.Hostname(), peerMessage.ID)
case event.ContextRetVal:
req, ok := pa.getValRequests.Load(peerMessage.ID)
if ok {
reqStr := []byte(req.(string))
pa.RetValHandler(pa.connection.Hostname(), reqStr, peerMessage.Data)
pa.getValRequests.Delete(peerMessage.ID)
}
default:
if pa.IsAllowed(pa.connection.Hostname()) {
pa.MessageHandler(pa.connection.Hostname(), peerMessage.ID, peerMessage.Context, peerMessage.Data)
} else {
pa.MessageHandler(pa.connection.Hostname(), peerMessage.Context, peerMessage.Data)
// Acknowledge the message
pa.SendMessage(PeerMessage{peerMessage.ID, event.ContextAck, []byte{}})
}
// Acknowledge the message
pa.SendMessage(PeerMessage{peerMessage.ID, event.ContextAck, []byte{}})
}
} else {
log.Errorf("Error unmarshalling PeerMessage package: %x %v", message, err)
@ -117,10 +88,7 @@ func (pa *PeerApp) listen() {
// SendMessage sends the peer a preformatted message
// NOTE: This is a stub, we will likely want to extend this to better reflect the desired protocol
func (pa *PeerApp) SendMessage(message PeerMessage) {
if message.Context == event.ContextGetVal {
pa.getValRequests.Store(message.ID, string(message.Data))
}
func (pa PeerApp) SendMessage(message PeerMessage) {
serialized, _ := json.Marshal(message)
pa.connection.Send(serialized)
}

View File

@ -0,0 +1,168 @@
package connections
import (
"crypto/rand"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/protocol/connections/fetch"
"cwtch.im/cwtch/protocol/connections/listen"
"cwtch.im/cwtch/protocol/connections/send"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/crypto/ed25519"
"sync"
"time"
)
// PeerServerConnection encapsulates a single Peer->Server connection
type PeerServerConnection struct {
connection.AutoConnectionHandler
Server string
stateMutex sync.Mutex
state ConnectionState
connection *connection.Connection
protocolEngine Engine
GroupMessageHandler func(string, *protocol.GroupMessage)
}
// NewPeerServerConnection creates a new Peer->Server outbound connection
func NewPeerServerConnection(engine Engine, serverhostname string) *PeerServerConnection {
psc := new(PeerServerConnection)
psc.protocolEngine = engine
psc.Server = serverhostname
psc.setState(DISCONNECTED)
psc.Init()
return psc
}
// GetState returns the current connection state
func (psc *PeerServerConnection) GetState() ConnectionState {
psc.stateMutex.Lock()
defer psc.stateMutex.Unlock()
return psc.state
}
func (psc *PeerServerConnection) setState(state ConnectionState) {
log.Debugf("Setting State to %v for %v\n", ConnectionStateName[state], psc.Server)
psc.stateMutex.Lock()
defer psc.stateMutex.Unlock()
psc.state = state
psc.protocolEngine.EventManager().Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: string(psc.Server),
event.ConnectionState: ConnectionStateName[state],
}))
}
// WaitTilSynced waits until the underlying connection is authenticated
func (psc *PeerServerConnection) WaitTilSynced() {
for {
if psc.GetState() == SYNCED {
break
}
time.Sleep(time.Second * 1)
}
}
// Run manages the setup and teardown of a peer server connection
func (psc *PeerServerConnection) Run() error {
log.Infof("Connecting to %v", psc.Server)
psc.setState(CONNECTING)
rc, err := goricochet.Open(psc.protocolEngine.ACN(), psc.Server)
if err == nil {
psc.connection = rc
if psc.GetState() == KILLED {
return nil
}
psc.setState(CONNECTED)
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err == nil {
_, err := connection.HandleOutboundConnection(psc.connection).ProcessAuthAsV3Client(identity.InitializeV3("cwtchpeer", &priv, &pub))
if err == nil {
if psc.GetState() == KILLED {
return nil
}
psc.setState(AUTHENTICATED)
go func() {
psc.connection.Do(func() error {
psc.connection.RequestOpenChannel("im.cwtch.server.fetch", &fetch.CwtchPeerFetchChannel{Handler: psc})
return nil
})
psc.connection.Do(func() error {
psc.connection.RequestOpenChannel("im.cwtch.server.listen", &listen.CwtchPeerListenChannel{Handler: psc})
return nil
})
}()
psc.connection.Process(psc)
}
}
}
psc.setState(FAILED)
return err
}
// Break makes Run() return and prevents processing, but doesn't close the connection.
func (psc *PeerServerConnection) Break() error {
return psc.connection.Break()
}
// SendGroupMessage sends the given protocol message to the Server.
func (psc *PeerServerConnection) SendGroupMessage(gm *protocol.GroupMessage) error {
if psc.state != SYNCED {
return errors.New("peer is not yet connected & authenticated & synced to server cannot send message")
}
err := psc.connection.Do(func() error {
psc.connection.RequestOpenChannel("im.cwtch.server.send", &send.CwtchPeerSendChannel{})
return nil
})
errCount := 0
for errCount < 5 {
time.Sleep(time.Second * time.Duration(errCount+1)) // back off retry
err = psc.connection.Do(func() error {
channel := psc.connection.Channel("im.cwtch.server.send", channels.Outbound)
if channel == nil {
return errors.New("no channel found")
}
sendchannel, ok := channel.Handler.(*send.CwtchPeerSendChannel)
if ok {
return sendchannel.SendGroupMessage(gm)
}
return errors.New("channel is not a peer send channel (this should definitely not happen)")
})
if err != nil {
errCount++
} else {
return nil
}
}
return err
}
// Close shuts down the connection (freeing the handler goroutines)
func (psc *PeerServerConnection) Close() {
psc.setState(KILLED)
if psc.connection != nil {
psc.connection.Close()
}
}
// HandleGroupMessage passes the given group message back to the profile.
func (psc *PeerServerConnection) HandleGroupMessage(gm *protocol.GroupMessage) {
psc.GroupMessageHandler(psc.Server, gm)
}
// HandleFetchDone calls the supplied callback for when a fetch connection is closed
func (psc *PeerServerConnection) HandleFetchDone() {
psc.setState(SYNCED)
}

View File

@ -0,0 +1,105 @@
package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/server/fetch"
"cwtch.im/cwtch/server/send"
"cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/libricochet-go"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/connection"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
identityOld "git.openprivacy.ca/openprivacy/libricochet-go/identity"
"golang.org/x/crypto/ed25519"
"net"
"testing"
"time"
)
func ServerAuthValid(hostname string, key ed25519.PublicKey) (allowed, known bool) {
return true, true
}
type TestServer struct {
connection.AutoConnectionHandler
Received chan bool
}
func (ts *TestServer) HandleGroupMessage(gm *protocol.GroupMessage) {
ts.Received <- true
}
func (ts *TestServer) HandleFetchRequest() []*protocol.GroupMessage {
return []*protocol.GroupMessage{{Ciphertext: []byte("hello"), Signature: []byte{}, Spamguard: []byte{}}, {Ciphertext: []byte("hello"), Signature: []byte{}, Spamguard: []byte{}}}
}
func runtestserver(t *testing.T, ts *TestServer, priv ed25519.PrivateKey, identity primitives.Identity, listenChan chan bool) {
ln, _ := net.Listen("tcp", "127.0.0.1:5451")
listenChan <- true
conn, _ := ln.Accept()
defer conn.Close()
rc, err := goricochet.NegotiateVersionInbound(conn)
if err != nil {
t.Errorf("Negotiate Version Error: %v", err)
}
// TODO switch from old identity to new tapir identity.
pub := identity.PublicKey()
err = connection.HandleInboundConnection(rc).ProcessAuthAsV3Server(identityOld.InitializeV3("", &priv, &pub), ServerAuthValid)
if err != nil {
t.Errorf("ServerAuth Error: %v", err)
}
ts.RegisterChannelHandler("im.cwtch.server.send", func() channels.Handler {
server := new(send.CwtchServerSendChannel)
server.Handler = ts
return server
})
ts.RegisterChannelHandler("im.cwtch.server.fetch", func() channels.Handler {
server := new(fetch.CwtchServerFetchChannel)
server.Handler = ts
return server
})
rc.Process(ts)
}
func TestPeerServerConnection(t *testing.T) {
identity, priv := primitives.InitializeEphemeralIdentity()
t.Logf("Launching Server....\n")
ts := new(TestServer)
ts.Init()
ts.Received = make(chan bool)
listenChan := make(chan bool)
go runtestserver(t, ts, priv, identity, listenChan)
<-listenChan
onionAddr := identity.Hostname()
manager := event.NewEventManager()
engine := NewProtocolEngine(identity, priv, connectivity.LocalProvider(), manager, nil, nil)
psc := NewPeerServerConnection(engine, "127.0.0.1:5451|"+onionAddr)
numcalls := 0
psc.GroupMessageHandler = func(s string, gm *protocol.GroupMessage) {
numcalls++
}
state := psc.GetState()
if state != DISCONNECTED {
t.Errorf("new connections should start in disconnected state")
}
time.Sleep(time.Second * 1)
go psc.Run()
psc.WaitTilSynced()
gm := &protocol.GroupMessage{Ciphertext: []byte("hello"), Signature: []byte{}}
psc.SendGroupMessage(gm)
// Wait until message is received
<-ts.Received
if numcalls != 2 {
t.Errorf("Should have received 2 calls from fetch request, instead received %v", numcalls)
}
}

View File

@ -0,0 +1,95 @@
package send
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/protocol/connections/spam"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
)
// CwtchPeerSendChannel is the peer implementation of im.cwtch.server.send
type CwtchPeerSendChannel struct {
channel *channels.Channel
spamGuard spam.Guard
challenge []byte
}
// Type returns the type string for this channel, e.g. "im.ricochet.server.send".
func (cpsc *CwtchPeerSendChannel) Type() string {
return "im.cwtch.server.send"
}
// Closed is called when the channel is closed for any reason.
func (cpsc *CwtchPeerSendChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch server channels only peers may open.
func (cpsc *CwtchPeerSendChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cpsc *CwtchPeerSendChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cpsc *CwtchPeerSendChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require no auth
func (cpsc *CwtchPeerSendChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound should never be called on peers.
func (cpsc *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 used to set up a new send channel and initialize spamguard
func (cpsc *CwtchPeerSendChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cpsc.spamGuard.Difficulty = 2
cpsc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cpsc.Type()), nil
}
// OpenOutboundResult confirms the open channel request and sets the spamguard challenge
func (cpsc *CwtchPeerSendChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
ce, _ := proto.GetExtension(crm, protocol.E_ServerNonce)
cpsc.challenge = ce.([]byte)[:]
cpsc.channel.Pending = false
}
}
}
// SendGroupMessage performs the spamguard proof of work and sends a message.
func (cpsc *CwtchPeerSendChannel) SendGroupMessage(gm *protocol.GroupMessage) error {
if cpsc.channel.Pending == false {
sgsolve := cpsc.spamGuard.SolveChallenge(cpsc.challenge, gm.GetCiphertext())
gm.Spamguard = sgsolve[:]
csp := &protocol.CwtchServerPacket{
GroupMessage: gm,
}
packet, _ := proto.Marshal(csp)
cpsc.channel.SendMessage(packet)
cpsc.channel.CloseChannel()
} else {
return errors.New("channel isn't set up yet")
}
return nil
}
// Packet should never be
func (cpsc *CwtchPeerSendChannel) Packet(data []byte) {
// If we receive a packet on this channel, close the connection
cpsc.channel.CloseChannel()
}

View File

@ -0,0 +1,108 @@
package send
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/protocol/connections/spam"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
)
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.Guard
sg.Difficulty = 2
closed := false
channel.CloseChannel = func() {
closed = true
}
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")
}
if !closed {
t.Errorf("send channel should have successfully closed after a valid group message")
}
pfc.Closed(nil)
}

View File

@ -0,0 +1,100 @@
package spam
import (
"crypto/rand"
"crypto/sha256"
"cwtch.im/cwtch/protocol"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"io"
//"fmt"
)
// Guard implements a spam protection mechanism for Cwtch Servers.
type Guard struct {
Difficulty int
nonce [24]byte
}
func getRandomness(arr *[24]byte) {
if _, err := io.ReadFull(rand.Reader, arr[:]); err != nil {
utils.CheckError(err)
}
}
//GenerateChallenge returns a channel result packet with a spamguard challenge nonce
func (sg *Guard) GenerateChallenge(channelID int32) []byte {
cr := &Protocol_Data_Control.ChannelResult{
ChannelIdentifier: proto.Int32(channelID),
Opened: proto.Bool(true),
}
var nonce [24]byte
getRandomness(&nonce)
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
}
// SolveChallenge takes in a challenge and a message and returns a solution
// The solution is a 24 byte nonce which when hashed with the challenge and the message
// produces a sha256 hash with Difficulty leading 0s
func (sg *Guard) SolveChallenge(challenge []byte, message []byte) []byte {
solved := false
var spamguard [24]byte
var sum [32]byte
solve := make([]byte, len(challenge)+len(message)+len(spamguard))
for !solved {
getRandomness(&spamguard)
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
}
}
}
//fmt.Printf("[SOLVED] %x\n",sha256.Sum256(solve))
return spamguard[:]
}
// ValidateChallenge returns true if the message and spamguard pass the challenge
func (sg *Guard) ValidateChallenge(message []byte, spamguard []byte) bool {
if len(spamguard) != 24 {
return false
}
// If the message is too large just throw it away.
if len(message) > 2048 {
return false
}
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)
for i := 0; i < sg.Difficulty; i++ {
if sum[i] != 0x00 {
return false
}
}
return true
}

View File

@ -0,0 +1,69 @@
package spam
import (
"cwtch.im/cwtch/protocol"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"testing"
)
func TestSpamGuard(t *testing.T) {
var spamGuard Guard
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 Guard Failed")
}
return
}
t.Errorf("Failed SpamGaurd")
}
func TestSpamGuardBadLength(t *testing.T) {
var spamGuard Guard
spamGuard.Difficulty = 2
spamGuard.GenerateChallenge(3)
result := spamGuard.ValidateChallenge([]byte("test"), []byte{0x00, 0x00})
if result {
t.Errorf("Validating Guard should have failed")
}
}
func TestSpamGuardFail(t *testing.T) {
var spamGuard Guard
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)[:]
var spamGuard2 Guard
spamGuard2.Difficulty = 1
sgsolve := spamGuard2.SolveChallenge(challenge, []byte("Hello"))
t.Logf("Solved: %v %v", challenge, sgsolve)
result := spamGuard.ValidateChallenge([]byte("Hello"), sgsolve)
if result {
t.Errorf("Validating Guard successes")
}
return
}
t.Errorf("Failed SpamGaurd")
}

View File

@ -22,10 +22,8 @@ const (
var (
// ConnectionStateName allows conversion of states to their string representations
ConnectionStateName = []string{"Disconnected", "Connecting", "Connected", "Authenticated", "Synced", "Failed", "Killed"}
)
// ConnectionStateToType allows conversion of strings to their state type
func ConnectionStateToType() map[string]ConnectionState {
return map[string]ConnectionState{"Disconnected": DISCONNECTED, "Connecting": CONNECTING,
// ConnectionStateToType allows conversion of strings to their state type
ConnectionStateToType = map[string]ConnectionState{"Disconnected": DISCONNECTED, "Connecting": CONNECTING,
"Connected": CONNECTED, "Authenticated": AUTHENTICATED, "Synced": SYNCED, "Failed": FAILED, "Killed": KILLED}
}
)

View File

@ -1,209 +0,0 @@
package connections
import (
"cwtch.im/cwtch/protocol/groups"
"encoding/json"
"errors"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"sync"
)
// NewTokenBoardClient generates a new Client for Token Board
func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenServiceOnion string, lastKnownSignature []byte, groupMessageHandler func(server string, gm *groups.EncryptedGroupMessage), serverSyncedHandler func(server string), serverClosedHandler func(server string)) tapir.Application {
tba := new(TokenBoardClient)
tba.acn = acn
tba.tokenService = privacypass.NewTokenServer()
tba.tokenService.Y = Y
tba.tokenServiceOnion = tokenServiceOnion
tba.receiveGroupMessageHandler = groupMessageHandler
tba.serverSyncedHandler = serverSyncedHandler
tba.serverClosedHandler = serverClosedHandler
tba.lastKnownSignature = lastKnownSignature
return tba
}
// TokenBoardClient defines a client for the TokenBoard server
type TokenBoardClient struct {
applications.AuthApp
connection tapir.Connection
receiveGroupMessageHandler func(server string, gm *groups.EncryptedGroupMessage)
serverSyncedHandler func(server string)
serverClosedHandler func(server string)
// Token service handling
acn connectivity.ACN
tokens []*privacypass.Token
tokenLock sync.Mutex
tokenService *privacypass.TokenServer
tokenServiceOnion string
lastKnownSignature []byte
}
// NewInstance Client a new TokenBoardApp
func (ta *TokenBoardClient) NewInstance() tapir.Application {
tba := new(TokenBoardClient)
tba.serverSyncedHandler = ta.serverSyncedHandler
tba.serverClosedHandler = ta.serverClosedHandler
tba.receiveGroupMessageHandler = ta.receiveGroupMessageHandler
tba.acn = ta.acn
tba.tokenService = ta.tokenService
tba.tokenServiceOnion = ta.tokenServiceOnion
tba.lastKnownSignature = ta.lastKnownSignature
return tba
}
// Init initializes the cryptographic TokenBoardApp
func (ta *TokenBoardClient) Init(connection tapir.Connection) {
ta.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
ta.connection = connection
ta.connection.SetCapability(groups.CwtchServerSyncedCapability)
log.Debugf("Successfully Initialized Connection")
go ta.Listen()
// Optimistically acquire many tokens for this server...
go ta.MakePayment()
go ta.MakePayment()
ta.Replay()
} else {
connection.Close()
}
}
// Listen processes the messages for this application
func (ta *TokenBoardClient) Listen() {
for {
log.Debugf("Client waiting...")
data := ta.connection.Expect()
if len(data) == 0 {
log.Debugf("Server closed the connection...")
ta.serverClosedHandler(ta.connection.Hostname())
return // connection is closed
}
// We always expect the server to follow protocol, and the second it doesn't we close the connection
var message groups.Message
if err := json.Unmarshal(data, &message); err != nil {
log.Debugf("Server sent an unexpected message, closing the connection: %v", err)
ta.serverClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
switch message.MessageType {
case groups.NewMessageMessage:
if message.NewMessage != nil {
ta.receiveGroupMessageHandler(ta.connection.Hostname(), &message.NewMessage.EGM)
} else {
log.Debugf("Server sent an unexpected NewMessage, closing the connection: %s", data)
ta.serverClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
case groups.PostResultMessage:
// TODO handle failure
case groups.ReplayResultMessage:
if message.ReplayResult != nil {
log.Debugf("Replaying %v Messages...", message.ReplayResult.NumMessages)
for i := 0; i < message.ReplayResult.NumMessages; i++ {
data := ta.connection.Expect()
if len(data) == 0 {
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection")
ta.serverClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
egm := &groups.EncryptedGroupMessage{}
if err := json.Unmarshal(data, egm); err == nil {
ta.receiveGroupMessageHandler(ta.connection.Hostname(), egm)
ta.lastKnownSignature = egm.Signature
} else {
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection: %v", err)
ta.serverClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
}
ta.serverSyncedHandler(ta.connection.Hostname())
}
}
}
}
// Replay posts a Replay Message to the server.
func (ta *TokenBoardClient) Replay() {
data, _ := json.Marshal(groups.Message{MessageType: groups.ReplayRequestMessage, ReplayRequest: &groups.ReplayRequest{LastCommit: ta.lastKnownSignature}})
ta.connection.Send(data)
}
// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler)
func (ta *TokenBoardClient) PurchaseTokens() {
ta.MakePayment()
}
// Post sends a Post Request to the server
func (ta *TokenBoardClient) Post(ct []byte, sig []byte) (bool, int) {
egm := groups.EncryptedGroupMessage{Ciphertext: ct, Signature: sig}
token, numTokens, err := ta.NextToken(egm.ToBytes(), ta.connection.Hostname())
if err == nil {
data, _ := json.Marshal(groups.Message{MessageType: groups.PostRequestMessage, PostRequest: &groups.PostRequest{EGM: egm, Token: token}})
log.Debugf("Message Length: %s %v", data, len(data))
ta.connection.Send(data)
return true, numTokens
}
log.Debugf("No Valid Tokens: %v", err)
return false, numTokens
}
// MakePayment uses the PoW based token protocol to obtain more tokens
func (ta *TokenBoardClient) MakePayment() error {
log.Debugf("Making a Payment %v", ta)
id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService)
client.Init(ta.acn, sk, &id)
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = ta.tokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
client.Connect(ta.tokenServiceOnion, powTokenApp)
log.Debugf("Waiting for successful PoW Auth...")
conn, err := client.WaitForCapabilityOrClose(ta.tokenServiceOnion, applications.HasTokensCapability)
if err == nil {
powtapp, _ := conn.App().(*applications.TokenApplication)
// Update tokens...we need a lock here to prevent SpendToken from modifying the tokens
// during this process..
log.Debugf("Updating Tokens")
ta.tokenLock.Lock()
ta.tokens = append(ta.tokens, powtapp.Tokens...)
ta.tokenLock.Unlock()
log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
conn.Close()
return nil
}
log.Debugf("Error making payment: to %v %v", ta.tokenServiceOnion, err)
return err
}
// NextToken retrieves the next token
func (ta *TokenBoardClient) NextToken(data []byte, hostname string) (privacypass.SpentToken, int, error) {
// Taken the first new token, we need a lock here because tokens can be appended by MakePayment
// which could result in weird behaviour...
ta.tokenLock.Lock()
defer ta.tokenLock.Unlock()
if len(ta.tokens) == 0 {
return privacypass.SpentToken{}, len(ta.tokens), errors.New("no more tokens")
}
token := ta.tokens[0]
ta.tokens = ta.tokens[1:]
return token.SpendToken(append(data, hostname...)), len(ta.tokens), nil
}

View File

@ -0,0 +1,135 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: cwtch-profile.proto
/*
Package protocol is a generated protocol buffer package.
It is generated from these files:
cwtch-profile.proto
It has these top-level messages:
CwtchPeerPacket
CwtchIdentity
GroupChatInvite
*/
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
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
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 fileDescriptor0, []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 fileDescriptor0, []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"`
SignedGroupId []byte `protobuf:"bytes,4,opt,name=signed_group_id,json=signedGroupId,proto3" json:"signed_group_id,omitempty"`
InitialMessage []byte `protobuf:"bytes,5,opt,name=initial_message,json=initialMessage,proto3" json:"initial_message,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 fileDescriptor0, []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 (m *GroupChatInvite) GetSignedGroupId() []byte {
if m != nil {
return m.SignedGroupId
}
return nil
}
func (m *GroupChatInvite) GetInitialMessage() []byte {
if m != nil {
return m.InitialMessage
}
return nil
}
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", fileDescriptor0) }

View File

@ -0,0 +1,21 @@
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;
bytes signed_group_id = 4;
bytes initial_message = 5;
}

View File

@ -0,0 +1,38 @@
package groups
import (
"crypto/rand"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/peer"
"golang.org/x/crypto/nacl/secretbox"
"io"
)
// Fuzz various group related functions
func Fuzz(data []byte) int {
peer := peer.NewCwtchPeer("fuzz")
peer.Init(event.NewEventManager())
inviteid, err := peer.GetProfile().ProcessInvite(string(data), peer.GetProfile().Onion)
if err != nil {
if inviteid != "" {
panic("should not have added a group on err")
}
return 1
}
id, _, _ := peer.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
var nonce [24]byte
io.ReadFull(rand.Reader, nonce[:])
encrypted := secretbox.Seal(nonce[:], data, &nonce, &peer.GetGroup(id).GroupKey)
ok, _, _, _ := peer.GetProfile().AttemptDecryption(encrypted, data)
if ok {
panic("this probably shouldn't happen")
}
ok = peer.GetProfile().VerifyGroupMessage(string(data), string(data), string(data), 0, encrypted, data)
if ok {
panic("this probably shouldn't happen")
}
return 0
}

View File

@ -0,0 +1,20 @@
package invites
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/peer"
)
// Fuzz import group function
func Fuzz(data []byte) int {
peer := peer.NewCwtchPeer("fuzz")
peer.Init(event.NewEventManager())
err := peer.ImportGroup(string(data))
if err != nil {
if len(peer.GetGroups()) > 0 {
panic("group added despite error")
}
return 0
}
return 1
}

View File

@ -0,0 +1,188 @@
// 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 control "git.openprivacy.ca/openprivacy/libricochet-go/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"`
XXX_unrecognized []byte `json:"-"`
}
func (m *CwtchServerPacket) Reset() { *m = CwtchServerPacket{} }
func (m *CwtchServerPacket) String() string { return proto.CompactTextString(m) }
func (*CwtchServerPacket) ProtoMessage() {}
func (*CwtchServerPacket) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} }
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
}
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() {}
func (*FetchMessage) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} }
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"`
Signature []byte `protobuf:"bytes,3,req,name=signature" json:"signature,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *GroupMessage) Reset() { *m = GroupMessage{} }
func (m *GroupMessage) String() string { return proto.CompactTextString(m) }
func (*GroupMessage) ProtoMessage() {}
func (*GroupMessage) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{2} }
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
}
func (m *GroupMessage) GetSignature() []byte {
if m != nil {
return m.Signature
}
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"`
Timestamp *int32 `protobuf:"varint,2,req,name=timestamp" json:"timestamp,omitempty"`
Text *string `protobuf:"bytes,3,req,name=text" json:"text,omitempty"`
SignedGroupId []byte `protobuf:"bytes,4,req,name=signed_group_id,json=signedGroupId" json:"signed_group_id,omitempty"`
PreviousMessageSig []byte `protobuf:"bytes,5,req,name=previous_message_sig,json=previousMessageSig" json:"previous_message_sig,omitempty"`
// Used to prevent analysis on text length, length is 1024 - len(text)
Padding []byte `protobuf:"bytes,6,req,name=padding" json:"padding,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *DecryptedGroupMessage) Reset() { *m = DecryptedGroupMessage{} }
func (m *DecryptedGroupMessage) String() string { return proto.CompactTextString(m) }
func (*DecryptedGroupMessage) ProtoMessage() {}
func (*DecryptedGroupMessage) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{3} }
func (m *DecryptedGroupMessage) GetOnion() string {
if m != nil && m.Onion != nil {
return *m.Onion
}
return ""
}
func (m *DecryptedGroupMessage) GetTimestamp() int32 {
if m != nil && m.Timestamp != nil {
return *m.Timestamp
}
return 0
}
func (m *DecryptedGroupMessage) GetText() string {
if m != nil && m.Text != nil {
return *m.Text
}
return ""
}
func (m *DecryptedGroupMessage) GetSignedGroupId() []byte {
if m != nil {
return m.SignedGroupId
}
return nil
}
func (m *DecryptedGroupMessage) GetPreviousMessageSig() []byte {
if m != nil {
return m.PreviousMessageSig
}
return nil
}
func (m *DecryptedGroupMessage) GetPadding() []byte {
if m != nil {
return m.Padding
}
return nil
}
var E_ServerNonce = &proto.ExtensionDesc{
ExtendedType: (*control.ChannelResult)(nil),
ExtensionType: ([]byte)(nil),
Field: 8200,
Name: "protocol.server_nonce",
Tag: "bytes,8200,opt,name=server_nonce,json=serverNonce",
Filename: "group_message.proto",
}
func init() {
proto.RegisterType((*CwtchServerPacket)(nil), "protocol.CwtchServerPacket")
proto.RegisterType((*FetchMessage)(nil), "protocol.FetchMessage")
proto.RegisterType((*GroupMessage)(nil), "protocol.GroupMessage")
proto.RegisterType((*DecryptedGroupMessage)(nil), "protocol.DecryptedGroupMessage")
proto.RegisterExtension(E_ServerNonce)
}
func init() { proto.RegisterFile("group_message.proto", fileDescriptor2) }
var fileDescriptor2 = []byte{
// 360 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x92, 0x4d, 0x6e, 0xdb, 0x30,
0x10, 0x85, 0x21, 0xff, 0xb4, 0xf5, 0x58, 0x6e, 0x51, 0xd6, 0x6d, 0x89, 0xa2, 0x28, 0x0c, 0x2d,
0x0a, 0xaf, 0x8c, 0x20, 0xcb, 0x78, 0x13, 0xc0, 0x41, 0x82, 0x2c, 0x12, 0x04, 0xf2, 0x01, 0x04,
0x42, 0x1a, 0x53, 0x4c, 0x24, 0x92, 0x20, 0x29, 0x27, 0xb9, 0x41, 0x36, 0x39, 0x5d, 0x2e, 0x14,
0x88, 0xb2, 0x6c, 0x39, 0x2b, 0x69, 0xde, 0x37, 0x6f, 0xde, 0x80, 0x24, 0xfc, 0xe0, 0x46, 0x55,
0x3a, 0x29, 0xd1, 0x5a, 0xc6, 0x71, 0xa1, 0x8d, 0x72, 0x8a, 0x7c, 0xf1, 0x9f, 0x54, 0x15, 0x7f,
0xa6, 0x2b, 0x25, 0x9d, 0x51, 0xc5, 0x2a, 0x67, 0x52, 0x62, 0xd1, 0xf0, 0xe8, 0x35, 0x80, 0xef,
0xab, 0x47, 0x97, 0xe6, 0x6b, 0x34, 0x5b, 0x34, 0x77, 0x2c, 0x7d, 0x40, 0x47, 0x96, 0x30, 0x39,
0x1a, 0x46, 0x83, 0x59, 0x30, 0x1f, 0x9f, 0xfe, 0x5a, 0xb4, 0xd3, 0x16, 0x57, 0x35, 0xbe, 0x69,
0x68, 0x1c, 0xf2, 0x4e, 0x55, 0x9b, 0x37, 0xe8, 0xd2, 0x7c, 0x6f, 0xee, 0x7d, 0x34, 0x5f, 0xd6,
0x78, 0x6f, 0xde, 0x74, 0xaa, 0xe8, 0x2b, 0x84, 0x5d, 0x1a, 0xdd, 0x43, 0xd8, 0x8d, 0x22, 0xff,
0x00, 0x52, 0xa1, 0x73, 0x34, 0x0e, 0x9f, 0x1c, 0x0d, 0x66, 0xbd, 0x79, 0x18, 0x77, 0x14, 0xf2,
0x17, 0x46, 0x56, 0xb3, 0x92, 0x57, 0xcc, 0x64, 0xb4, 0xe7, 0xf1, 0x41, 0xf0, 0x54, 0x70, 0xc9,
0x5c, 0x65, 0x90, 0xf6, 0x77, 0xb4, 0x15, 0xa2, 0xb7, 0x00, 0x7e, 0x5e, 0x60, 0x6a, 0x9e, 0xb5,
0xc3, 0xec, 0x28, 0x75, 0x0a, 0x43, 0x25, 0x85, 0x92, 0x3e, 0x70, 0x14, 0x37, 0x45, 0x3d, 0xcd,
0x89, 0x12, 0xad, 0x63, 0xa5, 0xf6, 0x59, 0xc3, 0xf8, 0x20, 0x10, 0x02, 0x03, 0xbf, 0x63, 0xdf,
0x5b, 0xfc, 0x3f, 0xf9, 0x0f, 0xdf, 0xea, 0x38, 0xcc, 0x92, 0xe6, 0x78, 0x45, 0x46, 0x07, 0x7e,
0x8b, 0x49, 0x23, 0xfb, 0xd0, 0xeb, 0x8c, 0x9c, 0xc0, 0x54, 0x1b, 0xdc, 0x0a, 0x55, 0xd9, 0xf6,
0x14, 0x13, 0x2b, 0x38, 0x1d, 0xfa, 0x66, 0xd2, 0xb2, 0xdd, 0x7a, 0x6b, 0xc1, 0x09, 0x85, 0xcf,
0x9a, 0x65, 0x99, 0x90, 0x9c, 0x7e, 0xf2, 0x4d, 0x6d, 0x79, 0xb6, 0x84, 0xd0, 0xfa, 0xbb, 0x4d,
0xa4, 0x92, 0x29, 0x92, 0xdf, 0x87, 0x7b, 0xd8, 0x3d, 0x85, 0x18, 0x6d, 0x55, 0x38, 0xfa, 0x72,
0x3e, 0x0b, 0xe6, 0x61, 0x3c, 0x6e, 0xba, 0x6f, 0xeb, 0xe6, 0xf7, 0x00, 0x00, 0x00, 0xff, 0xff,
0x33, 0x30, 0xca, 0x8b, 0x54, 0x02, 0x00, 0x00,
}

View File

@ -0,0 +1,35 @@
syntax = "proto2";
package protocol;
import "ControlChannel.proto";
message CwtchServerPacket {
optional GroupMessage group_message = 1;
optional FetchMessage fetch_message = 2;
}
extend protocol.ChannelResult {
optional bytes server_nonce = 8200; // 32 random bytes
}
message FetchMessage {
}
message GroupMessage {
required bytes ciphertext = 1;
required bytes spamguard = 2;
required bytes signature = 3;
}
// 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 signed_group_id = 4;
required bytes previous_message_sig =5;
// Used to prevent analysis on text length, length is 1024 - len(text)
required bytes padding = 6;
}

View File

@ -1,95 +0,0 @@
package groups
import (
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
)
// CwtchServerSyncedCapability is used to indicate that a given cwtch server is synced
const CwtchServerSyncedCapability = tapir.Capability("CwtchServerSyncedCapability")
// GroupInvite provides a structured type for communicating group information to peers
type GroupInvite struct {
GroupID string
GroupName string
SignedGroupID []byte
Timestamp uint64
SharedKey []byte
ServerHost string
}
// DecryptedGroupMessage is the main encapsulation of group message data
type DecryptedGroupMessage struct {
Text string
Onion string
Timestamp uint64
// NOTE: SignedGroupID is now a misnomer, the only way this is signed is indirectly via the signed encrypted group messages
// We now treat GroupID as binding to a server/key rather than an "owner" - additional validation logic (to e.g.
// respect particular group constitutions) can be built on top of group messages, but the underlying groups are
// now agnostic to those models.
SignedGroupID []byte
PreviousMessageSig []byte
Padding []byte
}
// EncryptedGroupMessage provides an encapsulation of the encrypted group message stored on the server
type EncryptedGroupMessage struct {
Ciphertext []byte
Signature []byte
}
// ToBytes converts the encrypted group message to a set of bytes for serialization
func (egm EncryptedGroupMessage) ToBytes() []byte {
data, _ := json.Marshal(egm)
return data
}
// MessageType defines the enum for TokenBoard messages
type MessageType int
// Message Types
const (
ReplayRequestMessage MessageType = iota
ReplayResultMessage
PostRequestMessage
PostResultMessage
NewMessageMessage
)
// Message encapsulates the application protocol
type Message struct {
MessageType MessageType
PostRequest *PostRequest `json:",omitempty"`
PostResult *PostResult `json:",omitempty"`
NewMessage *NewMessage `json:",omitempty"`
ReplayRequest *ReplayRequest `json:",omitempty"`
ReplayResult *ReplayResult `json:",omitempty"`
}
// ReplayRequest requests a reply from the given Commit
type ReplayRequest struct {
LastCommit []byte
}
// PostRequest requests to post the message to the board with the given token
type PostRequest struct {
Token privacypass.SpentToken
EGM EncryptedGroupMessage
}
// PostResult returns the success of a given post attempt
type PostResult struct {
Success bool
}
// ReplayResult is sent by the server before a stream of replayed messages
type ReplayResult struct {
NumMessages int
}
// NewMessage is used to send a new bulletin board message to interested peers.
type NewMessage struct {
//Token privacypass.SpentToken
EGM EncryptedGroupMessage
}

View File

@ -1,18 +1,11 @@
package main
import (
"crypto/rand"
"cwtch.im/cwtch/model"
cwtchserver "cwtch.im/cwtch/server"
"encoding/base64"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
mrand "math/rand"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
"os/signal"
"syscall"
"time"
"path"
)
const (
@ -22,44 +15,11 @@ const (
func main() {
log.AddEverythingFromPattern("server/app/main")
log.AddEverythingFromPattern("server/server")
log.SetLevel(log.LevelDebug)
configDir := os.Getenv("CWTCH_CONFIG_DIR")
if len(os.Args) == 2 && os.Args[1] == "gen1" {
config := new(cwtchserver.Config)
id, pk := primitives.InitializeEphemeralIdentity()
tid, tpk := primitives.InitializeEphemeralIdentity()
config.PrivateKey = pk
config.PublicKey = id.PublicKey()
config.TokenServerPrivateKey = tpk
config.TokenServerPublicKey = tid.PublicKey()
config.MaxBufferLines = 100000
config.ServerReporting = cwtchserver.Reporting{
LogMetricsToFile: true,
ReportingGroupID: "",
ReportingServerAddr: "",
}
config.Save(".", "serverConfig.json")
return
}
serverConfig := cwtchserver.LoadConfig(configDir, serverConfigFile)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
_, err := rand.Read(key)
if err != nil {
panic(err)
}
os.MkdirAll("tordir/tor", 0700)
tor.NewTorrc().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("./tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("tordir", "", controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
acn, err := connectivity.StartTor(path.Join(configDir, "tor"), "")
if err != nil {
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)
@ -69,32 +29,7 @@ func main() {
server := new(cwtchserver.Server)
log.Infoln("starting cwtch server...")
server.Setup(serverConfig)
// TODO create a random group for testing
group, _ := model.NewGroup(tor.GetTorV3Hostname(serverConfig.PublicKey))
invite, err := group.Invite()
if err != nil {
panic(err)
}
bundle := server.KeyBundle().Serialize()
log.Infof("Server Config: server:%s", base64.StdEncoding.EncodeToString(bundle))
log.Infof("Server Tofu Bundle: tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString(bundle), invite)
// Graceful Shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
acn.Close()
server.Close()
os.Exit(1)
}()
server.Run(acn)
for {
time.Sleep(time.Second)
}
// TODO load params from .cwtch/server.conf or command line flag
// TODO: respond to HUP so t.Close is gracefully called
server.Run(acn, serverConfig)
}

View File

@ -0,0 +1,100 @@
package fetch
import (
"cwtch.im/cwtch/protocol"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
)
// CwtchServerFetchChannel implements the ChannelHandler interface for a channel of
// type "im.cwtch.server.fetch" - this implementation only handles server side logic.
type CwtchServerFetchChannel struct {
Handler CwtchServerFetchHandler
channel *channels.Channel
}
// CwtchServerFetchHandler defines the interface for interacting with this 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
}
// SendGroupMessages sends a series of group messages to the client.
func (cc *CwtchServerFetchChannel) SendGroupMessages(gms []*protocol.GroupMessage) {
for _, gm := range gms {
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 *CwtchServerFetchChannel) Packet(data []byte) {
csp := &protocol.CwtchServerPacket{}
err := proto.Unmarshal(data, csp)
if err == nil {
if csp.GetFetchMessage() != nil {
cc.SendGroupMessages(cc.Handler.HandleFetchRequest())
}
}
// If we receive a packet on this channel, close the connection
cc.channel.CloseChannel()
}

View File

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

View File

@ -0,0 +1,85 @@
package listen
import (
"cwtch.im/cwtch/protocol"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
)
// CwtchServerListenChannel implements the ChannelHandler interface for a channel of
// type "im.cwtch.server.listen" - this implementation only handles server side logic.
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 sends a single group message to the peer
func (cc *CwtchServerListenChannel) SendGroupMessage(gm *protocol.GroupMessage) {
csp := &protocol.CwtchServerPacket{
GroupMessage: gm,
}
packet, _ := proto.Marshal(csp)
cc.channel.SendMessage(packet)
}
// Packet is called for each raw packet received on this channel.
func (cc *CwtchServerListenChannel) Packet(data []byte) {
// If we receive a packet on this channel, close the connection
cc.channel.CloseChannel()
}

View File

@ -0,0 +1,94 @@
package listen
import (
"cwtch.im/cwtch/protocol"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"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"),
Signature: []byte{},
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)
}
}

View File

@ -12,7 +12,6 @@ import (
type counter struct {
startTime time.Time
count uint64
total uint64
}
// Counter providers a threadsafe counter to use for storing long running counts
@ -26,7 +25,7 @@ type Counter interface {
// NewCounter initializes a counter starting at time.Now() and a count of 0 and returns it
func NewCounter() Counter {
c := &counter{startTime: time.Now(), count: 0, total: 0}
c := &counter{startTime: time.Now(), count: 0}
return c
}
@ -152,13 +151,11 @@ func (mh *monitorHistory) Months() []float64 {
}
func (mh *monitorHistory) Report(w *bufio.Writer) {
mh.lock.Lock()
fmt.Fprintln(w, "Minutes:", reportLine(mh.monitorType, mh.perMinutePerHour[:]))
fmt.Fprintln(w, "Hours: ", reportLine(mh.monitorType, mh.perHourForDay[:]))
fmt.Fprintln(w, "Days: ", reportLine(mh.monitorType, mh.perDayForWeek[:]))
fmt.Fprintln(w, "Weeks: ", reportLine(mh.monitorType, mh.perWeekForMonth[:]))
fmt.Fprintln(w, "Months: ", reportLine(mh.monitorType, mh.perMonthForYear[:]))
mh.lock.Unlock()
}
func reportLine(t MonitorType, array []float64) string {

View File

@ -21,7 +21,7 @@ func TestCounter(t *testing.T) {
}
for i := 0; i < max; i++ {
<-done
<- done
}
val := c.Count()
@ -32,6 +32,6 @@ func TestCounter(t *testing.T) {
counterStart := c.GetStarttime()
if counterStart.Sub(starttime) > time.Millisecond {
t.Errorf("counter's starttime was innaccurate %v", counterStart.Sub(starttime))
t.Error("counter's starttime was innaccurate")
}
}

View File

@ -3,12 +3,11 @@ package metrics
import (
"bufio"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"github.com/struCoder/pidusage"
"os"
"path"
"sync"
"time"
)
@ -18,51 +17,28 @@ const (
// Monitors is a package of metrics for a Cwtch Server including message count, CPU, Mem, and conns
type Monitors struct {
MessageCounter Counter
TotalMessageCounter Counter
Messages MonitorHistory
CPU MonitorHistory
Memory MonitorHistory
ClientConns MonitorHistory
starttime time.Time
breakChannel chan bool
log bool
configDir string
MessageCounter Counter
Messages MonitorHistory
CPU MonitorHistory
Memory MonitorHistory
ClientConns MonitorHistory
starttime time.Time
breakChannel chan bool
log bool
configDir string
}
// Start initializes a Monitors's monitors
func (mp *Monitors) Start(ts tapir.Service, configDir string, log bool) {
func (mp *Monitors) Start(ra *application.RicochetApplication, configDir string, log bool) {
mp.log = log
mp.configDir = configDir
mp.starttime = time.Now()
mp.breakChannel = make(chan bool)
mp.MessageCounter = NewCounter()
// Maintain a count of total messages
mp.TotalMessageCounter = NewCounter()
mp.Messages = NewMonitorHistory(Count, Cumulative, func() (c float64) {
c = float64(mp.MessageCounter.Count())
mp.TotalMessageCounter.Add(int(c))
mp.MessageCounter.Reset()
return
})
var pidUsageLock sync.Mutex
mp.CPU = NewMonitorHistory(Percent, Average, func() float64 {
pidUsageLock.Lock()
defer pidUsageLock.Unlock()
sysInfo, _ := pidusage.GetStat(os.Getpid())
return float64(sysInfo.CPU)
})
mp.Memory = NewMonitorHistory(MegaBytes, Average, func() float64 {
pidUsageLock.Lock()
defer pidUsageLock.Unlock()
sysInfo, _ := pidusage.GetStat(os.Getpid())
return float64(sysInfo.Memory)
})
// TODO: replace with ts.
mp.ClientConns = NewMonitorHistory(Count, Average, func() float64 { return float64(ts.Metrics().ConnectionCount) })
mp.Messages = NewMonitorHistory(Count, Cumulative, func() (c float64) { c = float64(mp.MessageCounter.Count()); mp.MessageCounter.Reset(); return })
mp.CPU = NewMonitorHistory(Percent, Average, func() float64 { sysInfo, _ := pidusage.GetStat(os.Getpid()); return float64(sysInfo.CPU) })
mp.Memory = NewMonitorHistory(MegaBytes, Average, func() float64 { sysInfo, _ := pidusage.GetStat(os.Getpid()); return float64(sysInfo.Memory) })
mp.ClientConns = NewMonitorHistory(Count, Average, func() float64 { return float64(ra.ConnectionCount()) })
if mp.log {
go mp.run()

View File

@ -0,0 +1,99 @@
package send
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/protocol/connections/spam"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
)
// CwtchServerSendChannel implements the ChannelHandler interface for a channel of
// type "im.cwtch.server.send - this implementation only handles server-side logic.
type CwtchServerSendChannel struct {
// Methods of Handler are called for Cwtch events on this channel
Handler CwtchServerSendChannelHandler
channel *channels.Channel
spamguard spam.Guard
}
// CwtchServerSendChannelHandler defines the interface needed to interact with this channel
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)
} else {
log.Errorf("Failed to validate spamguard %v\n", gm)
}
}
} else {
log.Errorf("Failed to decode packet on SEND channel %v\n", err)
}
cc.channel.CloseChannel()
}

View File

@ -0,0 +1,174 @@
package send
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/protocol/connections/spam"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"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.Guard
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"),
Signature: []byte{},
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.Guard
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"),
Signature: []byte{},
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)
}
}

View File

@ -1,161 +1,100 @@
package server
import (
"crypto/ed25519"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/server/fetch"
"cwtch.im/cwtch/server/listen"
"cwtch.im/cwtch/server/metrics"
"cwtch.im/cwtch/server/send"
"cwtch.im/cwtch/server/storage"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
tor2 "git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/persistence"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"path"
"sync"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
"strconv"
"time"
)
// Server encapsulates a complete, compliant Cwtch server.
type Server struct {
service tapir.Service
config Config
metricsPack metrics.Monitors
tokenTapirService tapir.Service
tokenServer *privacypass.TokenServer
tokenService primitives.Identity
tokenServicePrivKey ed25519.PrivateKey
tokenServiceStopped bool
onionServiceStopped bool
running bool
existingMessageCount int
lock sync.RWMutex
}
// Setup initialized a server from a given configuration
func (s *Server) Setup(serverConfig Config) {
s.config = serverConfig
bs := new(persistence.BoltPersistence)
bs.Open(path.Join(serverConfig.ConfigDir, "tokens.db"))
s.tokenServer = privacypass.NewTokenServerFromStore(&serverConfig.TokenServiceK, bs)
log.Infof("Y: %v", s.tokenServer.Y)
s.tokenService = s.config.TokenServiceIdentity()
s.tokenServicePrivKey = s.config.TokenServerPrivateKey
}
// Identity returns the main onion identity of the server
func (s *Server) Identity() primitives.Identity {
return s.config.Identity()
app *application.RicochetApplication
config Config
metricsPack metrics.Monitors
closed bool
}
// Run starts a server with the given privateKey
func (s *Server) Run(acn connectivity.ACN) error {
addressIdentity := tor.GetTorV3Hostname(s.config.PublicKey)
identity := primitives.InitializeIdentity("", &s.config.PrivateKey, &s.config.PublicKey)
var service tapir.Service
service = new(tor2.BaseOnionService)
service.Init(acn, s.config.PrivateKey, &identity)
s.service = service
log.Infof("cwtch server running on cwtch:%s\n", addressIdentity+".onion:")
s.metricsPack.Start(service, s.config.ConfigDir, s.config.ServerReporting.LogMetricsToFile)
// TODO: surface errors
// TODO: handle HUP/KILL signals to exit and close Tor gracefully
// TODO: handle user input to exit
func (s *Server) Run(acn connectivity.ACN, serverConfig Config) {
s.closed = false
s.config = serverConfig
cwtchserver := new(application.RicochetApplication)
s.metricsPack.Start(cwtchserver, serverConfig.ConfigDir, s.config.ServerReporting.LogMetricsToFile)
af := application.InstanceFactory{}
af.Init()
ms := new(storage.MessageStore)
err := ms.Init(s.config.ConfigDir, s.config.MaxBufferLines, s.metricsPack.MessageCounter)
err := ms.Init(serverConfig.ConfigDir, s.config.MaxBufferLines, s.metricsPack.MessageCounter)
if err != nil {
return err
log.Errorln(err)
acn.Close()
os.Exit(1)
}
af.AddHandler("im.cwtch.server.listen", func(rai *application.Instance) func() channels.Handler {
return func() channels.Handler {
cslc := new(listen.CwtchServerListenChannel)
return cslc
}
})
// Needed because we only collect metrics on a per-session basis
// TODO fix metrics so they persist across sessions?
s.existingMessageCount = len(ms.FetchMessages())
af.AddHandler("im.cwtch.server.fetch", func(rai *application.Instance) func() channels.Handler {
si := new(Instance)
si.Init(rai, cwtchserver, ms)
return func() channels.Handler {
cssc := new(fetch.CwtchServerFetchChannel)
cssc.Handler = si
return cssc
}
})
s.tokenTapirService = new(tor2.BaseOnionService)
s.tokenTapirService.Init(acn, s.tokenServicePrivKey, &s.tokenService)
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = s.tokenServer
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
go func() {
s.tokenTapirService.Listen(powTokenApp)
s.tokenServiceStopped = true
}()
go func() {
s.service.Listen(NewTokenBoardServer(ms, s.tokenServer))
s.onionServiceStopped = true
}()
af.AddHandler("im.cwtch.server.send", func(rai *application.Instance) func() channels.Handler {
si := new(Instance)
si.Init(rai, cwtchserver, ms)
return func() channels.Handler {
cssc := new(send.CwtchServerSendChannel)
cssc.Handler = si
return cssc
}
})
s.lock.Lock()
s.running = true
s.lock.Unlock()
return nil
}
addressIdentity := utils.GetTorV3Hostname(s.config.PublicKey)
cwtchserver.Init(acn, "cwtch server for "+addressIdentity, s.config.Identity(), af, new(application.AcceptAllContactManager))
port := strconv.Itoa(application.RicochetPort)
log.Infof("cwtch server running on cwtch:%s\n", addressIdentity+".onion:"+port)
// KeyBundle provides the signed keybundle of the server
func (s *Server) KeyBundle() *model.KeyBundle {
kb := model.NewKeyBundle()
identity := s.config.Identity()
kb.Keys[model.KeyTypeServerOnion] = model.Key(identity.Hostname())
kb.Keys[model.KeyTypeTokenOnion] = model.Key(s.tokenService.Hostname())
kb.Keys[model.KeyTypePrivacyPass] = model.Key(s.tokenServer.Y.String())
kb.Sign(identity)
return kb
}
s.app = cwtchserver
// CheckStatus returns true if the server is running and/or an error if any part of the server needs to be restarted.
func (s *Server) CheckStatus() (bool, error) {
s.lock.RLock()
defer s.lock.RUnlock()
if s.onionServiceStopped == true || s.tokenServiceStopped == true {
return s.running, fmt.Errorf("one of more server components are down: onion:%v token service: %v", s.onionServiceStopped, s.tokenServiceStopped)
for true {
listenService, err := acn.Listen(s.config.PrivateKey, application.RicochetPort)
if err != nil {
log.Errorf("Listen() error setting up onion service: %v\n", err)
} else {
s.app.Run(listenService)
}
if s.closed {
return
}
time.Sleep(5 * time.Second)
}
return s.running, nil
}
// Shutdown kills the app closing all connections and freeing all goroutines
func (s *Server) Shutdown() {
s.lock.Lock()
defer s.lock.Unlock()
s.service.Shutdown()
s.tokenTapirService.Shutdown()
s.closed = true
s.app.Shutdown()
s.metricsPack.Stop()
s.running = true
}
// Statistics is an encapsulation of information about the server that an operator might want to know at a glance.
type Statistics struct {
TotalMessages int
}
// GetStatistics is a stub method for providing some high level information about
// the server operation to bundling applications (e.g. the UI)
func (s *Server) GetStatistics() Statistics {
// TODO Statistics from Metrics is very awkward. Metrics needs an overhaul to make safe
total := s.existingMessageCount
if s.metricsPack.TotalMessageCounter != nil {
total += s.metricsPack.TotalMessageCounter.Count()
}
return Statistics{
TotalMessages: total,
}
}
// ConfigureAutostart sets whether this server should autostart (in the Cwtch UI/bundling application)
func (s *Server) ConfigureAutostart(autostart bool) {
s.config.AutoStart = autostart
s.config.Save(s.config.ConfigDir, s.config.FilePath)
}
// Close shuts down the cwtch server in a safe way.
func (s *Server) Close() {
log.Infof("Shutting down server")
s.lock.Lock()
defer s.lock.Unlock()
log.Infof("Closing Token Server Database...")
s.tokenServer.Close()
}

View File

@ -3,9 +3,8 @@ package server
import (
"crypto/rand"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"path"
@ -20,30 +19,16 @@ type Reporting struct {
// Config is a struct for storing basic server configuration
type Config struct {
ConfigDir string `json:"-"`
FilePath string `json:"-"`
MaxBufferLines int `json:"maxBufferLines"`
PublicKey ed25519.PublicKey `json:"publicKey"`
PrivateKey ed25519.PrivateKey `json:"privateKey"`
TokenServerPublicKey ed25519.PublicKey `json:"tokenServerPublicKey"`
TokenServerPrivateKey ed25519.PrivateKey `json:"tokenServerPrivateKey"`
TokenServiceK ristretto255.Scalar `json:"tokenServiceK"`
ServerReporting Reporting `json:"serverReporting"`
AutoStart bool `json:"autostart"`
ConfigDir string `json:"-"`
MaxBufferLines int `json:"maxBufferLines"`
PublicKey ed25519.PublicKey `json:"publicKey"`
PrivateKey ed25519.PrivateKey `json:"privateKey"`
ServerReporting Reporting `json:"serverReporting"`
}
// Identity returns an encapsulation of the servers keys
func (config *Config) Identity() primitives.Identity {
return primitives.InitializeIdentity("", &config.PrivateKey, &config.PublicKey)
}
// TokenServiceIdentity returns an encapsulation of the servers token server (experimental)
func (config *Config) TokenServiceIdentity() primitives.Identity {
return primitives.InitializeIdentity("", &config.TokenServerPrivateKey, &config.TokenServerPublicKey)
// Identity returns an encapsulation of the servers keys for running ricochet
func (config *Config) Identity() identity.Identity {
return identity.InitializeV3("", &config.PrivateKey, &config.PublicKey)
}
// Save dumps the latest version of the config to a json file given by filename
@ -57,33 +42,9 @@ func (config *Config) Save(dir, filename string) {
func LoadConfig(configDir, filename string) Config {
log.Infof("Loading config from %s\n", path.Join(configDir, filename))
config := Config{}
id, pk := primitives.InitializeEphemeralIdentity()
tid, tpk := primitives.InitializeEphemeralIdentity()
config.PrivateKey = pk
config.PublicKey = id.PublicKey()
config.TokenServerPrivateKey = tpk
config.TokenServerPublicKey = tid.PublicKey()
config.MaxBufferLines = 100000
config.ServerReporting = Reporting{
LogMetricsToFile: true,
ReportingGroupID: "",
ReportingServerAddr: "",
}
config.AutoStart = false
config.ConfigDir = configDir
config.FilePath = filename
k := new(ristretto255.Scalar)
b := make([]byte, 64)
_, err := rand.Read(b)
if err != nil {
// unable to generate secure random numbers
panic("unable to generate secure random numbers")
}
k.FromUniformBytes(b)
config.TokenServiceK = *k
config.MaxBufferLines = 100000
config.ServerReporting.LogMetricsToFile = false
raw, err := ioutil.ReadFile(path.Join(configDir, filename))
if err == nil {
err = json.Unmarshal(raw, &config)
@ -92,6 +53,11 @@ func LoadConfig(configDir, filename string) Config {
log.Errorf("reading config: %v", err)
}
}
if config.PrivateKey == nil {
config.PublicKey, config.PrivateKey, _ = ed25519.GenerateKey(rand.Reader)
}
// Always save (first time generation, new version with new variables populated)
config.Save(configDir, filename)
return config

45
server/server_instance.go Normal file
View File

@ -0,0 +1,45 @@
package server
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/server/listen"
"cwtch.im/cwtch/server/storage"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
)
// Instance encapsulates the Ricochet application.
type Instance struct {
rai *application.Instance
ra *application.RicochetApplication
msi storage.MessageStoreInterface
}
// Init sets up a Server Instance
func (si *Instance) Init(rai *application.Instance, ra *application.RicochetApplication, msi storage.MessageStoreInterface) {
si.rai = rai
si.ra = ra
si.msi = msi
}
// HandleFetchRequest returns a list of all messages in the servers buffer
func (si *Instance) HandleFetchRequest() []*protocol.GroupMessage {
return si.msi.FetchMessages()
}
// HandleGroupMessage takes in a group message and distributes it to all listening peers
func (si *Instance) HandleGroupMessage(gm *protocol.GroupMessage) {
si.msi.AddMessage(*gm)
go si.ra.Broadcast(func(rai *application.Instance) {
rai.Connection.Do(func() error {
channel := rai.Connection.Channel("im.cwtch.server.listen", channels.Inbound)
if channel != nil {
cslc, ok := channel.Handler.(*listen.CwtchServerListenChannel)
if ok {
cslc.SendGroupMessage(gm)
}
}
return nil
})
})
}

View File

@ -0,0 +1,37 @@
package server
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/server/metrics"
"cwtch.im/cwtch/server/storage"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"os"
"testing"
"time"
)
func TestServerInstance(t *testing.T) {
si := new(Instance)
ai := new(application.Instance)
ra := new(application.RicochetApplication)
msi := new(storage.MessageStore)
os.RemoveAll("messages")
msi.Init(".", 5, metrics.NewCounter())
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 1 Group messages Instead got %v", res)
}
// ra.HandleApplicationInstance(ai)
si.HandleGroupMessage(&gm)
time.Sleep(time.Second * 2)
}

View File

@ -1,118 +0,0 @@
package server
import (
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/server/storage"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
)
// NewTokenBoardServer generates new Server for Token Board
func NewTokenBoardServer(store storage.MessageStoreInterface, tokenService *privacypass.TokenServer) tapir.Application {
tba := new(TokenboardServer)
tba.TokenService = tokenService
tba.LegacyMessageStore = store
return tba
}
// TokenboardServer defines the token board server
type TokenboardServer struct {
applications.AuthApp
connection tapir.Connection
TokenService *privacypass.TokenServer
LegacyMessageStore storage.MessageStoreInterface
}
// NewInstance creates a new TokenBoardApp
func (ta *TokenboardServer) NewInstance() tapir.Application {
tba := new(TokenboardServer)
tba.TokenService = ta.TokenService
tba.LegacyMessageStore = ta.LegacyMessageStore
return tba
}
// Init initializes the cryptographic TokenBoardApp
func (ta *TokenboardServer) Init(connection tapir.Connection) {
ta.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
ta.connection = connection
go ta.Listen()
} else {
connection.Close()
}
}
// Listen processes the messages for this application
func (ta *TokenboardServer) Listen() {
for {
data := ta.connection.Expect()
if len(data) == 0 {
log.Debugf("Server Closing Connection")
ta.connection.Close()
return // connection is closed
}
var message groups.Message
if err := json.Unmarshal(data, &message); err != nil {
log.Debugf("Server Closing Connection Because of Malformed Client Packet %v", err)
ta.connection.Close()
return // connection is closed
}
switch message.MessageType {
case groups.PostRequestMessage:
if message.PostRequest != nil {
postrequest := *message.PostRequest
log.Debugf("Received a Post Message Request: %v", ta.connection.Hostname())
ta.postMessageRequest(postrequest)
} else {
log.Debugf("Server Closing Connection Because of PostRequestMessage Client Packet")
ta.connection.Close()
return // connection is closed
}
case groups.ReplayRequestMessage:
if message.ReplayRequest != nil {
log.Debugf("Received Replay Request %v", message.ReplayRequest)
messages := ta.LegacyMessageStore.FetchMessages()
response, _ := json.Marshal(groups.Message{MessageType: groups.ReplayResultMessage, ReplayResult: &groups.ReplayResult{NumMessages: len(messages)}})
log.Debugf("Sending Replay Response %v", groups.ReplayResult{NumMessages: len(messages)})
ta.connection.Send(response)
for _, message := range messages {
data, _ = json.Marshal(message)
ta.connection.Send(data)
}
// Set sync and then send any new messages that might have happened while we were syncing
ta.connection.SetCapability(groups.CwtchServerSyncedCapability)
newMessages := ta.LegacyMessageStore.FetchMessages()
if len(newMessages) > len(messages) {
for _, message := range newMessages[len(messages):] {
data, _ = json.Marshal(groups.Message{MessageType: groups.NewMessageMessage, NewMessage: &groups.NewMessage{EGM: *message}})
ta.connection.Send(data)
}
}
} else {
log.Debugf("Server Closing Connection Because of Malformed ReplayRequestMessage Packet")
ta.connection.Close()
return // connection is closed
}
}
}
}
func (ta *TokenboardServer) postMessageRequest(pr groups.PostRequest) {
if err := ta.TokenService.SpendToken(pr.Token, append(pr.EGM.ToBytes(), ta.connection.ID().Hostname()...)); err == nil {
log.Debugf("Token is valid")
ta.LegacyMessageStore.AddMessage(pr.EGM)
data, _ := json.Marshal(groups.Message{MessageType: groups.PostResultMessage, PostResult: &groups.PostResult{Success: true}})
ta.connection.Send(data)
data, _ = json.Marshal(groups.Message{MessageType: groups.NewMessageMessage, NewMessage: &groups.NewMessage{EGM: pr.EGM}})
ta.connection.Broadcast(data, groups.CwtchServerSyncedCapability)
} else {
log.Debugf("Attempt to spend an invalid token: %v", err)
data, _ := json.Marshal(groups.Message{MessageType: groups.PostResultMessage, PostResult: &groups.PostResult{Success: false}})
ta.connection.Send(data)
}
}

View File

@ -2,11 +2,11 @@ package storage
import (
"bufio"
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/server/metrics"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
"path"
"sync"
@ -20,8 +20,8 @@ const (
// MessageStoreInterface defines an interface to interact with a store of cwtch messages.
type MessageStoreInterface interface {
AddMessage(groups.EncryptedGroupMessage)
FetchMessages() []*groups.EncryptedGroupMessage
AddMessage(protocol.GroupMessage)
FetchMessages() []*protocol.GroupMessage
}
// MessageStore is a file-backed implementation of MessageStoreInterface
@ -30,7 +30,7 @@ type MessageStore struct {
filePos int
storeDirectory string
lock sync.Mutex
messages []*groups.EncryptedGroupMessage
messages []*protocol.GroupMessage
messageCounter metrics.Counter
maxBufferLines int
bufferPos int
@ -45,7 +45,7 @@ func (ms *MessageStore) Close() {
ms.lock.Unlock()
}
func (ms *MessageStore) updateBuffer(gm *groups.EncryptedGroupMessage) {
func (ms *MessageStore) updateBuffer(gm *protocol.GroupMessage) {
ms.messages[ms.bufferPos] = gm
ms.bufferPos++
if ms.bufferPos == ms.maxBufferLines {
@ -70,7 +70,7 @@ func (ms *MessageStore) initAndLoadFiles() error {
for scanner.Scan() {
gms := scanner.Text()
ms.filePos++
gm := &groups.EncryptedGroupMessage{}
gm := &protocol.GroupMessage{}
err := json.Unmarshal([]byte(gms), gm)
if err == nil {
ms.updateBuffer(gm)
@ -83,7 +83,7 @@ func (ms *MessageStore) initAndLoadFiles() error {
return nil
}
func (ms *MessageStore) updateFile(gm *groups.EncryptedGroupMessage) {
func (ms *MessageStore) updateFile(gm *protocol.GroupMessage) {
s, err := json.Marshal(gm)
if err != nil {
log.Errorf("Failed to unmarshal group message %v\n", err)
@ -118,7 +118,7 @@ func (ms *MessageStore) Init(appDirectory string, maxBufferLines int, messageCou
ms.bufferPos = 0
ms.maxBufferLines = maxBufferLines
ms.messages = make([]*groups.EncryptedGroupMessage, maxBufferLines)
ms.messages = make([]*protocol.GroupMessage, maxBufferLines)
ms.bufferRotated = false
ms.messageCounter = messageCounter
@ -127,13 +127,13 @@ func (ms *MessageStore) Init(appDirectory string, maxBufferLines int, messageCou
}
// FetchMessages returns all messages from the backing file.
func (ms *MessageStore) FetchMessages() (messages []*groups.EncryptedGroupMessage) {
func (ms *MessageStore) FetchMessages() (messages []*protocol.GroupMessage) {
ms.lock.Lock()
if !ms.bufferRotated {
messages = make([]*groups.EncryptedGroupMessage, ms.bufferPos)
messages = make([]*protocol.GroupMessage, ms.bufferPos)
copy(messages, ms.messages[0:ms.bufferPos])
} else {
messages = make([]*groups.EncryptedGroupMessage, ms.maxBufferLines)
messages = make([]*protocol.GroupMessage, ms.maxBufferLines)
copy(messages, ms.messages[ms.bufferPos:ms.maxBufferLines])
copy(messages[ms.bufferPos:], ms.messages[0:ms.bufferPos])
}
@ -142,7 +142,7 @@ func (ms *MessageStore) FetchMessages() (messages []*groups.EncryptedGroupMessag
}
// AddMessage adds a GroupMessage to the store
func (ms *MessageStore) AddMessage(gm groups.EncryptedGroupMessage) {
func (ms *MessageStore) AddMessage(gm protocol.GroupMessage) {
ms.messageCounter.Add(1)
ms.lock.Lock()
ms.updateBuffer(&gm)

View File

@ -1,7 +1,7 @@
package storage
import (
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/server/metrics"
"os"
"strconv"
@ -14,8 +14,9 @@ func TestMessageStore(t *testing.T) {
counter := metrics.NewCounter()
ms.Init("./", 1000, counter)
for i := 0; i < 499; i++ {
gm := groups.EncryptedGroupMessage{
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)
}
@ -32,8 +33,9 @@ func TestMessageStore(t *testing.T) {
counter.Reset()
for i := 0; i < 1000; i++ {
gm := groups.EncryptedGroupMessage{
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)
}

View File

@ -5,7 +5,7 @@ import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/storage/v0"
"cwtch.im/cwtch/storage/v1"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"io/ioutil"
"path"
"strconv"
@ -79,7 +79,7 @@ func upgradeV0ToV1(directory, password string) error {
func versionCheckUpgrade(directory, password string) {
version := detectVersion(directory)
log.Debugf("versionCheck: %v\n", version)
log.Infof("versionCheck: %v\n", version)
if version == -1 {
return
}

View File

@ -4,10 +4,9 @@ package storage
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/storage/v0"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"os"
"testing"
"time"
@ -33,8 +32,6 @@ func TestProfileStoreUpgradeV0toV1(t *testing.T) {
fmt.Println("Creating and initializing v0 profile and store...")
profile := NewProfile(testProfileName)
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
ps1 := v0.NewProfileWriterStore(eventBus, testingDir, password, profile)
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
@ -45,7 +42,7 @@ func TestProfileStoreUpgradeV0toV1(t *testing.T) {
t.Errorf("Creating group invite: %v\n", err)
}
ps1.AddGroup(invite)
ps1.AddGroup(invite, profile.Onion)
fmt.Println("Sending 200 messages...")

View File

@ -3,7 +3,7 @@ package v0
import (
"crypto/rand"
"errors"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"

View File

@ -52,9 +52,9 @@ func ReadProfile(directory, password string) (*model.Profile, error) {
/********************************************************************************************/
// AddGroup For testing, adds a group to the profile (and starts a stream store)
func (ps *ProfileStoreV0) AddGroup(invite string) {
gid, err := ps.profile.ProcessInvite(invite)
// AddGroup For testing, adds a group to the profile (and startsa stream store)
func (ps *ProfileStoreV0) AddGroup(invite []byte, peer string) {
gid, err := ps.profile.ProcessInvite(string(invite), peer)
if err == nil {
ps.save()
group := ps.profile.Groups[gid]

View File

@ -40,7 +40,7 @@ func TestProfileStoreWriteRead(t *testing.T) {
t.Errorf("Creating group invite: %v\n", err)
}
ps1.AddGroup(invite)
ps1.AddGroup(invite, profile.Onion)
ps1.AddGroupMessage(groupid, time.Now().Format(time.RFC3339Nano), time.Now().Format(time.RFC3339Nano), ps1.getProfileCopy(true).Onion, testMessage)

View File

@ -4,7 +4,7 @@ import (
"cwtch.im/cwtch/model"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"io/ioutil"
"os"
"path"
@ -46,7 +46,7 @@ func NewStreamStore(directory string, filenameBase string, password string) (sto
return ss
}
// Read returns all messages from the backing file (not the buffer, for writing to the current file)
// Read returns all messages from the backing file (not the buffer, which is jsut for writing to the current file)
func (ss *streamStore) Read() (messages []model.Message) {
ss.lock.Lock()
defer ss.lock.Unlock()

View File

@ -3,7 +3,7 @@ package v1
import (
"crypto/rand"
"errors"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
@ -12,8 +12,8 @@ import (
"path"
)
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
// createKeySalt derives a key from a password: returns key, salt, err
func createKeySalt(password string) ([32]byte, [128]byte, error) {
var salt [128]byte
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
@ -26,8 +26,7 @@ func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
return dkr, salt, nil
}
// CreateKey derives a key from a password and salt
func CreateKey(password string, salt []byte) [32]byte {
func createKey(password string, salt []byte) [32]byte {
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
var dkr [32]byte
@ -35,8 +34,8 @@ func CreateKey(password string, salt []byte) [32]byte {
return dkr
}
//EncryptFileData encrypts the data with the supplied key
func EncryptFileData(data []byte, key [32]byte) ([]byte, error) {
//encryptFileData encrypts the cwtchPeer via the specified key.
func encryptFileData(data []byte, key [32]byte) ([]byte, error) {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
@ -48,8 +47,8 @@ func EncryptFileData(data []byte, key [32]byte) ([]byte, error) {
return encrypted, nil
}
//DecryptFile decrypts the passed ciphertext with the supplied key.
func DecryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
//decryptFile decrypts the passed ciphertext into a cwtchPeer via the specified key.
func decryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key)
@ -59,11 +58,11 @@ func DecryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
return nil, errors.New("Failed to decrypt")
}
// ReadEncryptedFile reads data from an encrypted file in directory with key
func ReadEncryptedFile(directory, filename string, key [32]byte) ([]byte, error) {
// load instantiates a cwtchPeer from the file store
func readEncryptedFile(directory, filename string, key [32]byte) ([]byte, error) {
encryptedbytes, err := ioutil.ReadFile(path.Join(directory, filename))
if err == nil {
data, err := DecryptFile(encryptedbytes, key)
data, err := decryptFile(encryptedbytes, key)
if err == nil {
return data, nil
}

View File

@ -1,7 +1,7 @@
package v1
import (
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"io/ioutil"
"os"
"path"
@ -33,7 +33,7 @@ func NewFileStore(directory string, filename string, key [32]byte) FileStore {
// write serializes a cwtchPeer to a file
func (fps *fileStore) Write(data []byte) error {
encryptedbytes, err := EncryptFileData(data, fps.key)
encryptedbytes, err := encryptFileData(data, fps.key)
if err != nil {
return err
}
@ -43,7 +43,7 @@ func (fps *fileStore) Write(data []byte) error {
}
func (fps *fileStore) Read() ([]byte, error) {
return ReadEncryptedFile(fps.directory, fps.filename, fps.key)
return readEncryptedFile(fps.directory, fps.filename, fps.key)
}
func (fps *fileStore) Delete() {

View File

@ -3,13 +3,11 @@ package v1
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"encoding/base64"
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"io/ioutil"
"os"
"path"
"strconv"
"time"
)
@ -34,7 +32,7 @@ type ProfileStoreV1 struct {
}
func initV1Directory(directory, password string) ([32]byte, [128]byte, error) {
key, salt, err := CreateKeySalt(password)
key, salt, err := createKeySalt(password)
if err != nil {
log.Errorf("Could not create key for profile store from password: %v\n", err)
return [32]byte{}, [128]byte{}, err
@ -68,7 +66,8 @@ func (ps *ProfileStoreV1) initProfileWriterStore() {
ps.queue = event.NewQueue()
go ps.eventHandler()
ps.eventManager.Subscribe(event.SetPeerAuthorization, ps.queue)
ps.eventManager.Subscribe(event.BlockPeer, ps.queue)
ps.eventManager.Subscribe(event.UnblockPeer, ps.queue)
ps.eventManager.Subscribe(event.PeerCreated, ps.queue)
ps.eventManager.Subscribe(event.GroupCreated, ps.queue)
ps.eventManager.Subscribe(event.SetProfileName, ps.queue)
@ -76,18 +75,13 @@ func (ps *ProfileStoreV1) initProfileWriterStore() {
ps.eventManager.Subscribe(event.SetPeerAttribute, ps.queue)
ps.eventManager.Subscribe(event.SetGroupAttribute, ps.queue)
ps.eventManager.Subscribe(event.AcceptGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.RejectGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.NewGroup, ps.queue)
ps.eventManager.Subscribe(event.NewGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.NewMessageFromGroup, ps.queue)
ps.eventManager.Subscribe(event.SendMessageToPeer, ps.queue)
ps.eventManager.Subscribe(event.PeerAcknowledgement, ps.queue)
ps.eventManager.Subscribe(event.NewMessageFromPeer, ps.queue)
ps.eventManager.Subscribe(event.PeerStateChange, ps.queue)
ps.eventManager.Subscribe(event.ServerStateChange, ps.queue)
ps.eventManager.Subscribe(event.DeleteContact, ps.queue)
ps.eventManager.Subscribe(event.DeleteGroup, ps.queue)
ps.eventManager.Subscribe(event.ChangePassword, ps.queue)
ps.eventManager.Subscribe(event.UpdateMessageFlags, ps.queue)
}
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
@ -98,17 +92,15 @@ func LoadProfileWriterStore(eventManager event.Manager, directory, password stri
return nil, err
}
key := CreateKey(password, salt)
key := createKey(password, salt)
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, directory: directory, profile: nil, eventManager: eventManager, streamStores: map[string]StreamStore{}, writer: true}
copy(ps.salt[:], salt)
err = ps.load()
if err != nil {
return nil, err
}
ps.initProfileWriterStore()
ps.load()
return ps, nil
}
@ -186,14 +178,14 @@ func (ps *ProfileStoreV1) GetStatusMessages() []*event.Event {
// ChangePassword restores all data under a new password's encryption
func (ps *ProfileStoreV1) ChangePassword(oldpass, newpass, eventID string) {
oldkey := CreateKey(oldpass, ps.salt[:])
oldkey := createKey(oldpass, ps.salt[:])
if oldkey != ps.key {
ps.eventManager.Publish(event.NewEventList(event.ChangePasswordError, event.Error, "Supplied current password does not match", event.EventID, eventID))
return
}
newkey := CreateKey(newpass, ps.salt[:])
newkey := createKey(newpass, ps.salt[:])
newStreamStores := map[string]StreamStore{}
idToNewLocalID := map[string]string{}
@ -219,11 +211,7 @@ func (ps *ProfileStoreV1) ChangePassword(oldpass, newpass, eventID string) {
if len(ssid) == groupIDLen {
ps.profile.Groups[ssid].LocalID = newLocalID
} else {
if ps.profile.Contacts[ssid] != nil {
ps.profile.Contacts[ssid].LocalID = newLocalID
} else {
log.Errorf("Unknown Contact: %v. This is probably the result of corrupted development data from fuzzing. This contact will not appear in the new profile.", ssid)
}
ps.profile.Contacts[ssid].LocalID = newLocalID
}
}
@ -245,26 +233,9 @@ func (ps *ProfileStoreV1) save() error {
bytes, _ := json.Marshal(ps.profile)
return ps.fs.Write(bytes)
}
return nil
}
func (ps *ProfileStoreV1) regenStreamStore(messages []model.Message, contact string) {
oldss := ps.streamStores[contact]
newLocalID := model.GenerateRandomID()
newSS := NewStreamStore(ps.directory, newLocalID, ps.key)
newSS.WriteN(messages)
if len(contact) == groupIDLen {
ps.profile.Groups[contact].LocalID = newLocalID
} else {
// We can assume this exists as regen stream store should only happen to *update* a message
ps.profile.Contacts[contact].LocalID = newLocalID
}
ps.streamStores[contact] = newSS
ps.save()
oldss.Delete()
}
// load instantiates a cwtchPeer from the file store
func (ps *ProfileStoreV1) load() error {
decrypted, err := ps.fs.Read()
@ -273,48 +244,15 @@ func (ps *ProfileStoreV1) load() error {
}
cp := new(model.Profile)
err = json.Unmarshal(decrypted, &cp)
if err == nil {
ps.profile = cp
// TODO 2020.06.09: v1 update, Remove on v2
// if we already have the contact it can be assumed "approved" unless blocked
for _, contact := range cp.Contacts {
if contact.Authorization == "" {
if contact.DeprecatedBlocked {
contact.Authorization = model.AuthBlocked
} else {
contact.Authorization = model.AuthApproved
}
}
// Check if there is any saved history...
saveHistory, keyExists := contact.GetAttribute(event.SaveHistoryKey)
if !keyExists {
contact.SetAttribute(event.SaveHistoryKey, event.DeleteHistoryDefault)
}
if saveHistory == event.SaveHistoryConfirmed {
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
cp.Contacts[contact.Onion].Timeline.SetMessages(ss.Read())
ps.streamStores[contact.Onion] = ss
}
}
for gid, group := range cp.Groups {
if group.Version == 0 {
log.Infof("group %v is of unsupported version 0. dropping group...\n", group.GroupID)
delete(cp.Groups, gid)
continue
}
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
cp.Groups[gid].Timeline.SetMessages(ss.Read())
ps.streamStores[group.GroupID] = ss
}
ps.save()
}
return err
@ -326,20 +264,33 @@ func (ps *ProfileStoreV1) GetProfileCopy(timeline bool) *model.Profile {
}
func (ps *ProfileStoreV1) eventHandler() {
log.Debugln("eventHandler()!")
for {
ev := ps.queue.Next()
log.Debugf("eventHandler event %v %v\n", ev.EventType, ev.EventID)
switch ev.EventType {
case event.SetPeerAuthorization:
err := ps.profile.SetContactAuthorization(ev.Data[event.RemotePeer], model.Authorization(ev.Data[event.Authorization]))
if err == nil {
case event.BlockPeer:
contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer])
if exists {
contact.Blocked = true
ps.save()
}
case event.UnblockPeer:
contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer])
if exists {
contact.Blocked = false
ps.save()
}
case event.PeerCreated:
var pp *model.PublicProfile
json.Unmarshal([]byte(ev.Data[event.Data]), &pp)
ps.profile.AddContact(ev.Data[event.RemotePeer], pp)
// TODO: configure - allow peers to be configured to turn on limited storage
/*ss := NewStreamStore(ps.directory, pp.LocalID, ps.password)
pp.Timeline.SetMessages(ss.Read())
ps.streamStores[pp.Onion] = ss
ps.save()*/
case event.GroupCreated:
var group *model.Group
json.Unmarshal([]byte(ev.Data[event.Data]), &group)
@ -358,27 +309,6 @@ func (ps *ProfileStoreV1) eventHandler() {
if exists {
contact.SetAttribute(ev.Data[event.Key], ev.Data[event.Data])
ps.save()
switch ev.Data[event.Key] {
case event.SaveHistoryKey:
if event.DeleteHistoryConfirmed == ev.Data[event.Data] {
ss, exists := ps.streamStores[ev.Data[event.RemotePeer]]
if exists {
ss.Delete()
delete(ps.streamStores, ev.Data[event.RemotePeer])
}
} else if event.SaveHistoryConfirmed == ev.Data[event.Data] {
_, exists := ps.streamStores[ev.Data[event.RemotePeer]]
if !exists {
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
ps.streamStores[ev.Data[event.RemotePeer]] = ss
}
}
default:
{
}
}
} else {
log.Errorf("error setting attribute on peer %v peer does not exist", ev)
}
@ -397,11 +327,9 @@ func (ps *ProfileStoreV1) eventHandler() {
} else {
log.Errorf("error accepting group invite")
}
case event.RejectGroupInvite:
ps.profile.RejectInvite(ev.Data[event.GroupID])
ps.save()
case event.NewGroup:
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite])
case event.NewGroupInvite:
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite], ev.Data[event.RemotePeer])
log.Errorf("gid: %v err:%v\n", gid, err)
if err == nil {
ps.save()
group := ps.profile.Groups[gid]
@ -409,35 +337,13 @@ func (ps *ProfileStoreV1) eventHandler() {
} else {
log.Errorf("error storing new group invite: %v (%v)", err, ev)
}
case event.SendMessageToPeer: // cache the message till an ack, then it's given to stream store.
// stream store doesn't support updates, so we don't want to commit it till ack'd
ps.profile.AddSentMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now(), ev.EventID)
case event.NewMessageFromPeer:
ps.profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now())
ps.attemptSavePeerMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.TimestampReceived], true)
case event.PeerAcknowledgement:
onion := ev.Data[event.RemotePeer]
eventID := ev.Data[event.EventID]
contact, ok := ps.profile.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
message := contact.Timeline.Messages[mIdx]
ps.attemptSavePeerMessage(onion, message.Message, message.Timestamp.Format(time.RFC3339Nano), false)
}
}
ps.profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
case event.NewMessageFromGroup:
groupid := ev.Data[event.GroupID]
received, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
sent, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampSent])
sig, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
prevsig, _ := base64.StdEncoding.DecodeString(ev.Data[event.PreviousSignature])
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: sig, PreviousMessageSig: prevsig, Acknowledged: true}
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: []byte(ev.Data[event.Signature]), PreviousMessageSig: []byte(ev.Data[event.PreviousSignature])}
ss, exists := ps.streamStores[groupid]
if exists {
// We need to store a local copy of the message...
ps.profile.GetGroup(groupid).Timeline.Insert(&message)
ss.Write(message)
} else {
log.Errorf("error storing new group message: %v stream store does not exist", ev)
@ -456,11 +362,6 @@ func (ps *ProfileStoreV1) eventHandler() {
onion := ev.Data[event.RemotePeer]
ps.profile.DeleteContact(onion)
ps.save()
ss, exists := ps.streamStores[onion]
if exists {
ss.Delete()
delete(ps.streamStores, onion)
}
case event.DeleteGroup:
groupID := ev.Data[event.GroupID]
ps.profile.DeleteGroup(groupID)
@ -474,29 +375,6 @@ func (ps *ProfileStoreV1) eventHandler() {
oldpass := ev.Data[event.Password]
newpass := ev.Data[event.NewPassword]
ps.ChangePassword(oldpass, newpass, ev.EventID)
case event.UpdateMessageFlags:
handle := ev.Data[event.Handle]
mIx, err := strconv.Atoi(ev.Data[event.Index])
if err != nil {
log.Errorf("Invalid Message Index: %v", err)
return
}
flags, err := strconv.ParseUint(ev.Data[event.Flags], 2, 64)
if err != nil {
log.Errorf("Invalid Message Falgs: %v", err)
return
}
ps.profile.UpdateMessageFlags(handle, mIx, flags)
if len(handle) == groupIDLen {
ps.regenStreamStore(ps.profile.GetGroup(handle).Timeline.Messages, handle)
} else if contact, exists := ps.profile.GetContact(handle); exists {
if exists {
val, _ := contact.GetAttribute(event.SaveHistoryKey)
if val == event.SaveHistoryConfirmed {
ps.regenStreamStore(contact.Timeline.Messages, handle)
}
}
}
default:
return
}
@ -504,42 +382,6 @@ func (ps *ProfileStoreV1) eventHandler() {
}
}
// attemptSavePeerMessage checks if the peer has been configured to save history from this peer
// and if so the peer saves the message into history. fromPeer is used to control if the message is saved
// as coming from the remote peer or if it was sent by out profile.
func (ps *ProfileStoreV1) attemptSavePeerMessage(peerID, messageData, timestampeReceived string, fromPeer bool) {
contact, exists := ps.profile.GetContact(peerID)
if exists {
val, _ := contact.GetAttribute(event.SaveHistoryKey)
switch val {
case event.SaveHistoryConfirmed:
{
peerID := peerID
var received time.Time
var message model.Message
if fromPeer {
received, _ = time.Parse(time.RFC3339Nano, timestampeReceived)
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: peerID, Signature: []byte{}, PreviousMessageSig: []byte{}}
} else {
received := time.Now()
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: ps.profile.Onion, Signature: []byte{}, PreviousMessageSig: []byte{}, Acknowledged: true}
}
ss, exists := ps.streamStores[peerID]
if exists {
ss.Write(message)
} else {
log.Errorf("error storing new peer message: %v stream store does not exist", peerID)
}
}
default:
{
}
}
} else {
log.Errorf("error saving message for peer that doesn't exist: %v", peerID)
}
}
// Shutdown shuts down the queue / thread
func (ps *ProfileStoreV1) Shutdown() {
if ps.queue != nil {

View File

@ -4,7 +4,6 @@ package v1
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"fmt"
"log"
"os"
@ -23,9 +22,6 @@ func TestProfileStoreWriteRead(t *testing.T) {
os.RemoveAll(testingDir)
eventBus := event.NewEventManager()
profile := NewProfile(testProfileName)
// The lightest weight server entry possible (usually we would import a key bundle...)
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{event.Key: testKey, event.Data: testVal}))
@ -39,7 +35,7 @@ func TestProfileStoreWriteRead(t *testing.T) {
t.Errorf("Creating group invite: %v\n", err)
}
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
eventBus.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
time.Sleep(1 * time.Second)
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
@ -83,8 +79,6 @@ func TestProfileStoreChangePassword(t *testing.T) {
eventBus.Subscribe(event.ChangePasswordSuccess, queue)
profile := NewProfile(testProfileName)
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
@ -95,7 +89,7 @@ func TestProfileStoreChangePassword(t *testing.T) {
t.Errorf("Creating group invite: %v\n", err)
}
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
eventBus.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
time.Sleep(1 * time.Second)
fmt.Println("Sending 200 messages...")

View File

@ -4,7 +4,7 @@ import (
"cwtch.im/cwtch/model"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"io/ioutil"
"os"
"path"
@ -57,7 +57,7 @@ func (ss *streamStore) initBuffer() {
func (ss *streamStore) initBufferFromStorage() error {
filename := fmt.Sprintf("%s.%d", ss.filenameBase, 0)
bytes, _ := ReadEncryptedFile(ss.storeDirectory, filename, ss.key)
bytes, _ := readEncryptedFile(ss.storeDirectory, filename, ss.key)
msgs := []model.Message{}
err := json.Unmarshal([]byte(bytes), &msgs)
@ -83,7 +83,7 @@ func (ss *streamStore) updateFile() error {
}
// ENCRYPT
encryptedMsgs, err := EncryptFileData(msgs, ss.key)
encryptedMsgs, err := encryptFileData(msgs, ss.key)
if err != nil {
log.Errorf("Failed to encrypt messages: %v\n", err)
return err
@ -108,7 +108,7 @@ func (ss *streamStore) Delete() {
}
}
// Read returns all messages from the backing file (not the buffer, for writing to the current file)
// Read returns all messages from the backing file (not the buffer, which is jsut for writing to the current file)
func (ss *streamStore) Read() (messages []model.Message) {
ss.lock.Lock()
defer ss.lock.Unlock()
@ -118,7 +118,7 @@ func (ss *streamStore) Read() (messages []model.Message) {
for i := fileStorePartitions - 1; i >= 0; i-- {
filename := fmt.Sprintf("%s.%d", ss.filenameBase, i)
bytes, err := ReadEncryptedFile(ss.storeDirectory, filename, ss.key)
bytes, err := readEncryptedFile(ss.storeDirectory, filename, ss.key)
if err != nil {
continue
}

View File

@ -15,7 +15,7 @@ func TestStreamStoreWriteRead(t *testing.T) {
os.Remove(".test.json")
os.RemoveAll(testingDir)
os.Mkdir(testingDir, 0777)
key, _, _ := CreateKeySalt(password)
key, _, _ := createKeySalt(password)
ss1 := NewStreamStore(testingDir, filenameBase, key)
m := model.Message{Message: line1}
ss1.Write(m)
@ -34,7 +34,7 @@ func TestStreamStoreWriteReadRotate(t *testing.T) {
os.Remove(".test.json")
os.RemoveAll(testingDir)
os.Mkdir(testingDir, 0777)
key, _, _ := CreateKeySalt(password)
key, _, _ := createKeySalt(password)
ss1 := NewStreamStore(testingDir, filenameBase, key)
m := model.Message{Message: line1}
for i := 0; i < 400; i++ {

View File

@ -1,29 +1,22 @@
package testing
import (
"crypto/rand"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/app/utils"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/event/bridge"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
cwtchserver "cwtch.im/cwtch/server"
"encoding/base64"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"golang.org/x/net/proxy"
mrand "math/rand"
"os"
"os/user"
"path"
"runtime"
"runtime/pprof"
"strings"
"testing"
"time"
)
@ -67,48 +60,46 @@ func serverCheck(t *testing.T, serverAddr string) bool {
}
func waitForPeerGroupConnection(t *testing.T, peer peer.CwtchPeer, groupID string) {
peerName, _ := peer.GetAttribute(attr.GetLocalScope("name"))
for {
fmt.Printf("%v checking group connection...\n", peerName)
state, ok := peer.GetGroupState(groupID)
_, ok := peer.GetProfile().Groups[groupID]
if ok {
fmt.Printf("Waiting for Peer %v to join group %v - state: %v\n", peerName, groupID, state)
state := peer.GetGroupState(groupID)
//log.Infof("Waiting for Peer %v to join group %v - state: %v\n", peer.GetProfile().Name, groupID, state)
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peer.GetOnion(), groupID)
t.Fatalf("%v could not connect to %v", peer.GetProfile().Onion, groupID)
}
if state != connections.SYNCED {
fmt.Printf("peer %v %v waiting connect to group %v, currently: %v\n", peerName, peer.GetOnion(), groupID, connections.ConnectionStateName[state])
fmt.Printf("peer %v waiting connect to group %v, currently: %v\n", peer.GetProfile().Onion, groupID, connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
fmt.Printf("peer %v %v CONNECTED to group %v\n", peerName, peer.GetOnion(), groupID)
break
}
}
time.Sleep(time.Second * 2)
} // It might take a second for the server to show up as it is now going through the event bus
time.Sleep(time.Second)
}
return
}
func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) {
for {
state, ok := peera.GetPeerState(peerb.GetOnion())
//peers := peera.GetPeers()
_, ok := peera.GetProfile().Contacts[peerb.GetProfile().Onion]
if ok {
state := peera.GetPeerState(peerb.GetProfile().Onion)
//log.Infof("Waiting for Peer %v to peer with peer: %v - state: %v\n", peera.GetProfile().Name, peerb.GetProfile().Name, state)
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion())
t.Fatalf("%v could not connect to %v", peera.GetProfile().Onion, peerb.GetProfile().Onion)
}
if state != connections.AUTHENTICATED {
fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state])
fmt.Printf("peer% v waiting connect to peer %v, currently: %v\n", peera.GetProfile().Onion, peerb.GetProfile().Onion, connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
peerAName, _ := peera.GetAttribute(attr.GetLocalScope("name"))
peerBName, _ := peerb.GetAttribute(attr.GetLocalScope("name"))
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
break
}
}
} // It might take a second for the peer to show up as it is now going through the event bus
time.Sleep(time.Second)
}
return
}
@ -122,37 +113,17 @@ func TestCwtchPeerIntegration(t *testing.T) {
log.ExcludeFromPattern("outbound/3dhauthchannel")
log.ExcludeFromPattern("event/eventmanager")
log.ExcludeFromPattern("pipeBridge")
log.ExcludeFromPattern("tapir")
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
_, err := rand.Read(key)
if err != nil {
panic(err)
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
acn, err := connectivity.StartTor(".", "")
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
pid, _ := acn.GetPID()
t.Logf("Tor pid: %v", pid)
// ***** Cwtch Server management *****
var server *cwtchserver.Server
serverOnline := false
var serverAddr string
var serverKeyBundle []byte
if !serverOnline {
// launch app with new key
fmt.Println("No server found!")
@ -162,10 +133,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
config := cwtchserver.LoadConfig(".", "server-test.json")
identity := config.Identity()
serverAddr = identity.Hostname()
server.Setup(config)
serverKeyBundle, _ = json.Marshal(server.KeyBundle())
log.Debugf("server key bundle %s", serverKeyBundle)
go server.Run(acn)
go server.Run(acn, config)
// let tor get established
fmt.Printf("Establishing Tor hidden service: %v...\n", serverAddr)
@ -177,13 +145,8 @@ func TestCwtchPeerIntegration(t *testing.T) {
app := app2.NewApp(acn, "./storage")
usr, _ := user.Current()
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
os.Mkdir(cwtchDir, 0700)
os.RemoveAll(path.Join(cwtchDir, "testing"))
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
bridgeClient := bridge.NewPipeBridgeClient(path.Join(cwtchDir, "testing/clientPipe"), path.Join(cwtchDir, "testing/servicePipe"))
bridgeService := bridge.NewPipeBridgeService(path.Join(cwtchDir, "testing/servicePipe"), path.Join(cwtchDir, "testing/clientPipe"))
bridgeClient := bridge.NewPipeBridgeClient("./clientPipe", "./servicePipe")
bridgeService := bridge.NewPipeBridgeService("./servicePipe", "./clientPipe")
appClient := app2.NewAppClient("./storage", bridgeClient)
appService := app2.NewAppService(acn, "./storage", bridgeService)
@ -201,43 +164,27 @@ func TestCwtchPeerIntegration(t *testing.T) {
appClient.CreatePeer("carol", "asdfasdf")
alice := utils.WaitGetPeer(app, "alice")
fmt.Println("Alice created:", alice.GetOnion())
alice.SetAttribute(attr.GetPublicScope("name"), "Alice")
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
app.AddPeerPlugin(alice.GetProfile().Onion, plugins.NETWORKCHECK)
fmt.Println("Alice created:", alice.GetProfile().Onion)
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite})
bob := utils.WaitGetPeer(app, "bob")
fmt.Println("Bob created:", bob.GetOnion())
bob.SetAttribute(attr.GetPublicScope("name"), "Bob")
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
fmt.Println("Bob created:", bob.GetProfile().Onion)
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite})
carol := utils.WaitGetPeer(appClient, "carol")
fmt.Println("Carol created:", carol.GetOnion())
carol.SetAttribute(attr.GetPublicScope("name"), "Carol")
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
fmt.Println("Carol created:", carol.GetProfile().Onion)
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite})
app.LaunchPeers()
appClient.LaunchPeers()
waitTime := time.Duration(60) * time.Second
t.Logf("** Waiting for Alice, Bob, and Carol to connect with onion network... (%v)\n", waitTime)
time.Sleep(waitTime)
fmt.Println("Waiting for Alice, Bob, and Carol to connect with onion network...")
time.Sleep(time.Second * 90)
numGoRoutinesPostPeerStart := runtime.NumGoroutine()
fmt.Println("** Wait Done!")
// ***** Peering, server joining, group creation / invite *****
fmt.Println("Alice joining server...")
if err := alice.AddServer(string(serverKeyBundle)); err != nil {
t.Fatalf("Failed to Add Server Bundle %v", err)
}
alice.JoinServer(serverAddr)
fmt.Println("Alice peering with Bob...")
alice.PeerWithOnion(bob.GetOnion())
fmt.Println("Alice peering with Carol...")
alice.PeerWithOnion(carol.GetOnion())
fmt.Println("Creating group on ", serverAddr, "...")
groupID, _, err := alice.StartGroup(serverAddr)
fmt.Printf("Created group: %v!\n", groupID)
@ -246,131 +193,100 @@ func TestCwtchPeerIntegration(t *testing.T) {
return
}
fmt.Println("Alice peering with Bob...")
alice.AddContact("Bob", bob.GetProfile().Onion, false) // Add contact so we can track connection state
alice.PeerWithOnion(bob.GetProfile().Onion)
fmt.Println("Alice peering with Carol...")
alice.AddContact("Carol", carol.GetProfile().Onion, false)
alice.PeerWithOnion(carol.GetProfile().Onion)
fmt.Println("Alice joining server...")
alice.JoinServer(serverAddr)
fmt.Println("Bob joining server...")
bob.JoinServer(serverAddr)
fmt.Println("Waiting for alice to join server...")
waitForPeerGroupConnection(t, alice, groupID)
//fmt.Println("Waiting for bob to join server...")
//waitForPeerGroupConnection(t, bob, groupID)
fmt.Println("Waiting for alice and Bob to peer...")
waitForPeerPeerConnection(t, alice, bob)
// Need to add contact else SetContactAuth fails on peer peer doesnt exist
// Normal flow would be Bob app monitors for the new connection (a new connection state change to Auth
// and the adds the user to peer, and then approves or blocks it
bob.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
bob.AddServer(string(serverKeyBundle))
bob.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
waitForPeerPeerConnection(t, alice, carol)
carol.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
carol.AddServer(string(serverKeyBundle))
carol.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
fmt.Println("Alice and Bob getVal public.name...")
alice.SendGetValToPeer(bob.GetOnion(), attr.PublicScope, "name")
bob.SendGetValToPeer(alice.GetOnion(), attr.PublicScope, "name")
alice.SendGetValToPeer(carol.GetOnion(), attr.PublicScope, "name")
carol.SendGetValToPeer(alice.GetOnion(), attr.PublicScope, "name")
time.Sleep(10 * time.Second)
aliceName, exists := bob.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope("name"))
if !exists || aliceName != "Alice" {
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v\n", exists)
}
fmt.Printf("Bob has alice's name as '%v'\n", aliceName)
bobName, exists := alice.GetContactAttribute(bob.GetOnion(), attr.GetPeerScope("name"))
if !exists || bobName != "Bob" {
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name\n")
}
fmt.Printf("Alice has bob's name as '%v'\n", bobName)
aliceName, exists = carol.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope("name"))
if !exists || aliceName != "Alice" {
t.Fatalf("carol GetKeyVal error for alice peer.name %v\n", exists)
}
carolName, exists := alice.GetContactAttribute(carol.GetOnion(), attr.GetPeerScope("name"))
if !exists || carolName != "Carol" {
t.Fatalf("alice GetKeyVal error, carol peer.name\n")
}
fmt.Printf("Alice has carol's name as '%v'\n", carolName)
/*fmt.Println("Waiting for peerings and server joins...")
time.Sleep(time.Second * 240)*/
fmt.Println("Alice inviting Bob to group...")
err = alice.InviteOnionToGroup(bob.GetOnion(), groupID)
err = alice.InviteOnionToGroup(bob.GetProfile().Onion, groupID)
if err != nil {
t.Fatalf("Error for Alice inviting Bob to group: %v", err)
}
time.Sleep(time.Second * 5)
time.Sleep(time.Second * 10)
fmt.Println("Bob examining groups and accepting invites...")
for _, message := range bob.GetContact(alice.GetOnion()).Timeline.GetMessages() {
fmt.Printf("Found message from Alice: %v", message.Message)
if strings.HasPrefix(message.Message, "torv3") {
gid, err := bob.ImportGroup(message.Message)
if err == nil {
fmt.Printf("Bob found invite...now accepting %v...", gid)
bob.AcceptInvite(gid)
} else {
t.Fatalf("Bob could not accept invite...%v", gid)
}
for _, groupID := range bob.GetGroups() {
group := bob.GetGroup(groupID)
fmt.Printf("Bob group: %v (Accepted: %v)\n", group.GroupID, group.Accepted)
if group.Accepted == false {
fmt.Printf("Bob received and accepting group invite: %v\n", group.GroupID)
bob.AcceptInvite(group.GroupID)
}
}
fmt.Println("Waiting for Bob to join connect to group server...")
waitForPeerGroupConnection(t, bob, groupID)
time.Sleep(time.Second * 5)
numGoRoutinesPostServerConnect := runtime.NumGoroutine()
// Wait for them to join the server
waitForPeerGroupConnection(t, alice, groupID)
waitForPeerGroupConnection(t, bob, groupID)
//numGouRoutinesPostServerConnect := runtime.NumGoroutine()
// ***** Conversation *****
fmt.Println("Starting conversation in group...")
// Conversation
fmt.Printf("%v> %v\n", aliceName, aliceLines[0])
_, err = alice.SendMessageToGroupTracked(groupID, aliceLines[0])
fmt.Println("Alice> ", aliceLines[0])
err = alice.SendMessageToGroup(groupID, aliceLines[0])
if err != nil {
t.Fatalf("Alice failed to send a message to the group: %v", err)
}
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v\n", bobName, bobLines[0])
_, err = bob.SendMessageToGroupTracked(groupID, bobLines[0])
fmt.Println("Bob> ", bobLines[0])
err = bob.SendMessageToGroup(groupID, bobLines[0])
if err != nil {
t.Fatalf("Bob failed to send a message to the group: %v", err)
}
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v\n", aliceName, aliceLines[1])
alice.SendMessageToGroupTracked(groupID, aliceLines[1])
fmt.Println("Alice> ", aliceLines[1])
alice.SendMessageToGroup(groupID, aliceLines[1])
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v\n", bobName, bobLines[1])
bob.SendMessageToGroupTracked(groupID, bobLines[1])
fmt.Println("Bob> ", bobLines[1])
bob.SendMessageToGroup(groupID, bobLines[1])
time.Sleep(time.Second * 10)
fmt.Println("Alice inviting Carol to group...")
err = alice.InviteOnionToGroup(carol.GetOnion(), groupID)
err = alice.InviteOnionToGroup(carol.GetProfile().Onion, groupID)
if err != nil {
t.Fatalf("Error for Alice inviting Carol to group: %v", err)
}
time.Sleep(time.Second * 60) // Account for some token acquisition in Alice and Bob flows.
time.Sleep(time.Second * 10)
fmt.Println("Carol examining groups and accepting invites...")
for _, message := range carol.GetContact(alice.GetOnion()).Timeline.GetMessages() {
fmt.Printf("Found message from Alice: %v", message.Message)
if strings.HasPrefix(message.Message, "torv3") {
gid, err := carol.ImportGroup(message.Message)
if err == nil {
fmt.Printf("Carol found invite...now accepting %v...", gid)
carol.AcceptInvite(gid)
} else {
t.Fatalf("Carol could not accept invite...%v", gid)
}
for _, groupID := range carol.GetGroups() {
group := carol.GetGroup(groupID)
fmt.Printf("Carol group: %v (Accepted: %v)\n", group.GroupID, group.Accepted)
if group.Accepted == false {
fmt.Printf("Carol received and accepting group invite: %v\n", group.GroupID)
carol.AcceptInvite(group.GroupID)
}
}
fmt.Println("Shutting down Alice...")
app.ShutdownPeer(alice.GetOnion())
app.ShutdownPeer(alice.GetProfile().Onion)
time.Sleep(time.Second * 5)
numGoRoutinesPostAlice := runtime.NumGoroutine()
@ -379,15 +295,13 @@ func TestCwtchPeerIntegration(t *testing.T) {
waitForPeerGroupConnection(t, carol, groupID)
numGoRotinesPostCarolConnect := runtime.NumGoroutine()
fmt.Printf("%v> %v", bobName, bobLines[2])
bob.SendMessageToGroupTracked(groupID, bobLines[2])
// Bob should have enough tokens so we don't need to account for
// token acquisition here...
fmt.Println("Bob> ", bobLines[2])
bob.SendMessageToGroup(groupID, bobLines[2])
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v", carolName, carolLines[0])
carol.SendMessageToGroupTracked(groupID, carolLines[0])
time.Sleep(time.Second * 30) // we need to account for spam-based token acquisition, but everything should
// be warmed-up and delays should be pretty small.
fmt.Println("Carol> ", carolLines[0])
carol.SendMessageToGroup(groupID, carolLines[0])
time.Sleep(time.Second * 10)
// ***** Verify Test *****
@ -414,14 +328,14 @@ func TestCwtchPeerIntegration(t *testing.T) {
fmt.Printf("Bob's TimeLine:\n")
bobVerified := printAndCountVerifedTimeline(t, bobsGroup.GetTimeline())
if bobVerified != 6 {
t.Errorf("Bob did not have 6 verified messages")
t.Errorf("Bob did not have 5 verified messages")
}
carolsGroup := carol.GetGroup(groupID)
fmt.Printf("Carol's TimeLine:\n")
carolVerified := printAndCountVerifedTimeline(t, carolsGroup.GetTimeline())
if carolVerified != 6 {
t.Errorf("Carol did not have 6 verified messages")
t.Errorf("Carol did not have 3 verified messages")
}
if len(alicesGroup.GetTimeline()) != 4 {
@ -460,7 +374,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
}
fmt.Println("Shutting down Bob...")
app.ShutdownPeer(bob.GetOnion())
app.ShutdownPeer(bob.GetProfile().Onion)
time.Sleep(time.Second * 3)
numGoRoutinesPostBob := runtime.NumGoroutine()
if server != nil {
@ -471,7 +385,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
numGoRoutinesPostServerShutdown := runtime.NumGoroutine()
fmt.Println("Shutting down Carol...")
appClient.ShutdownPeer(carol.GetOnion())
appClient.ShutdownPeer(carol.GetProfile().Onion)
time.Sleep(time.Second * 3)
numGoRoutinesPostCarol := runtime.NumGoroutine()
@ -512,4 +426,5 @@ func TestCwtchPeerIntegration(t *testing.T) {
if numGoRoutinesStart != numGoRoutinesPostACN {
t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, leak detected!", numGoRoutinesStart, numGoRoutinesPostACN)
}
}

View File

@ -9,8 +9,16 @@ go test -race ${1} -coverprofile=storage.v0.cover.out -v ./storage/v0
go test -race ${1} -coverprofile=storage.v1.cover.out -v ./storage/v1
go test -race ${1} -coverprofile=storage.cover.out -v ./storage
go test -race ${1} -coverprofile=peer.connections.cover.out -v ./protocol/connections
go test -race ${1} -coverprofile=protocol.spam.cover.out -v ./protocol/connections/spam
go test -race ${1} -coverprofile=peer.fetch.cover.out -v ./protocol/connections/fetch
go test -race ${1} -coverprofile=peer.listen.cover.out -v ./protocol/connections/listen
go test -race ${1} -coverprofile=peer.send.cover.out -v ./protocol/connections/send
go test -race ${1} -coverprofile=peer.cover.out -v ./peer
go test -race ${1} -coverprofile=server.fetch.cover.out -v ./server/fetch
go test -race ${1} -coverprofile=server.listen.cover.out -v ./server/listen
go test -race ${1} -coverprofile=server.send.cover.out -v ./server/send
go test -race ${1} -coverprofile=server.metrics.cover.out -v ./server/metrics
go test -race ${1} -coverprofile=server.cover.out -v ./server
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
rm -rf *.cover.out