Compare commits

..

1 Commits

114 changed files with 5416 additions and 8947 deletions

View File

@ -5,43 +5,28 @@ workspace:
pipeline:
fetch:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/bin/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/torrc
- chmod a+x tor
- go get -u golang.org/x/lint/golint
- export GO111MODULE=on
- go mod vendor
- go list ./... | xargs go get
- go get -u github.com/golang/lint/golint
quality:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
commands:
- go list ./... | xargs go vet
- go list ./... | xargs golint -set_exit_status
units-tests:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
commands:
- export PATH=$PATH:/go/src/cwtch.im/cwtch
- sh testing/tests.sh
integ-test:
image: golang
when:
repo: cwtch.im/cwtch
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
@ -49,15 +34,10 @@ pipeline:
skip_verify: true
from: drone@openprivacy.ca
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
status: [ failure ]
status: [ success, changed, failure ]
notify-gogs:
image: openpriv/drone-gogs
when:
repo: cwtch.im/cwtch
branch: master
event: pull_request
status: [ success, changed, failure ]
secrets: [gogs_account_token]

7
.gitignore vendored
View File

@ -3,12 +3,9 @@
*private_key*
*.messages
*.test
*/*test_*
*/*_test*
*.json
*/messages/*
server/app/messages
.reviewboardrc
/vendor/
/testing/tor/
/storage/*/testing/
/storage/testing/
/testing/storage/

View File

@ -1,60 +0,0 @@
image: golang:latest
#before_script:
stages:
- test
- docker-push
- deploy-staging
test-server:
stage: test
script:
- mkdir /go/src/cwtch.im
- ln -s /builds/${CI_PROJECT_NAMESPACE}/cwtch /go/src/cwtch.im/cwtch
- cd /go/src/cwtch.im/cwtch/server/app/
- go get
- go tool vet -composites=false -shadow=true *.go
- go test
test-client:
stage: test
script:
- mkdir /go/src/cwtch.im
- ln -s /builds/${CI_PROJECT_NAMESPACE}/cwtch /go/src/cwtch.im/cwtch
- cd /go/src/cwtch.im/cwtch/app/cli/
- go get
- go tool vet -composites=false -shadow=true *.go
- go test
# We don't really care about the client here but it's useful to know what's
# happening on t'other side of the coin
allow_failure: true
gitlab-registry:
stage: docker-push
image: docker:latest
services:
- docker:dind
tags:
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN ${CI_REGISTRY}
- docker build -t ${CI_REGISTRY_IMAGE}:latest -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA:0:8} .
- docker push ${CI_REGISTRY_IMAGE}:latest
- docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA:0:8}
dependencies:
- test-server
docker-hub:
stage: docker-push
image: docker:latest
services:
- docker:dind
tags:
script:
- docker login -u ${DOCKER_HUB_ID} -p ${DOCKER_HUB_PASSWORD} registry.hub.docker.com
- docker build -t registry.hub.docker.com/${DOCKER_HUB_ID}/cwtch:latest -t registry.hub.docker.com/${DOCKER_HUB_ID}/cwtch:${CI_COMMIT_SHA:0:8} .
- docker push registry.hub.docker.com/${DOCKER_HUB_ID}/cwtch:latest
- docker push registry.hub.docker.com/${DOCKER_HUB_ID}/cwtch:${CI_COMMIT_SHA:0:8}
dependencies:
- test-server

View File

@ -1,78 +0,0 @@
FROM golang as server-build-stage
ENV CGO_ENABLED=0 GOOS=linux
WORKDIR /go/src/cwtch.im/cwtch
COPY . .
RUN go get -d -v ./...
#RUN go install -v ./...
WORKDIR /go/src/cwtch.im/cwtch/server/app/
RUN go build -ldflags "-extldflags '-static'"
#----------------------------------------------
FROM alpine:latest as tor-build-stage
# Install prerequisites
RUN apk --no-cache add --update \
gnupg \
build-base \
libevent \
libevent-dev \
libressl \
libressl-dev \
xz-libs \
xz-dev \
zlib \
zlib-dev \
zstd \
zstd-dev \
&& wget -q https://www.torproject.org/dist/tor-0.3.5.3-alpha.tar.gz \
&& tar xf tor-0.3.5.3-alpha.tar.gz \
&& cd tor-0.3.5.3-alpha \
&& ./configure \
&& make install \
&& ls -R /usr/local/
FROM alpine:latest
MAINTAINER Ablative Hosting <support@ablative.hosting>
#BSD habits die hard
ENV TOR_USER=_tor
# Installing dependencies of Tor and pwgen
RUN apk --no-cache add --update \
libevent \
libressl \
xz-libs \
zlib \
zstd \
zstd-dev \
pwgen
# Copy Tor
COPY --from=tor-build-stage /usr/local/ /usr/local/
# Create an unprivileged tor user
RUN addgroup -S $TOR_USER && adduser -G $TOR_USER -S $TOR_USER && adduser -G _tor -S cwtchd && mkdir /run/tor
# Copy Tor configuration file
COPY ./server/docker/torrc /etc/tor/torrc
# Copy docker-entrypoint
COPY ./server/docker/docker-entrypoint /usr/local/bin/
# Copy across cwtch
COPY --from=server-build-stage /go/src/cwtch.im/cwtch/server/app/app /usr/local/bin/cwtch_server
# Persist data
VOLUME /etc/tor /var/lib/tor /etc/cwtch
ENTRYPOINT ["docker-entrypoint"]
#cwtchd is in the _tor group so can access the socket but that's it
#USER cwtchd
#Launches the cwtchd daemon
CMD ["/usr/local/bin/cwtch_server"]

View File

@ -19,7 +19,7 @@ We seek to protect the following communication contexts:
Beyond individual conversations, we also seek to defend against context correlation attacks, whereby multiple conversations are analyzed to derive higher level information:
* **Relationships** - Discovering social relationships between parties by analyzing the frequency and length of their communications over a period of time. (Carol and Eve call each other every single day for multiple hours at a time.)
* **Cliques** - Discovering social relationships between multiple parties by deriving casual communication chains from their communication metadata (e.g. everytime Alice talks to Bob she talks to Carol almost immediately after.)
* **Cliques** - Discovering social relationships between multiple parties by deriving casual communication chains from their communication metadata (e.g. everytime Alice talks to Bob she talks to Carol almost immediately after.)
* **Pattern of Life** - Discovering which communications are cyclical and predictable. (e.g. Alice calls Eve every Monday evening for around an hour.)
@ -27,32 +27,4 @@ More Information: [https://cwtch.im](https://cwtch.im)
Development and Contributing information in [CONTRIBUTING.md](https://git.openprivacy.ca/cwtch.im/cwtch/src/master/CONTRIBUTING.md)
## Running Cwtch
### Server
#### Docker
This repository contains a `Dockerfile` allowing you to build and run the server as a [docker](https://www.docker.com/) container.
To get started issue `docker build -t openpriv/cwtch-server:latest .`, this will create 2 temporary docker containers, one to build the Tor daemon and one to build Cwtch. The compiled binaries will then be bundled into a new image and tagged as `openpriv/cwtch-server:latest`.
To run Cwtch in the foreground execute `docker run openpriv/cwtch-server:latest`, you will see a small amount of output from Tor and then Cwtch will output your server address. When you `Ctrl + C` the container will terminate. To run Cwtch in the background execute `docker run --name my-cwtch-server -d openpriv/cwtch-server:latest`. To get your Cwtch server address issue `docker logs my-cwtch-server`.
The image creates 3 volumes, for /etc/cwtch, /etc/tor, /var/lib/tor
##### Upgrading
To upgrade with continuity
```
# Stop current container/service
docker stop my-cwtch-server
docker pull openpriv/cwtch-server
# Create a new container and copy the volumes (cwtch/onion keys, message store)
docker create --name my-cwtch-server-2 --volumes-from my-cwtch-server openpriv/cwtch-server:latest
# Resume service with the new container
docker start my-cwtch-server-2
```
![](https://git.openprivacy.ca/avatars/5?s=140)

View File

@ -1,294 +1,182 @@
package app
import (
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"crypto/rand"
"cwtch.im/cwtch/connectivity/tor"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/storage"
"encoding/hex"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"log"
"os"
"path"
"strconv"
"path/filepath"
"sync"
)
// AttributeTag is a const name for a peer attribute that can be set at creation time, for example for versioning info
const AttributeTag = "tag"
type applicationCore struct {
eventBuses map[string]event.Manager
directory string
coremutex sync.Mutex
}
type application struct {
applicationCore
appletPeers
appletACN
appletPlugins
storage map[string]storage.ProfileStore
engines map[string]connections.Engine
appBus event.Manager
appmutex sync.Mutex
peers map[string]peer.CwtchPeer
torManager *tor.Manager
directory string
mutex sync.Mutex
primaryonion string
}
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
type Application interface {
LoadProfiles(password string)
CreatePeer(name string, password string)
CreateTaggedPeer(name string, password string, tag string)
DeletePeer(onion string)
AddPeerPlugin(onion string, pluginID plugins.PluginID)
ChangePeerPassword(onion, oldpass, newpass string)
LaunchPeers()
GetPrimaryBus() event.Manager
GetEventBus(onion string) event.Manager
QueryACNStatus()
QueryACNVersion()
ShutdownPeer(string)
Shutdown()
LoadProfiles(password string) error
CreatePeer(name string, password string) (peer.CwtchPeer, error)
PrimaryIdentity() peer.CwtchPeer
GetPeer(onion string) peer.CwtchPeer
ListPeers() map[string]string
}
// LoadProfileFn is the function signature for a function in an app that loads a profile
type LoadProfileFn func(profile *model.Profile, store storage.ProfileStore)
GetTorStatus() (map[string]string, error)
func newAppCore(appDirectory string) *applicationCore {
appCore := &applicationCore{eventBuses: make(map[string]event.Manager), directory: appDirectory}
os.MkdirAll(path.Join(appCore.directory, "profiles"), 0700)
return appCore
Shutdown()
}
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
func NewApp(acn connectivity.ACN, appDirectory string) Application {
log.Debugf("NewApp(%v)\n", appDirectory)
app := &application{storage: make(map[string]storage.ProfileStore), engines: make(map[string]connections.Engine), applicationCore: *newAppCore(appDirectory), appBus: event.NewEventManager()}
app.appletPeers.init()
func NewApp(appDirectory string, torPath string) (Application, error) {
log.Printf("NewApp(%v, %v)\n", appDirectory, torPath)
app := &application{peers: make(map[string]peer.CwtchPeer), directory: appDirectory}
os.MkdirAll(path.Join(appDirectory, "tor"), 0700)
os.Mkdir(path.Join(app.directory, "profiles"), 0700)
app.appletACN.init(acn, app.getACNStatusHandler())
return app
err := app.startTor(torPath)
if err != nil {
return nil, err
}
return app, nil
}
// CreatePeer creates a new Peer with a given name and core required accessories (eventbus)
func (ac *applicationCore) CreatePeer(name string) (*model.Profile, error) {
log.Debugf("CreatePeer(%v)\n", name)
func generateRandomFilename() string {
randBytes := make([]byte, 16)
rand.Read(randBytes)
return filepath.Join(hex.EncodeToString(randBytes))
}
profile := storage.NewProfile(name)
// NewProfile creates a new cwtchPeer with a given name.
func (app *application) CreatePeer(name string, password string) (peer.CwtchPeer, error) {
log.Printf("CreatePeer(%v)\n", name)
ac.coremutex.Lock()
defer ac.coremutex.Unlock()
randomFileName := generateRandomFilename()
p, err := peer.NewCwtchPeer(name, password, path.Join(app.directory, "profiles", randomFileName))
if err != nil {
return nil, err
}
err = p.Save()
if err != nil {
p.Shutdown() //attempt
return nil, fmt.Errorf("Error attempting to save new profile: %v", err)
}
app.startPeer(p)
_, exists := ac.eventBuses[profile.Onion]
_, exists := app.peers[p.GetProfile().Onion]
if exists {
return nil, fmt.Errorf("error: profile for onion %v already exists", profile.Onion)
p.Shutdown()
return nil, fmt.Errorf("Error: profile for onion %v already exists", p.GetProfile().Onion)
}
app.mutex.Lock()
app.peers[p.GetProfile().Onion] = p
app.mutex.Unlock()
eventBus := event.NewEventManager()
ac.eventBuses[profile.Onion] = eventBus
return profile, nil
return p, nil
}
func (ac *applicationCore) DeletePeer(onion string) {
ac.coremutex.Lock()
defer ac.coremutex.Unlock()
ac.eventBuses[onion].Shutdown()
delete(ac.eventBuses, onion)
}
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
profile, err := app.applicationCore.CreatePeer(name)
func (app *application) LoadProfiles(password string) error {
files, err := ioutil.ReadDir(path.Join(app.directory, "profiles"))
if err != nil {
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
return
}
profileStore := storage.CreateProfileWriterStore(app.eventBuses[profile.Onion], path.Join(app.directory, "profiles", profile.LocalID), password, profile)
app.storage[profile.Onion] = profileStore
pc := app.storage[profile.Onion].GetProfileCopy(true)
p := peer.FromProfile(pc)
p.Init(app.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
// 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)
app.peers[profile.Onion] = p
app.engines[profile.Onion] = engine
if tag != "" {
p.SetAttribute(AttributeTag, tag)
}
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.True}))
}
// CreatePeer creates a new Peer with the given name and required accessories (eventbus, storage, protocol engine)
func (app *application) CreatePeer(name string, password string) {
app.CreateTaggedPeer(name, password, "")
}
func (app *application) DeletePeer(onion string) {
log.Infof("DeletePeer called on %v\n", onion)
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.appletPlugins.ShutdownPeer(onion)
app.plugins.Delete(onion)
app.peers[onion].Shutdown()
delete(app.peers, onion)
app.engines[onion].Shutdown()
delete(app.engines, onion)
app.storage[onion].Shutdown()
app.storage[onion].Delete()
delete(app.storage, onion)
app.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
app.applicationCore.DeletePeer(onion)
log.Debugf("Delete peer for %v Done\n", onion)
}
func (app *application) ChangePeerPassword(onion, oldpass, newpass string) {
app.eventBuses[onion].Publish(event.NewEventList(event.ChangePassword, event.Password, oldpass, event.NewPassword, newpass))
}
func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
app.AddPlugin(onion, pluginID, app.eventBuses[onion], app.acn)
}
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
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 {
eventBus := event.NewEventManager()
profileStore, err := storage.LoadProfileWriterStore(eventBus, path.Join(ac.directory, "profiles", file.Name()), password)
p, err := peer.LoadCwtchPeer(path.Join(app.directory, "profiles", file.Name()), password)
if err != nil {
continue
}
profile := profileStore.GetProfileCopy(timeline)
_, exists := ac.eventBuses[profile.Onion]
_, exists := app.peers[p.GetProfile().Onion]
if exists {
profileStore.Shutdown()
eventBus.Shutdown()
log.Errorf("profile for onion %v already exists", profile.Onion)
p.Shutdown()
log.Printf("Error: profile for onion %v already exists", p.GetProfile().Onion)
continue
}
ac.coremutex.Lock()
ac.eventBuses[profile.Onion] = eventBus
ac.coremutex.Unlock()
loadProfileFn(profile, profileStore)
}
return nil
}
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
func (app *application) LoadProfiles(password string) {
count := 0
app.applicationCore.LoadProfiles(password, true, func(profile *model.Profile, profileStore storage.ProfileStore) {
peer := peer.FromProfile(profile)
peer.Init(app.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
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}))
count++
})
if count == 0 {
message := event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0)
app.appBus.Publish(message)
}
}
// GetPrimaryBus returns the bus the Application uses for events that aren't peer specific
func (app *application) GetPrimaryBus() event.Manager {
return app.appBus
}
// GetEventBus returns a cwtchPeer's event bus
func (ac *applicationCore) GetEventBus(onion string) event.Manager {
if manager, ok := ac.eventBuses[onion]; ok {
return manager
}
return nil
}
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))
for _, bus := range app.eventBuses {
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
app.startPeer(p)
app.mutex.Lock()
app.peers[p.GetProfile().Onion] = p
if app.primaryonion == "" {
app.primaryonion = p.GetProfile().Onion
}
app.peerLock.Unlock()
app.mutex.Unlock()
}
return nil
}
func (app *application) QueryACNStatus() {
prog, status := app.acn.GetBootstrapStatus()
app.getACNStatusHandler()(prog, status)
// startTor will create a local torrc if needed
func (app *application) startTor(torPath string) error {
// Creating a local cwtch tor server config for the user
// creating $app.directory/torrc file
// SOCKSPort socksPort
// ControlPort controlPort
torrc := path.Join(app.directory, "tor", "torrc")
if _, err := os.Stat(torrc); os.IsNotExist(err) {
log.Printf("writing torrc to: %v\n", torrc)
file, err := os.Create(torrc)
if err != nil {
return err
}
fmt.Fprintf(file, "SOCKSPort %d\nControlPort %d\nCookieAuthentication 0\nSafeSocks 1\n", 9050, 9051)
file.Close()
}
tm, err := tor.NewTorManager(9050, 9051, torPath, torrc)
if err != nil {
return err
}
app.torManager = tm
return nil
}
func (app *application) QueryACNVersion() {
version := app.acn.GetVersion()
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
func (app *application) startPeer(peer peer.CwtchPeer) {
go func() {
e := peer.Listen()
if e != nil {
log.Fatalf("ERROR: peer %v has crashed with: %v\n", peer.GetProfile().Onion, e)
}
}()
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (app *application) ShutdownPeer(onion string) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.eventBuses[onion].Shutdown()
delete(app.eventBuses, onion)
app.peers[onion].Shutdown()
delete(app.peers, onion)
app.engines[onion].Shutdown()
delete(app.engines, onion)
app.storage[onion].Shutdown()
delete(app.storage, onion)
app.appletPlugins.Shutdown()
// ListPeers returns a map of onions to their profile's Name
func (app *application) ListPeers() map[string]string {
keys := map[string]string{}
for k, p := range app.peers {
keys[k] = p.GetProfile().Name
}
return keys
}
// PrimaryIdentity returns a Peer for a given onion address
func (app *application) PrimaryIdentity() peer.CwtchPeer {
return app.peers[app.primaryonion]
}
// GetPeer returns a Peer for a given onion address
func (app *application) GetPeer(onion string) peer.CwtchPeer {
if peer, ok := app.peers[onion]; ok {
return peer
}
return nil
}
// GetTorStatus returns tor control port bootstrap-phase status info in a map
func (app *application) GetTorStatus() (map[string]string, error) {
return app.torManager.GetStatus()
}
// Shutdown shutsdown all peers of an app and then the tormanager
func (app *application) Shutdown() {
for id, peer := range app.peers {
for _, peer := range app.peers {
peer.Shutdown()
app.appletPlugins.ShutdownPeer(id)
app.engines[id].Shutdown()
app.storage[id].Shutdown()
app.eventBuses[id].Shutdown()
}
app.appBus.Shutdown()
app.torManager.Shutdown()
}

View File

@ -1,39 +0,0 @@
package app
import "cwtch.im/cwtch/event"
import "git.openprivacy.ca/openprivacy/log"
const (
// DestApp should be used as a destination for IPC messages that are for the application itself an not a peer
DestApp = "app"
)
type applicationBridge struct {
applicationCore
bridge event.IPCBridge
handle func(*event.Event)
}
func (ab *applicationBridge) listen() {
log.Infoln("ab.listen()")
for {
ipcMessage, ok := ab.bridge.Read()
log.Debugf("listen() got %v for %v\n", ipcMessage.Message.EventType, ipcMessage.Dest)
if !ok {
log.Debugln("exiting appBridge.listen()")
return
}
if ipcMessage.Dest == DestApp {
ab.handle(&ipcMessage.Message)
} else {
if eventBus, exists := ab.eventBuses[ipcMessage.Dest]; exists {
eventBus.PublishLocal(ipcMessage.Message)
}
}
}
}
func (ab *applicationBridge) Shutdown() {
}

View File

@ -1,176 +0,0 @@
package app
import (
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/storage"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"path"
"strconv"
"sync"
)
type applicationClient struct {
applicationBridge
appletPeers
appBus event.Manager
acmutex sync.Mutex
}
// NewAppClient returns an Application that acts as a client to a AppService, connected by the IPCBridge supplied
func NewAppClient(appDirectory string, bridge event.IPCBridge) Application {
appClient := &applicationClient{appletPeers: appletPeers{peers: make(map[string]peer.CwtchPeer)}, applicationBridge: applicationBridge{applicationCore: *newAppCore(appDirectory), bridge: bridge}, appBus: event.NewEventManager()}
appClient.handle = appClient.handleEvent
go appClient.listen()
appClient.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadClient)})
log.Infoln("Created new App Client")
return appClient
}
// GetPrimaryBus returns the bus the Application uses for events that aren't peer specific
func (ac *applicationClient) GetPrimaryBus() event.Manager {
return ac.appBus
}
func (ac *applicationClient) handleEvent(ev *event.Event) {
switch ev.EventType {
case event.NewPeer:
localID := ev.Data[event.Identity]
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)
case event.DeletePeer:
onion := ev.Data[event.Identity]
ac.handleDeletedPeer(onion)
case event.PeerError:
ac.appBus.Publish(*ev)
case event.AppError:
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) {
var keyBytes [32]byte
var saltBytes [128]byte
copy(keyBytes[:], key)
copy(saltBytes[:], salt)
profile, err := storage.ReadProfile(path.Join(ac.directory, "profiles", localID), keyBytes, saltBytes)
if err != nil {
log.Errorf("Could not read profile for NewPeer event: %v\n", err)
ac.appBus.Publish(event.NewEventList(event.PeerError, event.Error, fmt.Sprintf("Could not read profile for NewPeer event: %v\n", err)))
return
}
_, exists := ac.peers[profile.Onion]
if exists {
log.Errorf("profile for onion %v already exists", profile.Onion)
ac.appBus.Publish(event.NewEventList(event.PeerError, event.Error, fmt.Sprintf("profile for onion %v already exists", profile.Onion)))
return
}
eventBus := event.NewIPCEventManager(ac.bridge, profile.Onion)
peer := peer.FromProfile(profile)
peer.Init(eventBus)
ac.peerLock.Lock()
defer ac.peerLock.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})
if reload {
npEvent.Data[event.Status] = event.StorageRunning
}
ac.appBus.Publish(npEvent)
if reload {
ac.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadPeer, event.Identity, profile.Onion)})
}
}
// CreatePeer messages the service to create a new Peer with the given name
func (ac *applicationClient) CreatePeer(name string, password string) {
ac.CreateTaggedPeer(name, password, "")
}
func (ac *applicationClient) CreateTaggedPeer(name, password, tag string) {
log.Infof("appClient CreatePeer %v\n", name)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.CreatePeer, map[event.Field]string{event.ProfileName: name, event.Password: password, event.Data: tag})}
ac.bridge.Write(&message)
}
// DeletePeer messages tehe service to delete a peer
func (ac *applicationClient) DeletePeer(onion string) {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.DeletePeer, map[event.Field]string{event.Identity: onion})}
ac.bridge.Write(&message)
}
func (ac *applicationClient) ChangePeerPassword(onion, oldpass, newpass string) {
message := event.IPCMessage{Dest: onion, Message: event.NewEventList(event.ChangePassword, event.Password, oldpass, event.NewPassword, newpass)}
ac.bridge.Write(&message)
}
func (ac *applicationClient) handleDeletedPeer(onion string) {
ac.acmutex.Lock()
defer ac.acmutex.Unlock()
ac.peers[onion].Shutdown()
delete(ac.peers, onion)
ac.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
ac.applicationCore.DeletePeer(onion)
}
func (ac *applicationClient) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.AddPeerPlugin, map[event.Field]string{event.Identity: onion, event.Data: strconv.Itoa(int(pluginID))})}
ac.bridge.Write(&message)
}
// LoadProfiles messages the service to load any profiles for the given password
func (ac *applicationClient) LoadProfiles(password string) {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.LoadProfiles, map[event.Field]string{event.Password: password})}
ac.bridge.Write(&message)
}
func (ac *applicationClient) QueryACNStatus() {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNStatus, map[event.Field]string{})}
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()
defer ac.acmutex.Unlock()
ac.eventBuses[onion].Shutdown()
delete(ac.eventBuses, onion)
ac.peers[onion].Shutdown()
delete(ac.peers, onion)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.ShutdownPeer, map[event.Field]string{event.Identity: onion})}
ac.bridge.Write(&message)
}
// Shutdown shuts down the application client and all front end peer components
func (ac *applicationClient) Shutdown() {
for id := range ac.peers {
ac.ShutdownPeer(id)
}
ac.applicationBridge.Shutdown()
ac.appBus.Shutdown()
}

View File

@ -1,201 +0,0 @@
package app
import (
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"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"
"path"
"strconv"
"sync"
)
type applicationService struct {
applicationBridge
appletACN
appletPlugins
storage map[string]storage.ProfileStore
engines map[string]connections.Engine
asmutex sync.Mutex
}
// ApplicationService is the back end of an application that manages engines and writing storage and communicates to an ApplicationClient by an IPCBridge
type ApplicationService interface {
Shutdown()
}
// NewAppService returns an ApplicationService that runs the backend of an app and communicates with a client by the supplied IPCBridge
func NewAppService(acn connectivity.ACN, appDirectory string, bridge event.IPCBridge) ApplicationService {
appService := &applicationService{storage: make(map[string]storage.ProfileStore), engines: make(map[string]connections.Engine), applicationBridge: applicationBridge{applicationCore: *newAppCore(appDirectory), bridge: bridge}}
appService.appletACN.init(acn, appService.getACNStatusHandler())
appService.handle = appService.handleEvent
go appService.listen()
log.Infoln("Created new App Service")
return appService
}
func (as *applicationService) handleEvent(ev *event.Event) {
log.Infof("app Service handleEvent %v\n", ev.EventType)
switch ev.EventType {
case event.CreatePeer:
profileName := ev.Data[event.ProfileName]
password := ev.Data[event.Password]
tag := ev.Data[event.Data]
as.createPeer(profileName, password, tag)
case event.DeletePeer:
onion := ev.Data[event.Identity]
as.deletePeer(onion)
message := event.IPCMessage{Dest: DestApp, Message: *ev}
as.bridge.Write(&message)
case event.AddPeerPlugin:
onion := ev.Data[event.Identity]
pluginID, _ := strconv.Atoi(ev.Data[event.Data])
as.AddPlugin(onion, plugins.PluginID(pluginID), as.eventBuses[onion], as.acn)
case event.LoadProfiles:
password := ev.Data[event.Password]
as.loadProfiles(password)
case event.ReloadClient:
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)
}
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadDone)}
as.bridge.Write(&message)
case event.ReloadPeer:
onion := ev.Data[event.Identity]
events := as.storage[onion].GetStatusMessages()
for _, ev := range events {
message := event.IPCMessage{Dest: onion, Message: *ev}
as.bridge.Write(&message)
}
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)
}
}
func (as *applicationService) createPeer(name, password, tag string) {
log.Infof("app Service create peer %v %v\n", name, password)
profile, err := as.applicationCore.CreatePeer(name)
as.eventBuses[profile.Onion] = event.IPCEventManagerFrom(as.bridge, profile.Onion, as.eventBuses[profile.Onion])
if err != nil {
log.Errorf("Could not create Peer: %v\n", err)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.PeerError, event.Error, err.Error())}
as.bridge.Write(&message)
return
}
if tag != "" {
profile.SetAttribute(AttributeTag, tag)
}
profileStore := storage.CreateProfileWriterStore(as.eventBuses[profile.Onion], path.Join(as.directory, "profiles", profile.LocalID), password, profile)
peerAuthorizations := profile.ContactsAuthorizations()
// 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)
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)
}
func (as *applicationService) loadProfiles(password string) {
count := 0
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()
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], peerAuthorizations)
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)
count++
})
if count == 0 {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0)}
as.bridge.Write(&message)
}
}
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()
for _, bus := range as.eventBuses {
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
}
}
}
func (as *applicationService) deletePeer(onion string) {
as.asmutex.Lock()
defer as.asmutex.Unlock()
as.appletPlugins.ShutdownPeer(onion)
as.plugins.Delete(onion)
as.engines[onion].Shutdown()
delete(as.engines, onion)
as.storage[onion].Shutdown()
as.storage[onion].Delete()
delete(as.storage, onion)
as.applicationCore.DeletePeer(onion)
}
func (as *applicationService) ShutdownPeer(onion string) {
as.engines[onion].Shutdown()
delete(as.engines, onion)
as.storage[onion].Shutdown()
delete(as.storage, onion)
as.eventBuses[onion].Shutdown()
delete(as.eventBuses, onion)
}
// Shutdown shuts down the application Service and all peer related backend parts
func (as *applicationService) Shutdown() {
log.Debugf("shutting down application service...")
as.appletPlugins.Shutdown()
for id := range as.engines {
log.Debugf("shutting down application service peer engine %v", id)
as.ShutdownPeer(id)
}
}

View File

@ -1,121 +0,0 @@
package app
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"sync"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/peer"
)
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
}
type appletACN struct {
acn connectivity.ACN
}
type appletPlugins struct {
plugins sync.Map //map[string] []plugins.Plugin
}
// ***** applet ACN
func (a *appletACN) init(acn connectivity.ACN, publish func(int, string)) {
a.acn = acn
acn.SetStatusCallback(publish)
prog, status := acn.GetBootstrapStatus()
publish(prog, status)
}
func (a *appletACN) Shutdown() {
a.acn.Close()
}
// ***** appletPeers
func (ap *appletPeers) init() {
ap.peers = make(map[string]peer.CwtchPeer)
ap.launched = false
}
// 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)
p.Listen()
log.Debugf("done Listen() for %v\n", pid)
p.StartPeersConnections()
log.Debugf("done StartPeersConnections() for %v\n", pid)
}
ap.launched = true
}
// 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()
}
return keys
}
// GetPeer returns a cwtchPeer for a given onion address
func (ap *appletPeers) GetPeer(onion string) peer.CwtchPeer {
if peer, ok := ap.peers[onion]; ok {
return peer
}
return nil
}
// ***** applet Plugins
func (ap *appletPlugins) Shutdown() {
log.Debugf("shutting down applet plugins...")
ap.plugins.Range(func(k, v interface{}) bool {
log.Debugf("shutting down plugins for %v", k)
ap.ShutdownPeer(k.(string))
return true
})
}
func (ap *appletPlugins) ShutdownPeer(peerid string) {
log.Debugf("shutting down plugins for %v", peerid)
pluginsI, ok := ap.plugins.Load(peerid)
if ok {
plugins := pluginsI.([]plugins.Plugin)
for _, plugin := range plugins {
log.Debugf("shutting down plugin: %v", plugin)
plugin.Shutdown()
}
}
}
func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event.Manager, acn connectivity.ACN) {
if _, exists := ap.plugins.Load(peerid); !exists {
ap.plugins.Store(peerid, []plugins.Plugin{})
}
pluginsinf, _ := ap.plugins.Load(peerid)
peerPlugins := pluginsinf.([]plugins.Plugin)
newp := plugins.Get(id, bus, acn, peerid)
newp.Start()
peerPlugins = append(peerPlugins, newp)
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)
ap.plugins.Store(peerid, peerPlugins)
}

View File

@ -1,86 +0,0 @@
package main
import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/utils"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"os"
"time"
)
func waitForPeerGroupConnection(peer peer.CwtchPeer, groupID string) error {
for {
group := peer.GetGroup(groupID)
if group != nil {
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])
time.Sleep(time.Second * 10)
continue
}
} else {
return errors.New("peer server connections should have entry for server but do not")
}
break
}
return nil
}
func main() {
if len(os.Args) != 2 {
fmt.Printf("Usage: ./servermon SERVER_ADDRESS\n")
os.Exit(1)
}
serverAddr := os.Args[1]
acn, err := tor.NewTorACN(".", "")
if err != nil {
fmt.Printf("Could not start tor: %v\n", err)
os.Exit(1)
}
app := app2.NewApp(acn, ".")
app.CreatePeer("servermon", "be gay, do crimes")
botPeer := utils.WaitGetPeer(app, "servermon")
fmt.Printf("Connecting to %v...\n", serverAddr)
botPeer.JoinServer(serverAddr)
groupID, _, err := botPeer.StartGroup(serverAddr)
if err != nil {
fmt.Printf("Error creating group on server %v: %v\n", serverAddr, err)
os.Exit(1)
}
err = waitForPeerGroupConnection(botPeer, groupID)
if err != nil {
fmt.Printf("Could not connect to server %v: %v\n", serverAddr, err)
os.Exit(1)
}
timeout := 1 * time.Second
timeElapsed := 0 * time.Second
for {
_, err := botPeer.SendMessageToGroupTracked(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)
} else {
fmt.Printf("Successfully sent message to %v at interval %v of total %v\n", serverAddr, timeout, timeElapsed)
}
time.Sleep(timeout)
timeElapsed += timeout
if timeout < 2*time.Minute {
timeout = timeout * 2
}
}
}

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

@ -0,0 +1,591 @@
package main
import (
app2 "cwtch.im/cwtch/app"
peer2 "cwtch.im/cwtch/peer"
"bytes"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/peer/connections"
"fmt"
"github.com/c-bata/go-prompt"
"golang.org/x/crypto/ssh/terminal"
"log"
"os"
"os/exec"
"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 main() {
cwtch :=
`
#, #'
@@@@@@:
@@@@@@.
@'@@+#' @@@@+
''''''@ #+@ :
@''''+;+' . '
@''@' :+' , ; ##, +'
,@@ ;' #'#@''. #''@''#
# ''''''#:,,#'''''@
: @''''@ :+'''@
' @;+'@ @'#
.:# '#..# '# @
@@@@@@
@@@@@@
'@@@@
@# . .
+++, #'@+'@
''', ''''''#
.#+# ''', @'''+,
@''# ''', .#@
:; '@''# .;. ''', ' : ;. ,
@+'''@ '+'+ @++ @+'@+''''+@ #+'''#: ''';#''+@ @@@@ @@@@@@@@@ :@@@@#
#''''''# +''. +'': +'''''''''+ @'''''''# '''+'''''@ @@@@ @@@@@@@@@@@@@@@@:
@'''@@'''@ @''# ,'''@ ''+ @@''+#+ :'''@@+''' ''''@@'''' @@@@ @@@@@@@@@@@@@@@@@
'''# @''# +''@ @'''# ;''@ +''+ @''@ ,+'', '''@ #'''. @@@@ @@@@ '@@@# @@@@
;''' @@; '''# #'@'' @''@ @''+ +''# .@@ ''', '''. @@@@ @@@ @@@ .@@@
@''# #'' ''#''#@''. #''# '''. '''. +'', @@@@ @@@ @@@ @@@
@''# @''@'' #'@+'+ #''# '''. ''', +'', +@@@.@@@ @@@@ @@@, @@@ ,@@@
;''+ @, +''@'# @'+''@ @''# +''; '+ ''', +'', @@@@@@@@# @@@@ @@@. .@@@ .@@@
'''# ++'+ ''''@ ,''''# #''' @''@ '@''+ ''', ''', @@@@@@@@: @@@@ @@@; .@@@' ;@@@
@'''@@'''@ #'''. +'''' ;'''#@ :'''#@+''+ ''', ''', @@@@@@# @@@@ @@@+ ,@@@. @@@@
#''''''# @''+ @''+ +'''' @'''''''# ''', ''', #@@@. @@@@ @@@+ @@@ @@@@
@+''+@ '++@ ;++@ '#''@ ##'''@: +++, +++, :@ @@@@ @@@' @@@ '@@@
:' ' '`
fmt.Printf("%v\n\n", cwtch)
quit := false
torPath, err := exec.LookPath("tor")
if err != nil {
log.Fatal("tor could not be found on this system. Please install it in the system $PATH")
}
usr, err := user.Current()
if err != nil {
log.Fatalf("\nError: could not load current user: %v\n", err)
}
app, err = app2.NewApp(path.Join(usr.HomeDir, ".cwtch"), torPath)
if err != nil {
log.Fatalf("Error initializing application: %v", err)
}
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.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":
if peer != nil {
peer.Save()
}
quit = true
case "/new-profile":
if len(commands) == 2 {
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", commands[1])
} else {
p, err := app.CreatePeer(commands[1], password)
if err == nil {
stopGroupFollow()
fmt.Printf("\nNew profile created for %v\n", commands[1])
peer = p
suggestions = append(suggestionsBase, suggestionsSelectedProfile...)
} else {
fmt.Printf("\nError creating profile for %v: %v\n", commands[1], err)
}
}
} 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))
err = app.LoadProfiles(string(bytePassword))
if err == nil {
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")
} 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 Peer / 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 {
peer.Save()
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])
peer.Save()
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.Messages
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 "/save":
peer.Save()
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)
}
}
}
if peer != nil {
peer.Save()
}
}
app.Shutdown()
os.Exit(0)
}

View File

@ -1,159 +0,0 @@
package main
import (
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"os"
//"bufio"
//"cwtch.im/cwtch/storage"
)
func convertTorFile(filename string, password string) error {
return errors.New("this code doesn't work and can never work :( it's a math thing")
/*name, _ := diceware.Generate(2)
sk, err := ioutil.ReadFile("hs_ed25519_secret_key")
if err != nil {
return err
}
sk = sk[32:]
pk, err := ioutil.ReadFile("hs_ed25519_public_key")
if err != nil {
return err
}
pk = pk[32:]
onion, err := ioutil.ReadFile("hostname")
if err != nil {
return err
}
onion = onion[:56]
peer := libpeer.NewCwtchPeer(strings.Join(name, "-"))
fmt.Printf("%d %d %s\n", len(peer.GetProfile().Ed25519PublicKey), len(peer.GetProfile().Ed25519PrivateKey), peer.GetProfile().Onion)
peer.GetProfile().Ed25519PrivateKey = sk
peer.GetProfile().Ed25519PublicKey = pk
peer.GetProfile().Onion = string(onion)
fileStore := storage2.NewFileStore(filename, password)
err = fileStore.save(peer)
if err != nil {
return err
}
log.Infof("success! loaded %d byte pk and %d byte sk for %s.onion\n", len(pk), len(sk), onion)
return nil*/
}
/*
func vanity() error {
for {
pk, sk, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
continue
}
onion := utils.GetTorV3Hostname(pk)
for i := 4; i < len(os.Args); i++ {
if strings.HasPrefix(onion, os.Args[i]) {
peer := libpeer.NewCwtchPeer(os.Args[i])
peer.GetProfile().Ed25519PrivateKey = sk
peer.GetProfile().Ed25519PublicKey = pk
peer.GetProfile().Onion = onion
profileStore, _ := storage2.NewProfileStore(nil, os.Args[3], onion+".cwtch")
profileStore.Init("")
// need to signal new onion? impossible
log.Infof("found %s.onion\n", onion)
}
}
}
}*/
func printHelp() {
log.Infoln("usage: cwtchutil {help, convert-cwtch-file, convert-tor-file, changepw, vanity}")
}
func main() {
log.SetLevel(log.LevelInfo)
if len(os.Args) < 2 {
printHelp()
os.Exit(1)
}
switch os.Args[1] {
default:
printHelp()
case "help":
printHelp()
case "convert-tor-file":
if len(os.Args) != 4 {
fmt.Println("example: cwtchutil convert-tor-file /var/lib/tor/hs1 passw0rd")
os.Exit(1)
}
err := convertTorFile(os.Args[2], os.Args[3])
if err != nil {
log.Errorln(err)
}
/*case "vanity":
if len(os.Args) < 5 {
fmt.Println("example: cwtchutil vanity 4 passw0rd erinn openpriv")
os.Exit(1)
}
goroutines, err := strconv.Atoi(os.Args[2])
if err != nil {
log.Errorf("first parameter after vanity should be a number\n")
os.Exit(1)
}
log.Infoln("searching. press ctrl+c to stop")
for i := 0; i < goroutines; i++ {
go vanity()
}
for { // run until ctrl+c
time.Sleep(time.Hour * 24)
}*/
/*case "changepw":
if len(os.Args) != 3 {
fmt.Println("example: cwtch changepw ~/.cwtch/profiles/XXX")
os.Exit(1)
}
fmt.Printf("old password: ")
reader := bufio.NewReader(os.Stdin)
pw, err := reader.ReadString('\n')
if err != nil {
log.Errorln(err)
os.Exit(1)
}
pw = pw[:len(pw)-1]
profileStore, _ := storage.NewProfileStore(nil, os.Args[2], pw)
err = profileStore.Read()
if err != nil {
log.Errorln(err)
os.Exit(1)
}
fmt.Printf("new password: ")
newpw1, err := reader.ReadString('\n')
if err != nil {
log.Errorln(err)
os.Exit(1)
}
newpw1 = newpw1[:len(newpw1)-1] // fuck go with this linebreak shit ^ea
fileStore2, _ := storage.NewProfileStore(nil, os.Args[2], newpw1)
// No way to copy, populate this method
err = fileStore2.save(peer)
if err != nil {
log.Errorln(err)
os.Exit(1)
}
log.Infoln("success!")
*/
}
}

View File

@ -1,38 +1,18 @@
package main
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"
"os"
"path"
"cwtch.im/cwtch/peer"
"log"
)
func main() {
alice, _ := peer.NewCwtchPeer("alice", "password", ".")
// System Setup, We need Tor and Logging up and Running
log.AddEverythingFromPattern("peer/alice")
log.SetLevel(log.LevelDebug)
acn, err := tor.NewTorACN(path.Join(".", ".cwtch"), "")
if err != nil {
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)
processData := func(onion string, data []byte) []byte {
log.Printf("Recieved %s from %v", data, onion)
return data
}
app := app2.NewApp(acn, ".")
app.CreatePeer("alice", "be gay, do crimes")
alice := utils.WaitGetPeer(app, "alice")
app.LaunchPeers()
eventBus := app.GetEventBus(alice.GetOnion())
queue := event.NewQueue()
eventBus.Subscribe(event.NewMessageFromPeer, queue)
// For every new Data Packet Alice received she will Print it out.
for {
event := queue.Next()
log.Printf(log.LevelInfo, "Received %v from %v: %s", event.EventType, event.Data["Onion"], event.Data["Data"])
}
alice.SetPeerDataHandler(processData)
alice.Listen()
}

View File

@ -1,39 +1,27 @@
package main
import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/utils"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"os"
"path"
"cwtch.im/cwtch/peer"
"log"
"strconv"
"time"
)
func main() {
bob, _ := peer.NewCwtchPeer("bob", "password", ".")
counter := 1
// System Boilerplate, We need Tor Up and Running
log.AddEverythingFromPattern("peer/bob")
log.SetLevel(log.LevelDebug)
acn, err := tor.NewTorACN(path.Join(".", ".cwtch"), "")
if err != nil {
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)
}
bob.SetPeerDataHandler(func(onion string, data []byte) []byte {
log.Printf("Recieved %s from %v", data, onion)
counter++
return []byte(strconv.Itoa(counter))
})
connection := bob.PeerWithOnion("f4b6thuwmfszsqd3fzqpr45sdem4qoazdlzr2xmnc7fq22qe746hjqqd")
app := app2.NewApp(acn, ".")
app.CreatePeer("bob", "be gay, do crimes")
bob := utils.WaitGetPeer(app, "bob")
// Add Alice's Onion Here (It changes run to run)
bob.PeerWithOnion("upiztu7myymjf2dn4x4czhagp7axlnqjvf5zwfegbhtpkqb6v3vgu5yd")
// Send the Message...
log.Infof("Waiting for Bob to Connect to Alice...")
bob.SendMessageToPeer("upiztu7myymjf2dn4x4czhagp7axlnqjvf5zwfegbhtpkqb6v3vgu5yd", "Hello Alice!!!")
log.Printf("Waiting for Bob to Connect to Alice...")
connection.SendPacket([]byte("Hello Alice!!!"))
// Wait a while...
// Everything is run in a goroutine so the main thread has to stay active
time.Sleep(time.Second * 100)
}

View File

@ -1,161 +0,0 @@
package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/log"
"sync"
"time"
)
const tickTime = 10 * time.Second
const maxBakoff int = 32 // 320 seconds or ~5 min
type connectionType int
const (
peerConn connectionType = iota
serverConn
)
type contact struct {
id string
state connections.ConnectionState
ctype connectionType
ticks int
backoff int
}
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}
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)
}
}
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]]
peer := e.Data[event.RemotePeer]
cr.handleEvent(peer, state, peerConn)
case event.ServerStateChange:
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 {
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}))
}
return true
})
} else if prog != "100" {
cr.networkUp = false
}
}
case <-time.After(tickTime):
continue
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}
cr.connections.Store(id, p)
return
}
pinf, _ := cr.connections.Load(id)
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 {
p.backoff *= 2
}
p.ticks = 0
} else if state == connections.CONNECTING || state == connections.CONNECTED {
p.state = state
} else if state == connections.AUTHENTICATED {
p.state = state
p.backoff = 0
}
}
func (cr *contactRetry) Shutdown() {
cr.breakChan <- true
}

View File

@ -1,175 +0,0 @@
package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"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
breakChan chan bool
running bool
offline bool
offlineLock sync.Mutex
}
// NewNetworkCheck returns a Plugin that when started will attempt various network tests
func NewNetworkCheck(bus event.Manager, acn connectivity.ACN) Plugin {
nc := &networkCheck{bus: bus, acn: acn, queue: event.NewQueue(), breakChan: make(chan bool, 1)}
return nc
}
func (nc *networkCheck) Start() {
go nc.run()
}
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 {
case <-nc.breakChan:
nc.running = false
return
case e := <-nc.queue.OutChan():
switch e.EventType {
// On receipt of a Listen request for an onion service we will add the onion to our list
// 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()
}
}
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] {
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.offlineLock.Unlock()
default:
// if we receive either an encrypted group message or a peer acknowledgement we can assume the network
// is up and running (our onion service might still not be available, but we would aim to detect that
// through other actions
// we reset out timer
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.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()
}
}
}
}
func (nc *networkCheck) Shutdown() {
if nc.running {
nc.queue.Shutdown()
log.Debugf("shutting down network status plugin")
nc.breakChan <- true
}
}
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)
err := ClientTimeout.ExecuteAction(func() error {
conn, _, err := nc.acn.Open(onion)
if err == nil {
conn.Close()
}
return err
})
nc.offlineLock.Lock()
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
} 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
}
}

View File

@ -1,33 +0,0 @@
package plugins
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/connectivity"
)
// PluginID is used as an ID for signaling plugin activities
type PluginID int
// These are the plugin IDs for the supplied plugins
const (
CONNECTIONRETRY PluginID = iota
NETWORKCHECK
)
// Plugin is the interface for a plugin
type Plugin interface {
Start()
Shutdown()
}
// Get is a plugin factory for the requested plugin
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) Plugin {
switch id {
case CONNECTIONRETRY:
return NewConnectionRetry(bus, onion)
case NETWORKCHECK:
return NewNetworkCheck(bus, acn)
}
return nil
}

View File

@ -1,25 +0,0 @@
package utils
import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/peer"
"time"
)
// WaitGetPeer is a helper function for utility apps not written using the event bus
// Proper use of an App is to call CreatePeer and then process the NewPeer event
// 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
}
}
time.Sleep(100 * time.Millisecond)
}
}

View File

@ -0,0 +1,168 @@
package tor
import (
"errors"
"fmt"
"github.com/yawning/bulb"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"strings"
"time"
)
// Manager checks connectivity of the Tor process used to support Cwtch
type Manager struct {
socksPort int
controlPort int
process *exec.Cmd
}
// NewTorManager Instantiates a new connection manager, returns non-nil error if it fails to connect to a tor daemon on the given ports.
func NewTorManager(socksPort int, controlPort int, torPath string, torrc string) (*Manager, error) {
torManager := new(Manager)
torManager.socksPort = socksPort
torManager.controlPort = controlPort
err := torManager.TestConnection()
if err == nil {
log.Printf("using existing tor proxy")
return torManager, nil
}
// try to start tor
cmd := exec.Command(torPath, "-f", torrc)
// on Android, home can be set to '/' which is not writeable
if os.Getenv("HOME") == "" {
cmd.Env = append(os.Environ(), fmt.Sprintf("HOME=%s", path.Dir(torrc)))
}
log.Printf("starting local tor proxy")
err = cmd.Start()
if err != nil {
log.Printf("starting tor failed %v", err)
return nil, err
}
torManager.process = cmd
// for 30 seconds check every 5 if tor is up and working
for i := 0; i < 6; i++ {
time.Sleep(time.Second * 5)
err = torManager.TestConnection()
if err == nil {
break
}
}
return torManager, err
}
type proxyStatus int
const (
proxyStatusOK proxyStatus = iota
proxyStatusWrongType
proxyStatusCannotConnect
proxyStatusTimeout
)
// Shutdown kills the managed Tor Process
func (tm *Manager) Shutdown() {
if tm.process != nil {
if err := tm.process.Process.Kill(); err != nil {
log.Fatal("failed to kill process: ", err)
}
}
}
// Detect whether a proxy is connectable and is a Tor proxy
func checkTorProxy(proxyAddress string) proxyStatus {
// A trick to do this without making an outward connection is,
// paradoxically, to try to open it as http.
// This is documented in section 4 here: https://github.com/torproject/torspec/blob/master/socks-extensions.txt
client := &http.Client{Timeout: 2 * time.Second}
response, err := client.Get("http://" + proxyAddress + "/")
if err != nil {
switch t := err.(type) {
case *url.Error:
switch t.Err.(type) {
case *net.OpError: // Network-level error. Will in turn contain a os.SyscallError
return proxyStatusCannotConnect
default:
// http.error unfortunately not exported, need to match on string
// net/http: request canceled
if strings.Index(t.Err.Error(), "request canceled") != -1 {
return proxyStatusTimeout
}
}
}
// Protocol-level errors mean that http failed, so it's not Tor
return proxyStatusWrongType
}
defer response.Body.Close()
if response.Status != "501 Tor is not an HTTP Proxy" {
return proxyStatusWrongType
}
return proxyStatusOK
}
func proxyStatusMessage(status proxyStatus) string {
switch status {
case proxyStatusWrongType:
return "Proxy specified is not a Tor proxy"
case proxyStatusCannotConnect:
return "Cannot connect to Tor proxy"
case proxyStatusTimeout:
return "Proxy timeout"
default:
return "Unknown proxy error"
}
}
// TestConnection returns nil if both the socks and control ports of the Tor connection are active, otherwise it returns an error.
func (tm *Manager) TestConnection() error {
proxyStatus := checkTorProxy(fmt.Sprintf("127.0.0.1:%d", tm.socksPort))
controlAddress := fmt.Sprintf("127.0.0.1:%d", tm.controlPort)
if proxyStatus == proxyStatusOK {
c, err := bulb.Dial("tcp4", controlAddress)
if c != nil {
c.Close()
}
if err == nil {
return nil
}
return fmt.Errorf("could not connect to Tor Control Port %v %v", tm.controlPort, err)
}
return errors.New(proxyStatusMessage(proxyStatus))
}
// GetStatus returns tor control port bootstrap-phase status info in a map
func (tm *Manager) GetStatus() (map[string]string, error) {
controlAddress := fmt.Sprintf("127.0.0.1:%d", tm.controlPort)
c, err := bulb.Dial("tcp4", controlAddress)
if err != nil {
return nil, err
}
defer c.Close()
c.Request("AUTHENTICATE \"\"")
resp, err := c.Request("GETINFO status/bootstrap-phase")
if err != nil {
return nil, err
}
lines := strings.SplitN(resp.RawLines[0], " ", 5)
var resps = make(map[string]string)
for _, l := range lines {
kv := strings.Split(l, "=")
if len(kv) == 2 {
resps[kv[0]] = kv[1]
}
}
return resps, nil
}

View File

@ -0,0 +1,31 @@
package tor
import (
"fmt"
"os"
"os/exec"
"testing"
)
func TestTorManager(t *testing.T) {
tor, err := exec.LookPath("tor")
if err != nil {
t.Errorf("tor not found in PATH")
}
os.Remove("/tmp/torrc")
file, _ := os.Create("/tmp/torrc")
fmt.Fprintf(file, "SOCKSPort %d\nControlPort %d\nDataDirectory /tmp/tor\n", 10050, 10051)
file.Close()
tm, err := NewTorManager(10050, 10051, tor, "/tmp/torrc")
if err != nil {
t.Errorf("creating a new tor manager failed: %v", err)
} else {
tm2, err := NewTorManager(10050, 10051, tor, "/tmp/torrc")
if err != nil {
t.Errorf("creating a new tor manager failed: %v", err)
}
tm2.Shutdown() // should not noop
}
tm.Shutdown()
}

View File

@ -1,57 +0,0 @@
package bridge
import (
"cwtch.im/cwtch/event"
"sync"
)
type goChanBridge struct {
in chan event.IPCMessage
out chan event.IPCMessage
closedChan chan bool
closed bool
lock sync.Mutex
}
// MakeGoChanBridge returns a simple testing IPCBridge made from inprocess go channels
func MakeGoChanBridge() (b1, b2 event.IPCBridge) {
chan1 := make(chan event.IPCMessage)
chan2 := make(chan event.IPCMessage)
closed := make(chan bool)
a := &goChanBridge{in: chan1, out: chan2, closedChan: closed, closed: false}
b := &goChanBridge{in: chan2, out: chan1, closedChan: closed, closed: false}
go monitor(a, b)
return a, b
}
func monitor(a, b *goChanBridge) {
<-a.closedChan
a.closed = true
b.closed = true
a.closedChan <- true
}
func (pb *goChanBridge) Read() (*event.IPCMessage, bool) {
message, ok := <-pb.in
return &message, ok
}
func (pb *goChanBridge) Write(message *event.IPCMessage) {
pb.lock.Lock()
defer pb.lock.Unlock()
if !pb.closed {
pb.out <- *message
}
}
func (pb *goChanBridge) Shutdown() {
if !pb.closed {
close(pb.in)
close(pb.out)
pb.closedChan <- true
<-pb.closedChan
}
}

View File

@ -1,72 +0,0 @@
package bridge
/* Todo: When go generics ships, refactor this and event.infiniteChannel into one */
// InfiniteChannel implements the Channel interface with an infinite buffer between the input and the output.
type InfiniteChannel struct {
input, output chan interface{}
length chan int
buffer *InfiniteQueue
}
func newInfiniteChannel() *InfiniteChannel {
ch := &InfiniteChannel{
input: make(chan interface{}),
output: make(chan interface{}),
length: make(chan int),
buffer: newInfiniteQueue(),
}
go ch.infiniteBuffer()
return ch
}
// In returns the input channel
func (ch *InfiniteChannel) In() chan<- interface{} {
return ch.input
}
// Out returns the output channel
func (ch *InfiniteChannel) Out() <-chan interface{} {
return ch.output
}
// Len returns the length of items in queue
func (ch *InfiniteChannel) Len() int {
return <-ch.length
}
// Close closes the InfiniteChanel
func (ch *InfiniteChannel) Close() {
close(ch.input)
}
func (ch *InfiniteChannel) infiniteBuffer() {
var input, output chan interface{}
var next interface{}
input = ch.input
for input != nil || output != nil {
select {
case elem, open := <-input:
if open {
ch.buffer.Add(elem)
} else {
input = nil
}
case output <- next:
ch.buffer.Remove()
case ch.length <- ch.buffer.Length():
}
if ch.buffer.Length() > 0 {
output = ch.output
next = ch.buffer.Peek()
} else {
output = nil
next = nil
}
}
close(ch.output)
close(ch.length)
}

View File

@ -1,105 +0,0 @@
package bridge
/* Todo: When go generics ships, refactor this and event.infinitQueue channel into one */
/*
Package queue provides a fast, ring-buffer queue based on the version suggested by Dariusz Górecki.
Using this instead of other, simpler, queue implementations (slice+append or linked list) provides
substantial memory and time benefits, and fewer GC pauses.
The queue implemented here is as fast as it is for an additional reason: it is *not* thread-safe.
*/
// minQueueLen is smallest capacity that queue may have.
// Must be power of 2 for bitwise modulus: x % n == x & (n - 1).
const minQueueLen = 16
// InfiniteQueue represents a single instance of the queue data structure.
type InfiniteQueue struct {
buf []interface{}
head, tail, count int
}
// New constructs and returns a new Queue.
func newInfiniteQueue() *InfiniteQueue {
return &InfiniteQueue{
buf: make([]interface{}, minQueueLen),
}
}
// Length returns the number of elements currently stored in the queue.
func (q *InfiniteQueue) Length() int {
return q.count
}
// resizes the queue to fit exactly twice its current contents
// this can result in shrinking if the queue is less than half-full
func (q *InfiniteQueue) resize() {
newBuf := make([]interface{}, q.count<<1)
if q.tail > q.head {
copy(newBuf, q.buf[q.head:q.tail])
} else {
n := copy(newBuf, q.buf[q.head:])
copy(newBuf[n:], q.buf[:q.tail])
}
q.head = 0
q.tail = q.count
q.buf = newBuf
}
// Add puts an element on the end of the queue.
func (q *InfiniteQueue) Add(elem interface{}) {
if q.count == len(q.buf) {
q.resize()
}
q.buf[q.tail] = elem
// bitwise modulus
q.tail = (q.tail + 1) & (len(q.buf) - 1)
q.count++
}
// Peek returns the element at the head of the queue. This call panics
// if the queue is empty.
func (q *InfiniteQueue) Peek() interface{} {
if q.count <= 0 {
panic("queue: Peek() called on empty queue")
}
return q.buf[q.head]
}
// Get returns the element at index i in the queue. If the index is
// invalid, the call will panic. This method accepts both positive and
// negative index values. Index 0 refers to the first element, and
// index -1 refers to the last.
func (q *InfiniteQueue) Get(i int) interface{} {
// If indexing backwards, convert to positive index.
if i < 0 {
i += q.count
}
if i < 0 || i >= q.count {
panic("queue: Get() called with index out of range")
}
// bitwise modulus
return q.buf[(q.head+i)&(len(q.buf)-1)]
}
// Remove removes and returns the element from the front of the queue. If the
// queue is empty, the call will panic.
func (q *InfiniteQueue) Remove() interface{} {
if q.count <= 0 {
panic("queue: Remove() called on empty queue")
}
ret := q.buf[q.head]
q.buf[q.head] = nil
// bitwise modulus
q.head = (q.head + 1) & (len(q.buf) - 1)
q.count--
// Resize down if buffer 1/4 full.
if len(q.buf) > minQueueLen && (q.count<<2) == len(q.buf) {
q.resize()
}
return ret
}

View File

@ -1,19 +0,0 @@
// +build windows
package bridge
import (
"cwtch.im/cwtch/event"
"log"
)
func NewPipeBridgeClient(inFilename, outFilename string) event.IPCBridge {
log.Fatal("Not supported on windows")
return nil
}
// NewPipeBridgeService returns a pipe backed IPCBridge for a service
func NewPipeBridgeService(inFilename, outFilename string) event.IPCBridge {
log.Fatal("Not supported on windows")
return nil
}

View File

@ -1,357 +0,0 @@
// +build !windows
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"
"syscall"
"time"
)
/* pipeBridge creates a pair of named pipes
Needs a call to new client and service to fully successfully open
*/
const maxBufferSize = 1000
const serviceName = "service"
const clientName = "client"
const syn = "SYN"
const synack = "SYNACK"
const ack = "ACK"
type pipeBridge struct {
infile, outfile string
in, out *os.File
read chan event.IPCMessage
write *InfiniteChannel
closedChan chan bool
state connections.ConnectionState
lock sync.Mutex
threeShake func() bool
// For logging / debugging purposes
name string
}
func newPipeBridge(inFilename, outFilename string) *pipeBridge {
syscall.Mkfifo(inFilename, 0600)
syscall.Mkfifo(outFilename, 0600)
pb := &pipeBridge{infile: inFilename, outfile: outFilename, state: connections.DISCONNECTED}
pb.read = make(chan event.IPCMessage, maxBufferSize)
pb.write = newInfiniteChannel() //make(chan event.IPCMessage, maxBufferSize)
return pb
}
// NewPipeBridgeClient returns a pipe backed IPCBridge for a client
func NewPipeBridgeClient(inFilename, outFilename string) event.IPCBridge {
log.Debugf("Making new PipeBridge Client...\n")
pb := newPipeBridge(inFilename, outFilename)
pb.name = clientName
pb.threeShake = pb.threeShakeClient
go pb.connectionManager()
return pb
}
// NewPipeBridgeService returns a pipe backed IPCBridge for a service
func NewPipeBridgeService(inFilename, outFilename string) event.IPCBridge {
log.Debugf("Making new PipeBridge Service...\n")
pb := newPipeBridge(inFilename, outFilename)
pb.name = serviceName
pb.threeShake = pb.threeShakeService
go pb.connectionManager()
log.Debugf("Successfully created new PipeBridge Service!\n")
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 {
log.Debugf("clientConnManager loop start init\n")
pb.setState(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)
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)
continue
}
log.Debugf("Successfully connected PipeBridge %v!\n", pb.name)
pb.handleConns()
}
log.Debugf("exiting %v ConnectionManager\n", pb.name)
}
// threeShake performs a 3way handshake sync up
func (pb *pipeBridge) threeShakeService() bool {
synacked := false
for {
resp, err := pb.readString()
if err != nil {
return false
}
if string(resp) == syn {
if !synacked {
err = pb.writeString([]byte(synack))
if err != nil {
return false
}
synacked = true
}
} else if string(resp) == ack {
return true
}
}
}
func (pb *pipeBridge) synLoop(stop chan bool) {
delay := time.Duration(0)
for {
select {
case <-time.After(delay):
err := pb.writeString([]byte(syn))
if err != nil {
return
}
delay = time.Second
case <-stop:
return
}
}
}
func (pb *pipeBridge) threeShakeClient() bool {
stop := make(chan bool)
go pb.synLoop(stop)
for {
resp, err := pb.readString()
if err != nil {
return false
}
if string(resp) == synack {
stop <- true
err := pb.writeString([]byte(ack))
return err == nil
}
}
}
func (pb *pipeBridge) handleConns() {
if !pb.threeShake() {
pb.setState(connections.FAILED)
pb.closeReset()
return
}
pb.setState(connections.AUTHENTICATED)
pb.closedChan = make(chan bool, 5)
log.Debugf("handleConns authed, %v 2xgo\n", pb.name)
go pb.handleRead()
go pb.handleWrite()
<-pb.closedChan
log.Debugf("handleConns <-closedChan (%v)\n", pb.name)
if pb.getState() != connections.KILLED {
pb.setState(connections.FAILED)
}
pb.closeReset()
log.Debugf("handleConns done for %v, exit\n", pb.name)
}
func (pb *pipeBridge) closeReset() {
pb.in.Close()
pb.out.Close()
close(pb.read)
pb.write.Close()
if pb.getState() != connections.KILLED {
pb.read = make(chan event.IPCMessage, maxBufferSize)
pb.write = newInfiniteChannel()
}
}
func (pb *pipeBridge) handleWrite() {
log.Debugf("handleWrite() %v\n", pb.name)
defer log.Debugf("exiting handleWrite() %v\n", pb.name)
for {
select {
case messageInf := <-pb.write.output:
if messageInf == nil {
pb.closedChan <- true
return
}
message := messageInf.(event.IPCMessage)
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("handleWrite <- message: %v %v ...\n", message.Dest, message.Message.EventType)
} else {
log.Debugf("handleWrite <- message: %v\n", message)
}
if pb.getState() == 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))
}
messageJSON, _ := json.Marshal(encMessage)
err := pb.writeString(messageJSON)
if err != nil {
pb.closedChan <- true
return
}
} else {
return
}
}
}
}
func (pb *pipeBridge) handleRead() {
log.Debugf("handleRead() %v\n", pb.name)
defer log.Debugf("exiting handleRead() %v", pb.name)
for {
log.Debugf("Waiting to handleRead()...\n")
buffer, err := pb.readString()
if err != nil {
pb.closedChan <- true
return
}
var message event.IPCMessage
err = json.Unmarshal(buffer, &message)
if err != nil {
log.Errorf("Read error: '%v', value: '%v'", err, buffer)
pb.closedChan <- true
return // probably new connection trying to initialize
}
for k, v := range message.Message.Data {
val, _ := base64.StdEncoding.DecodeString(v)
message.Message.Data[k] = string(val)
}
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("handleRead read<-: %v %v ...\n", message.Dest, message.Message.EventType)
} else {
log.Debugf("handleRead read<-: %v\n", message)
}
pb.read <- message
log.Debugf("handleRead wrote\n")
}
}
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 {
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)
} else {
log.Debugf("Read %v: %v\n", pb.name, message)
}
}
return &message, pb.getState() != connections.KILLED
}
func (pb *pipeBridge) Write(message *event.IPCMessage) {
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("Write %v: %v %v ...\n", pb.name, message.Dest, message.Message.EventType)
} else {
log.Debugf("Write %v: %v\n", pb.name, message)
}
pb.write.input <- *message
log.Debugf("Wrote\n")
}
func (pb *pipeBridge) Shutdown() {
log.Debugf("pb.Shutdown() for %v currently in state: %v\n", pb.name, connections.ConnectionStateName[pb.getState()])
pb.state = connections.KILLED
pb.closedChan <- true
log.Debugf("Done Shutdown for %v\n", pb.name)
}
func (pb *pipeBridge) writeString(message []byte) error {
size := make([]byte, 2)
binary.LittleEndian.PutUint16(size, uint16(len(message)))
pb.out.Write(size)
for pos := 0; pos < len(message); {
n, err := pb.out.Write(message[pos:])
if err != nil {
log.Errorf("Writing out on pipeBridge: %v\n", err)
return err
}
pos += n
}
return nil
}
func (pb *pipeBridge) readString() ([]byte, error) {
var n int
size := make([]byte, 2)
var err error
n, err = pb.in.Read(size)
if err != nil || n != 2 {
log.Errorf("Could not read len int from stream: %v\n", err)
return nil, err
}
n = int(binary.LittleEndian.Uint16(size))
pos := 0
buffer := make([]byte, n)
for n > 0 {
m, err := pb.in.Read(buffer[pos:])
if err != nil {
log.Errorf("Reading into buffer from pipe: %v\n", err)
return nil, err
}
n -= m
pos += m
}
return buffer, nil
}

View File

@ -1,131 +0,0 @@
package bridge
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/log"
"os"
"testing"
"time"
)
var (
clientPipe = "./client"
servicePipe = "./service"
)
func clientHelper(t *testing.T, in, out string, messageOrig *event.IPCMessage, done chan bool) {
client := NewPipeBridgeClient(in, out)
messageAfter, ok := client.Read()
if !ok {
t.Errorf("Reading from client IPCBridge failed")
done <- true
return
}
if messageOrig.Dest != messageAfter.Dest {
t.Errorf("Dest's value differs expected: %v actaul: %v", messageOrig.Dest, messageAfter.Dest)
}
if messageOrig.Message.EventType != messageAfter.Message.EventType {
t.Errorf("EventTypes's value differs expected: %v actaul: %v", messageOrig.Message.EventType, messageAfter.Message.EventType)
}
if messageOrig.Message.Data[event.Identity] != messageAfter.Message.Data[event.Identity] {
t.Errorf("Data[Identity]'s value differs expected: %v actaul: %v", messageOrig.Message.Data[event.Identity], messageAfter.Message.Data[event.Identity])
}
done <- true
}
func serviceHelper(t *testing.T, in, out string, messageOrig *event.IPCMessage, done chan bool) {
service := NewPipeBridgeService(in, out)
service.Write(messageOrig)
done <- true
}
func TestPipeBridge(t *testing.T) {
os.Remove(servicePipe)
os.Remove(clientPipe)
messageOrig := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.NewPeer, event.Identity, "It is I")}
serviceDone := make(chan bool)
clientDone := make(chan bool)
go clientHelper(t, clientPipe, servicePipe, messageOrig, clientDone)
go serviceHelper(t, servicePipe, clientPipe, messageOrig, serviceDone)
<-serviceDone
<-clientDone
}
func restartingClient(t *testing.T, in, out string, done chan bool) {
client := NewPipeBridgeClient(in, out)
message1 := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.NewPeer)}
log.Infoln("client writing message 1")
client.Write(message1)
time.Sleep(100 * time.Millisecond)
log.Infoln("client shutdown")
client.Shutdown()
log.Infoln("client new client")
client = NewPipeBridgeClient(in, out)
message2 := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.DeleteContact)}
log.Infoln("client2 write message2")
client.Write(message2)
done <- true
}
func stableService(t *testing.T, in, out string, done chan bool) {
service := NewPipeBridgeService(in, out)
log.Infoln("service wait read 1")
message1, ok := service.Read()
log.Infof("service read 1 %v ok:%v\n", message1, ok)
if !ok {
t.Errorf("Reading from client IPCBridge 1st time failed")
done <- true
return
}
if message1.Message.EventType != event.NewPeer {
t.Errorf("Wrong message received, expected NewPeer\n")
done <- true
return
}
log.Infoln("service wait read 2")
message2, ok := service.Read()
log.Infof("service read 2 got %v ok:%v\n", message2, ok)
if !ok {
t.Errorf("Reading from client IPCBridge 2nd time failed")
done <- true
return
}
if message2.Message.EventType != event.DeleteContact {
t.Errorf("Wrong message received, expected DeleteContact, got %v\n", message2)
done <- true
return
}
done <- true
}
func TestReconnect(t *testing.T) {
log.Infoln("TestReconnect")
os.Remove(servicePipe)
os.Remove(clientPipe)
serviceDone := make(chan bool)
clientDone := make(chan bool)
go restartingClient(t, clientPipe, servicePipe, clientDone)
go stableService(t, servicePipe, clientPipe, serviceDone)
<-serviceDone
<-clientDone
}

View File

@ -1,351 +0,0 @@
package event
// Type captures the definition of many common Cwtch application events
type Type string
// Defining Common Event Types
const (
StatusRequest = Type("StatusRequest")
ProtocolEngineStatus = Type("ProtocolEngineStatus")
// Attempt to outbound peer with a given remote peer
// attributes:
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
PeerRequest = Type("PeerRequest")
// RetryPeerRequest
// Identical to PeerRequest, but allows Engine to make decisions regarding blocked peers
// attributes:
// 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")
// Turn on/off blocking of unknown peers (if peers aren't in the contact list then they will be autoblocked
BlockUnknownPeers = Type("BlockUnknownPeers")
AllowUnknownPeers = Type("AllowUnknownPeers")
// GroupServer
JoinServer = Type("JoinServer")
// attributes GroupServer - the onion of the server to leave
LeaveServer = Type("LeaveServer")
ProtocolEngineStartListen = Type("ProtocolEngineStartListen")
ProtocolEngineStopped = Type("ProtocolEngineStopped")
InvitePeerToGroup = Type("InvitePeerToGroup")
// a group invite has been received from a remote peer
// attributes:
// 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")
//Ciphertext, Signature:
EncryptedGroupMessage = Type("EncryptedGroupMessage")
//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
// Signature: The signature of the Message that failed to send
// Error: string describing the error
SendMessageToGroupError = Type("SendMessageToGroupError")
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
SendMessageToPeerError = Type("SendMessageToPeerError")
// REQUESTS TO STORAGE ENGINE
// a peer contact has been added
// attributes:
// RemotePeer [eg ""]
// Authorization
PeerCreated = Type("PeerCreated")
// Password, NewPassword
ChangePassword = Type("ChangePassword")
// Error(err), EventID
ChangePasswordError = Type("ChangePasswordError")
// EventID
ChangePasswordSuccess = Type("ChangePasswordSuccess")
// a group has been successfully added or newly created
// attributes:
// Data [serialized *model.Group]
GroupCreated = Type("GroupCreated")
// RemotePeer
DeleteContact = Type("DeleteContact")
// GroupID
DeleteGroup = Type("DeleteGroup")
// change the .Name attribute of a profile (careful - this is not a custom attribute. it is used in the underlying protocol during handshakes!)
// attributes:
// ProfileName [eg "erinn"]
SetProfileName = Type("SetProfileName")
// request to store a profile-wide attribute (good for e.g. per-profile settings like theme prefs)
// attributes:
// Key [eg "fontcolor"]
// Data [eg "red"]
SetAttribute = Type("SetAttribute")
// request to store a per-contact attribute (e.g. display names for a peer)
// attributes:
// RemotePeer [eg ""]
// Key [eg "nick"]
// Data [eg "erinn"]
SetPeerAttribute = Type("SetPeerAttribute")
// request to store a per-cwtch-group attribute (e.g. display name for a group)
// attributes:
// GroupID [eg ""]
// Key [eg "nick"]
// Data [eg "open privacy board"]
SetGroupAttribute = Type("SetGroupAttribute")
// PeerStateChange servers as a new incoming connection message as well, and can/is consumed by frontends to alert of new p2p connections
// RemotePeer
// ConnectionState
PeerStateChange = Type("PeerStateChange")
// GroupServer
// ConnectionState
ServerStateChange = Type("ServerStateChange")
/***** Application client / service messages *****/
// 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)
NewPeer = Type("NewPeer")
// Identity(onion)
DeletePeer = Type("DeletePeer")
// Identity(onion), Data(pluginID)
AddPeerPlugin = Type("AddPeerPlugin")
// Password
LoadProfiles = Type("LoadProfiles")
// Client has reloaded, triggers NewPeer s then ReloadDone
ReloadClient = Type("ReloadClient")
ReloadDone = Type("ReloadDone")
// Identity - Ask service to resend all connection states
ReloadPeer = Type("ReloadPeer")
// Identity(onion)
ShutdownPeer = Type("ShutdownPeer")
Shutdown = Type("Shutdown")
// Error(err)
// Error creating peer
PeerError = Type("PeerError")
// Error(err)
AppError = Type("AppError")
GetACNStatus = Type("GetACNStatus")
GetACNVersion = Type("GetACNVersion")
// 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")
)
// Field defines common event attributes
type Field string
// Defining Common Field Types
const (
// A peers local onion address
Onion = Field("Onion")
RemotePeer = Field("RemotePeer")
Ciphertext = Field("Ciphertext")
Signature = Field("Signature")
PreviousSignature = Field("PreviousSignature")
TimestampSent = Field("TimestampSent")
TimestampReceived = Field("TimestampReceived")
Identity = Field("Identity")
GroupID = Field("GroupID")
GroupServer = Field("GroupServer")
ServerTokenY = Field("ServerTokenY")
ServerTokenOnion = Field("ServerTokenOnion")
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")
Salt = Field("Salt")
Error = Field("Error")
Progress = 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
const (
AppErrLoaded0 = "Loaded 0 profiles"
)
// Values to be suplied in event.NewPeer for Status
const (
StorageRunning = "running"
StorageNew = "new"
)
// Defining Protocol Contexts
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,112 +0,0 @@
package event
import (
"sync"
)
type queue struct {
infChan infiniteChannel
lock sync.Mutex
closed bool
}
type simpleQueue struct {
eventChannel chan Event
lock sync.Mutex
closed bool
}
// Queue is a wrapper around a channel for handling Events in a consistent way across subsystems.
// The expectation is that each subsystem in Cwtch will manage a given an event.Queue fed from
// the event.Manager.
type Queue interface {
Publish(event Event)
Next() Event
Shutdown()
OutChan() <-chan Event
Len() int
}
// NewQueue initializes an event.Queue
func NewQueue() Queue {
queue := &queue{infChan: *newInfiniteChannel()}
return queue
}
// NewSimpleQueue initializes an event.Queue of the given buffer size.
func NewSimpleQueue(buffer int) Queue {
queue := new(simpleQueue)
queue.eventChannel = make(chan Event, buffer)
return queue
}
func (sq *simpleQueue) inChan() chan<- Event {
return sq.eventChannel
}
func (sq *simpleQueue) OutChan() <-chan Event {
return sq.eventChannel
}
// Backlog returns the length of the queue backlog
func (sq *simpleQueue) Len() int {
return len(sq.eventChannel)
}
// Next returns the next available event from the front of the queue
func (sq *simpleQueue) Next() Event {
event := <-sq.eventChannel
return event
}
// Shutdown closes our eventChannel
func (sq *simpleQueue) Shutdown() {
sq.lock.Lock()
sq.closed = true
close(sq.eventChannel)
sq.lock.Unlock()
}
// Shutdown closes our eventChannel
func (sq *simpleQueue) Publish(event Event) {
sq.lock.Lock()
if !sq.closed {
sq.inChan() <- event
}
sq.lock.Unlock()
}
func (iq *queue) inChan() chan<- Event {
return iq.infChan.In()
}
func (iq *queue) OutChan() <-chan Event {
return iq.infChan.Out()
}
// Out returns the next available event from the front of the queue
func (iq *queue) Next() Event {
event := <-iq.infChan.Out()
return event
}
func (iq *queue) Len() int {
return iq.infChan.Len()
}
// Shutdown closes our eventChannel
func (iq *queue) Shutdown() {
iq.lock.Lock()
iq.closed = true
iq.infChan.Close()
iq.lock.Unlock()
}
func (iq *queue) Publish(event Event) {
iq.lock.Lock()
if !iq.closed {
iq.inChan() <- event
}
iq.lock.Unlock()
}

View File

@ -1,175 +0,0 @@
package event
import (
"crypto/rand"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"math"
"math/big"
"os"
"runtime"
"strings"
"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.
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}
}
// 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
func NewEventList(eventType Type, args ...interface{}) Event {
data := map[Field]string{}
for i := 0; i < len(args); i += 2 {
key, kok := args[i].(Field)
val, vok := args[i+1].(string)
if kok && vok {
data[key] = val
}
}
return Event{EventType: eventType, EventID: 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
mapMutex sync.Mutex
internal chan bool
closed bool
trace bool
}
// Manager is an interface for an event bus
// FIXME this interface lends itself to race conditions around channels
type Manager interface {
Subscribe(Type, Queue)
Publish(Event)
PublishLocal(Event)
Shutdown()
}
// NewEventManager returns an initialized EventManager
func NewEventManager() Manager {
em := &manager{}
em.initialize()
return em
}
// Initialize sets up the Manager.
func (em *manager) initialize() {
em.subscribers = make(map[Type][]Queue)
em.events = make(chan []byte)
em.internal = make(chan bool)
em.closed = false
_, em.trace = os.LookupEnv("CWTCH_EVENT_SOURCE")
go em.eventBus()
}
// Subscribe takes an eventType and an Channel and associates them in the eventBus. All future events of that type
// will be sent to the eventChannel.
func (em *manager) Subscribe(eventType Type, queue Queue) {
em.mapMutex.Lock()
defer em.mapMutex.Unlock()
em.subscribers[eventType] = append(em.subscribers[eventType], 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
}
}
// Publish an event only locally, not going over an IPC bridge if there is one
func (em *manager) PublishLocal(event Event) {
em.Publish(event)
}
// eventBus is an internal function that is used to distribute events to all subscribers
func (em *manager) eventBus() {
for {
eventJSON := <-em.events
// In the case on an empty event. Teardown the Queue
if len(eventJSON) == 0 {
log.Errorf("Received zero length event")
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]
em.mapMutex.Unlock()
// 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)
}
}
// We are about to exit the eventbus thread, fire off an event internally
em.internal <- true
}
// Shutdown triggers, and waits for, the internal eventBus goroutine to finish
func (em *manager) Shutdown() {
em.events <- []byte{}
em.closed = true
// wait for eventBus to finish
<-em.internal
close(em.events)
close(em.internal)
}

View File

@ -1,104 +0,0 @@
package event
import (
"git.openprivacy.ca/openprivacy/log"
"sync"
"testing"
"time"
)
// Most basic Manager Test, Initialize, Subscribe, Publish, Receive
func TestEventManager(t *testing.T) {
eventManager := NewEventManager()
// We need to make this buffer at least 1, otherwise we will log an error!
testChan := make(chan Event, 1)
simpleQueue := &simpleQueue{testChan, sync.Mutex{}, false}
eventManager.Subscribe("TEST", simpleQueue)
eventManager.Publish(Event{EventType: "TEST", Data: map[Field]string{"Value": "Hello World"}})
event := <-testChan
if event.EventType == "TEST" && event.Data["Value"] == "Hello World" {
} else {
t.Errorf("Received Invalid Event")
}
eventManager.Shutdown()
}
// Most basic Manager Test, Initialize, Subscribe, Publish, Receive
func TestEventManagerOverflow(t *testing.T) {
eventManager := NewEventManager()
// Explicitly setting this to 0 log an error!
testChan := make(chan Event)
simpleQueue := &simpleQueue{testChan, sync.Mutex{}, false}
eventManager.Subscribe("TEST", simpleQueue)
eventManager.Publish(Event{EventType: "TEST"})
}
func TestEventManagerMultiple(t *testing.T) {
log.SetLevel(log.LevelDebug)
eventManager := NewEventManager()
groupEventQueue := NewQueue()
peerEventQueue := NewQueue()
allEventQueue := NewQueue()
eventManager.Subscribe("PeerEvent", peerEventQueue)
eventManager.Subscribe("GroupEvent", groupEventQueue)
eventManager.Subscribe("PeerEvent", allEventQueue)
eventManager.Subscribe("GroupEvent", allEventQueue)
eventManager.Subscribe("ErrorEvent", allEventQueue)
eventManager.Publish(Event{EventType: "PeerEvent", Data: map[Field]string{"Value": "Hello World Peer"}})
eventManager.Publish(Event{EventType: "GroupEvent", Data: map[Field]string{"Value": "Hello World Group"}})
eventManager.Publish(Event{EventType: "PeerEvent", Data: map[Field]string{"Value": "Hello World Peer"}})
eventManager.Publish(Event{EventType: "ErrorEvent", Data: map[Field]string{"Value": "Hello World Error"}})
eventManager.Publish(Event{EventType: "NobodyIsSubscribedToThisEvent", Data: map[Field]string{"Value": "Noone should see this!"}})
assertLength := func(len int, expected int, label string) {
if len != expected {
t.Errorf("Expected %s to be %v was %v", label, expected, len)
}
}
time.Sleep(time.Second)
assertLength(groupEventQueue.Len(), 1, "Group Event Queue Length")
assertLength(peerEventQueue.Len(), 2, "Peer Event Queue Length")
assertLength(allEventQueue.Len(), 4, "All Event Queue Length")
checkEvent := func(eventType Type, expected Type, label string) {
if eventType != expected {
t.Errorf("Expected %s to be %v was %v", label, expected, eventType)
}
}
event := groupEventQueue.Next()
checkEvent(event.EventType, "GroupEvent", "First Group Event")
event = peerEventQueue.Next()
checkEvent(event.EventType, "PeerEvent", "First Peer Event")
event = peerEventQueue.Next()
checkEvent(event.EventType, "PeerEvent", "Second Peer Event")
event = allEventQueue.Next()
checkEvent(event.EventType, "PeerEvent", "ALL: First Peer Event")
event = allEventQueue.Next()
checkEvent(event.EventType, "GroupEvent", "ALL: First Group Event")
event = allEventQueue.Next()
checkEvent(event.EventType, "PeerEvent", "ALL: Second Peer Event")
event = allEventQueue.Next()
checkEvent(event.EventType, "ErrorEvent", "ALL: First Error Event")
eventManager.Shutdown()
groupEventQueue.Shutdown()
peerEventQueue.Shutdown()
allEventQueue.Shutdown()
// Reading from a closed queue should result in an instant return and an empty event
event = groupEventQueue.Next()
checkEvent(event.EventType, "", "Test Next() on Empty Queue")
}

View File

@ -1,38 +0,0 @@
package event
type ipcManager struct {
manager Manager
onion string
ipcBridge IPCBridge
}
// NewIPCEventManager returns an EvenetManager that also pipes events over and supplied IPCBridge
func NewIPCEventManager(bridge IPCBridge, onion string) Manager {
em := &ipcManager{onion: onion, ipcBridge: bridge, manager: NewEventManager()}
return em
}
// IPCEventManagerFrom returns an IPCEventManger from the supplied manager and IPCBridge
func IPCEventManagerFrom(bridge IPCBridge, onion string, manager Manager) Manager {
em := &ipcManager{onion: onion, ipcBridge: bridge, manager: manager}
return em
}
func (ipcm *ipcManager) Publish(ev Event) {
ipcm.manager.Publish(ev)
message := &IPCMessage{Dest: ipcm.onion, Message: ev}
ipcm.ipcBridge.Write(message)
}
func (ipcm *ipcManager) PublishLocal(ev Event) {
ipcm.manager.Publish(ev)
}
func (ipcm *ipcManager) Subscribe(eventType Type, queue Queue) {
ipcm.manager.Subscribe(eventType, queue)
}
func (ipcm *ipcManager) Shutdown() {
ipcm.manager.Shutdown()
}

View File

@ -1,72 +0,0 @@
package event
/*
This package is taken from https://github.com/eapache/channels
as per their suggestion we are not importing the entire package and instead cherry picking and adapting what is needed
It is covered by the MIT License https://github.com/eapache/channels/blob/master/LICENSE
*/
// infiniteChannel implements the Channel interface with an infinite buffer between the input and the output.
type infiniteChannel struct {
input, output chan Event
length chan int
buffer *infiniteQueue
}
func newInfiniteChannel() *infiniteChannel {
ch := &infiniteChannel{
input: make(chan Event),
output: make(chan Event),
length: make(chan int),
buffer: newInfinitQueue(),
}
go ch.infiniteBuffer()
return ch
}
func (ch *infiniteChannel) In() chan<- Event {
return ch.input
}
func (ch *infiniteChannel) Out() <-chan Event {
return ch.output
}
func (ch *infiniteChannel) Len() int {
return <-ch.length
}
func (ch *infiniteChannel) Close() {
close(ch.input)
}
func (ch *infiniteChannel) infiniteBuffer() {
var input, output chan Event
var next Event
input = ch.input
for input != nil || output != nil {
select {
case elem, open := <-input:
if open {
ch.buffer.Add(elem)
} else {
input = nil
}
case output <- next:
ch.buffer.Remove()
case ch.length <- ch.buffer.Length():
}
if ch.buffer.Length() > 0 {
output = ch.output
next = ch.buffer.Peek()
} else {
output = nil
//next = nil
}
}
close(ch.output)
close(ch.length)
}

View File

@ -1,108 +0,0 @@
package event
/*
This package is taken from https://github.com/eapache/channels
as per their suggestion we are not importing the entire package and instead cherry picking and adapting what is needed
It is covered by the MIT License https://github.com/eapache/channels/blob/master/LICENSE
*/
/*
Package queue provides a fast, ring-buffer queue based on the version suggested by Dariusz Górecki.
Using this instead of other, simpler, queue implementations (slice+append or linked list) provides
substantial memory and time benefits, and fewer GC pauses.
The queue implemented here is as fast as it is for an additional reason: it is *not* thread-safe.
*/
// minQueueLen is smallest capacity that queue may have.
// Must be power of 2 for bitwise modulus: x % n == x & (n - 1).
const minQueueLen = 16
// Queue represents a single instance of the queue data structure.
type infiniteQueue struct {
buf []Event
head, tail, count int
}
// New constructs and returns a new Queue.
func newInfinitQueue() *infiniteQueue {
return &infiniteQueue{
buf: make([]Event, minQueueLen),
}
}
// Length returns the number of elements currently stored in the queue.
func (q *infiniteQueue) Length() int {
return q.count
}
// resizes the queue to fit exactly twice its current contents
// this can result in shrinking if the queue is less than half-full
func (q *infiniteQueue) resize() {
newBuf := make([]Event, q.count<<1)
if q.tail > q.head {
copy(newBuf, q.buf[q.head:q.tail])
} else {
n := copy(newBuf, q.buf[q.head:])
copy(newBuf[n:], q.buf[:q.tail])
}
q.head = 0
q.tail = q.count
q.buf = newBuf
}
// Add puts an element on the end of the queue.
func (q *infiniteQueue) Add(elem Event) {
if q.count == len(q.buf) {
q.resize()
}
q.buf[q.tail] = elem
// bitwise modulus
q.tail = (q.tail + 1) & (len(q.buf) - 1)
q.count++
}
// Peek returns the element at the head of the queue. This call panics
// if the queue is empty.
func (q *infiniteQueue) Peek() Event {
if q.count <= 0 {
panic("queue: Peek() called on empty queue")
}
return q.buf[q.head]
}
// Get returns the element at index i in the queue. If the index is
// invalid, the call will panic. This method accepts both positive and
// negative index values. Index 0 refers to the first element, and
// index -1 refers to the last.
func (q *infiniteQueue) Get(i int) Event {
// If indexing backwards, convert to positive index.
if i < 0 {
i += q.count
}
if i < 0 || i >= q.count {
panic("queue: Get() called with index out of range")
}
// bitwise modulus
return q.buf[(q.head+i)&(len(q.buf)-1)]
}
// Remove removes and returns the element from the front of the queue. If the
// queue is empty, the call will panic.
func (q *infiniteQueue) Remove() Event {
if q.count <= 0 {
panic("queue: Remove() called on empty queue")
}
ret := q.buf[q.head]
//q.buf[q.head] = nil
// bitwise modulus
q.head = (q.head + 1) & (len(q.buf) - 1)
q.count--
// Resize down if buffer 1/4 full.
if len(q.buf) > minQueueLen && (q.count<<2) == len(q.buf) {
q.resize()
}
return ret
}

View File

@ -1,14 +0,0 @@
package event
// IPCMessage is a wrapper for a regular eventMessage with a destination (onion|AppDest) so the other side of the bridge can route appropriately
type IPCMessage struct {
Dest string
Message Event
}
// IPCBridge is an interface to a IPC construct used to communicate IPCMessages
type IPCBridge interface {
Read() (*IPCMessage, bool)
Write(message *IPCMessage)
Shutdown()
}

17
go.mod
View File

@ -1,17 +0,0 @@
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
)

110
go.sum
View File

@ -1,110 +0,0 @@
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=
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/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/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/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/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=
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/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/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-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/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/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,277 +1,154 @@
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/utils"
"github.com/golang/protobuf/proto"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"io"
"strings"
"log"
"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.
// 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
GroupKey [32]byte
GroupServer string
Timeline Timeline `json:"-"`
Accepted bool
IsCompromised bool
Attributes map[string]string
lock sync.Mutex
LocalID string
State string `json:"-"`
UnacknowledgedMessages []Message
Version int
GroupID string
SignedGroupID []byte
GroupKey [32]byte
GroupServer string
Timeline Timeline
Accepted bool
Owner string
IsCompromised bool
InitialMessage []byte
lock sync.Mutex
NewMessage chan Message `json:"-"`
}
// 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")
}
group.GroupServer = server
var groupID [16]byte
if _, err := io.ReadFull(rand.Reader, groupID[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
log.Printf("Error: Cannot read from random: %v\n", err)
return nil, err
}
group.GroupID = fmt.Sprintf("%x", groupID)
var groupKey [32]byte
if _, err := io.ReadFull(rand.Reader, groupKey[:]); err != nil {
log.Errorf("Error: Cannot read from random: %v\n", err)
log.Printf("Error: Cannot read from random: %v\n", err)
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.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
group.Owner = "self"
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
}
// 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
}
// Invite generates a invitation that can be sent to a cwtch peer
func (g *Group) Invite() (string, error) {
func (g *Group) Invite(initialMessage []byte) ([]byte, error) {
gci := &groups.GroupInvite{
GroupID: g.GroupID,
GroupName: g.Attributes[attr.GetLocalScope("name")],
SharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
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[:]
// AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte) Message {
g.lock.Lock()
defer g.lock.Unlock()
timelineMessage := Message{
Message: message.Text,
Timestamp: time.Unix(int64(message.Timestamp), 0),
Received: time.Unix(0, 0),
Signature: sig,
PeerID: message.Onion,
PreviousMessageSig: message.PreviousMessageSig,
ReceivedByServer: false,
gci := &protocol.GroupChatInvite{
GroupName: g.GroupID,
GroupSharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
SignedGroupId: g.SignedGroupID[:],
InitialMessage: initialMessage[:],
}
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages, timelineMessage)
return timelineMessage
}
// ErrorSentMessage removes a sent message from the unacknowledged list and sets its error flag if found, otherwise returns false
func (g *Group) ErrorSentMessage(sig []byte, error string) bool {
g.lock.Lock()
defer g.lock.Unlock()
var message *Message
// Delete the message from the unack'd buffer if it exists
for i, unAckedMessage := range g.UnacknowledgedMessages {
if compareSignatures(unAckedMessage.Signature, sig) {
message = &unAckedMessage
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
message.Error = error
g.Timeline.Insert(message)
return true
}
cp := &protocol.CwtchPeerPacket{
GroupChatInvite: gci,
}
return false
invite, err := proto.Marshal(cp)
return invite, err
}
// 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 {
g.lock.Lock()
defer g.lock.Unlock()
// Delete the message from the unack'd buffer if it exists
for i, unAckedMessage := range g.UnacknowledgedMessages {
if compareSignatures(unAckedMessage.Signature, sig) {
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,
ReceivedByServer: true,
Error: "",
Acknowledged: true,
PeerID: message.GetOnion(),
PreviousMessageSig: message.GetPreviousMessageSig(),
}
seen := g.Timeline.Insert(timelineMessage)
g.lock.Unlock()
return timelineMessage, seen
// Send a new Message notification if we have an app that is listening.
if g.NewMessage != nil && !seen {
g.NewMessage <- *timelineMessage
}
return timelineMessage
}
// GetTimeline provides a safe copy of the timeline
func (g *Group) GetTimeline() (timeline []Message) {
// GetTimeline provides a safe copy of the timeline-=
func (g *Group) GetTimeline() (t []Message) {
g.lock.Lock()
defer g.lock.Unlock()
return append(g.Timeline.GetMessages(), g.UnacknowledgedMessages...)
t = g.Timeline.GetMessages()
g.lock.Unlock()
return
}
//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)
if err != nil {
log.Printf("Error: Cannot read from random: %v\n", err)
return nil, err
}
wire, err := proto.Marshal(message)
utils.CheckError(err)
encrypted := secretbox.Seal(nonce[:], []byte(wire), &nonce, &g.GroupKey)
return encrypted, nil
}
// 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) {
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)
if err == nil {
return true, dm
}
func (g *Group) DecryptMessage(ciphertext []byte) (bool, *protocol.DecryptedGroupMessage) {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &g.GroupKey)
if ok {
dm := &protocol.DecryptedGroupMessage{}
err := proto.Unmarshal(decrypted, dm)
if err == nil {
return true, dm
}
}
return false, nil
}
// SetAttribute allows applications to store arbitrary configuration info at the group level.
func (g *Group) SetAttribute(name string, value string) {
g.lock.Lock()
defer g.lock.Unlock()
g.Attributes[name] = value
}
// GetAttribute returns the value of a value set with SetAttribute. If no such value has been set exists is set to false.
func (g *Group) GetAttribute(name string) (value string, exists bool) {
g.lock.Lock()
defer g.lock.Unlock()
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,115 +1,27 @@
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{},
g, _ := NewGroup("server.onion")
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
}
g.SetAttribute("test", "test_value")
value, exists := g.GetAttribute("test")
if !exists || value != "test_value" {
t.Errorf("Custom Attribute Should have been set, instead %v %v", exists, value)
}
t.Logf("Got message %v", message)
}
func TestGroupErr(t *testing.T) {
_, err := NewGroup("not a real group name")
if err == nil {
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

@ -1,13 +1,12 @@
package model
import (
"encoding/json"
"sort"
"sync"
"time"
)
// Timeline encapsulates a collection of ordered Messages, and a mechanism to access them
// Timeline encapsulates a collection of ordered messages, and a mechanism to access them
// in a threadsafe manner.
type Timeline struct {
Messages []Message
@ -23,25 +22,18 @@ type Message struct {
Message string
Signature []byte
PreviousMessageSig []byte
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
const MessageBaseSize = 104
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
@ -53,45 +45,23 @@ func (t *Timeline) GetMessages() []Message {
return messages
}
// GetCopy returns a duplicate of the Timeline
func (t *Timeline) GetCopy() *Timeline {
t.lock.Lock()
defer t.lock.Unlock()
bytes, _ := json.Marshal(t)
newt := &Timeline{}
json.Unmarshal(bytes, newt)
return newt
}
// SetMessages sets the Messages of this timeline. Only to be used in loading/initialization
func (t *Timeline) SetMessages(messages []Message) {
t.lock.Lock()
defer t.lock.Unlock()
t.Messages = messages
}
// Len gets the length of the timeline
func (t *Timeline) Len() int {
return len(t.Messages)
}
// Swap swaps 2 Messages on the timeline.
// Swap swaps 2 messages on the timeline.
func (t *Timeline) Swap(i, j int) {
t.Messages[i], t.Messages[j] = t.Messages[j], t.Messages[i]
}
// Less checks 2 Messages (i and j) in the timeline and returns true if i occurred before j, else false
// Less checks 2 messages (i and j) in the timeline and returns true if i occcured before j, else false
func (t *Timeline) Less(i, j int) bool {
if t.Messages[i].Timestamp.Before(t.Messages[j].Timestamp) {
return true
}
// Short circuit false if j is before i, signature checks will give a wrong order in this case.
if t.Messages[j].Timestamp.Before(t.Messages[i].Timestamp) {
return false
}
if compareSignatures(t.Messages[i].PreviousMessageSig, t.SignedGroupID) {
return true
}
@ -103,13 +73,6 @@ func (t *Timeline) Less(i, j int) bool {
return false
}
// Sort sorts the timeline in a canonical order.
func (t *Timeline) Sort() {
t.lock.Lock()
defer t.lock.Unlock()
sort.Sort(t)
}
// Insert inserts a message into the timeline in a thread safe way.
func (t *Timeline) Insert(mi *Message) bool {
t.lock.Lock()

View File

@ -1,6 +1,8 @@
package model
import (
"cwtch.im/cwtch/protocol"
"github.com/golang/protobuf/proto"
"strconv"
"testing"
"time"
@ -14,14 +16,15 @@ func TestMessagePadding(t *testing.T) {
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
gid, invite, _ := alice.StartGroup("aaa.onion")
gci := &protocol.CwtchPeerPacket{}
proto.Unmarshal(invite, gci)
sarah.ProcessInvite(gci.GetGroupChatInvite(), alice.Onion)
sarah.ProcessInvite(invite)
group := alice.GetGroupByGroupID(gid)
group := alice.GetGroup(gid)
c1, s1, err := sarah.EncryptMessageToGroup("Hello World 1", group.GroupID)
t.Logf("Length of Encrypted Message: %v %v", len(c1), err)
c1, s1, _ := sarah.EncryptMessageToGroup("Hello World 1", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c1))
alice.AttemptDecryption(c1, s1)
c2, s2, _ := alice.EncryptMessageToGroup("Hello World 2", group.GroupID)
@ -47,14 +50,12 @@ 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("aaa.onion")
gci := &protocol.CwtchPeerPacket{}
proto.Unmarshal(invite, gci)
sarah.ProcessInvite(gci.GetGroupChatInvite(), alice.Onion)
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite)
group := alice.GetGroup(gid)
group := alice.GetGroupByGroupID(gid)
t.Logf("group: %v, sarah %v", group, sarah)
@ -79,14 +80,14 @@ func TestTranscriptConsistency(t *testing.T) {
c5, s5, _ := alice.EncryptMessageToGroup("Hello World 5", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c5))
_, _, m1, _ := sarah.AttemptDecryption(c1, s1)
_, m1 := sarah.AttemptDecryption(c1, s1)
sarah.AttemptDecryption(c1, s1) // Try a duplicate
_, _, m2, _ := sarah.AttemptDecryption(c2, s2)
_, _, m3, _ := sarah.AttemptDecryption(c3, s3)
_, _, m4, _ := sarah.AttemptDecryption(c4, s4)
_, _, m5, _ := sarah.AttemptDecryption(c5, s5)
_, m2 := sarah.AttemptDecryption(c2, s2)
_, m3 := sarah.AttemptDecryption(c3, s3)
_, m4 := sarah.AttemptDecryption(c4, s4)
_, m5 := sarah.AttemptDecryption(c5, s5)
// Now we simulate a client receiving these Messages completely out of order
// Now we simulate a client receiving these messages completely out of order
timeline.Insert(m1)
timeline.Insert(m5)
timeline.Insert(m4)

View File

@ -2,47 +2,25 @@ 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"
"strings"
"sync"
"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"`
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
Name string
Ed25519PublicKey ed25519.PublicKey
Trusted bool
Blocked bool
Onion string
}
// Profile encapsulates all the attributes necessary to be a Cwtch Peer.
@ -51,109 +29,53 @@ type Profile struct {
Contacts map[string]*PublicProfile
Ed25519PrivateKey ed25519.PrivateKey
Groups map[string]*Group
}
// MaxGroupMessageLength is the maximum length of a message posted to a server group.
// TODO: Should this be per server?
const MaxGroupMessageLength = 1800
// GenerateRandomID generates a random 16 byte hex id code
func GenerateRandomID() string {
randBytes := make([]byte, 16)
rand.Read(randBytes)
return filepath.Join(hex.EncodeToString(randBytes))
}
func (p *PublicProfile) init() {
if p.Attributes == nil {
p.Attributes = make(map[string]string)
}
p.UnacknowledgedMessages = make(map[string]int)
p.LocalID = GenerateRandomID()
}
// SetAttribute allows applications to store arbitrary configuration info at the profile level.
func (p *PublicProfile) SetAttribute(name string, value string) {
p.lock.Lock()
defer p.lock.Unlock()
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()
defer p.lock.Unlock()
value, exists = p.Attributes[name]
return
Custom map[string]string
lock sync.Mutex
}
// GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name.
func GenerateNewProfile(name string) *Profile {
p := new(Profile)
p.init()
p.Name = name
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
p.Groups = make(map[string]*Group)
p.Custom = make(map[string]string)
return p
}
// GetCwtchIdentityPacket returns the wire message for conveying this profiles identity.
func (p *Profile) GetCwtchIdentityPacket() (message []byte) {
ci := &protocol.CwtchIdentity{
Name: p.Name,
Ed25519PublicKey: p.Ed25519PublicKey,
}
cpp := &protocol.CwtchPeerPacket{
CwtchIdentify: ci,
}
message, err := proto.Marshal(cpp)
utils.CheckError(err)
return
}
// AddCwtchIdentity takes a wire message and if it is a CwtchIdentity message adds the identity as a contact
// otherwise returns an error
func (p *Profile) AddCwtchIdentity(onion string, ci *protocol.CwtchIdentity) {
p.AddContact(onion, &PublicProfile{Name: ci.GetName(), Ed25519PublicKey: ci.GetEd25519PublicKey(), Onion: onion})
}
// AddContact allows direct manipulation of cwtch contacts
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
}
}
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()
defer p.lock.Unlock()
delete(p.Contacts, onion)
}
// DeleteGroup deletes a group
func (p *Profile) DeleteGroup(groupID string) {
p.lock.Lock()
defer p.lock.Unlock()
delete(p.Groups, groupID)
}
// RejectInvite rejects and removes a group invite
func (p *Profile) RejectInvite(groupID string) {
p.lock.Lock()
@ -161,88 +83,6 @@ func (p *Profile) RejectInvite(groupID string) {
p.lock.Unlock()
}
// AddSentMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt string, sent time.Time, eventID string) *Message {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
now := time.Now()
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)
}
contact.Timeline.Insert(message)
contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1
return message
}
return nil
}
// AddMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, sent time.Time) (message *Message) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
// We don't really need a Signature here, but we use it to maintain order
now := time.Now()
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
if ok {
message = &Message{PeerID: onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: true}
contact.Timeline.Insert(message)
}
return
}
// 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 {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
contact.Timeline.Messages[mIdx].Error = error
delete(contact.UnacknowledgedMessages, eventID)
return mIdx
}
}
return -1
}
// AckSentMessageToPeer sets mesage to a peer as acknowledged
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) int {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
contact.Timeline.Messages[mIdx].Acknowledged = true
delete(contact.UnacknowledgedMessages, eventID)
return mIdx
}
}
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)
}
}
// AcceptInvite accepts a group invite
func (p *Profile) AcceptInvite(groupID string) (err error) {
p.lock.Lock()
@ -267,6 +107,21 @@ func (p *Profile) GetGroups() []string {
return keys
}
// SetCustomAttribute allows applications to store arbitrary configuration info at the profile level.
func (p *Profile) SetCustomAttribute(name string, value string) {
p.lock.Lock()
defer p.lock.Unlock()
p.Custom[name] = value
}
// GetCustomAttribute returns the value of a value set with SetCustomAttribute. If no such value has been set exists is set to false.
func (p *Profile) GetCustomAttribute(name string) (value string, exists bool) {
p.lock.Lock()
defer p.lock.Unlock()
value, exists = p.Custom[name]
return
}
// GetContacts returns an unordered list of contact onions associated with this profile.
func (p *Profile) GetContacts() []string {
p.lock.Lock()
@ -280,38 +135,39 @@ 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 {
// 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 {
return contact.Authorization
contact.Trusted = true
} 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{}
for _, contact := range p.GetContacts() {
c, _ := p.GetContact(contact)
authorizations[c.Onion] = c.Authorization
// 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 authorizations
return false
}
// GetContact returns a contact if the profile has it
@ -322,36 +178,22 @@ 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)
group := p.GetGroupByGroupID(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 {
if err == nil {
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
}
return false
@ -365,160 +207,145 @@ 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()
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
return
}
// GetGroup a pointer to a Group by the group Id, returns nil if no group found.
func (p *Profile) GetGroup(groupID string) (g *Group) {
// GetGroupByGroupID a pointer to a Group by the group Id, returns nil if no group found.
func (p *Profile) GetGroupByGroupID(groupID string) (g *Group) {
p.lock.Lock()
defer p.lock.Unlock()
g = p.Groups[groupID]
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)
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.LocalID = GenerateRandomID()
copy(group.GroupKey[:], gci.SharedKey[:])
group.GroupServer = gci.ServerHost
group.Accepted = false
group.Attributes = make(map[string]string)
group.Attributes[attr.GetLocalScope("name")] = gci.GroupName
p.AddGroup(group)
return gci.GroupID, nil
}
return "", err
// ProcessInvite adds a new group invite to the profile.
func (p *Profile) ProcessInvite(gci *protocol.GroupChatInvite, peerHostname string) {
group := new(Group)
group.GroupID = gci.GetGroupName()
group.SignedGroupID = gci.GetSignedGroupId()
copy(group.GroupKey[:], gci.GetGroupSharedKey()[:])
group.GroupServer = gci.GetServerHost()
group.InitialMessage = gci.GetInitialMessage()[:]
group.Accepted = false
group.Owner = peerHostname
p.AddGroup(group)
}
// 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]
existingGroup, exists := p.Groups[group.GroupID]
if !exists {
owner, ok := p.GetContact(group.Owner)
if ok {
valid := ed25519.Verify(owner.Ed25519PublicKey, []byte(group.GroupID+group.GroupServer), group.SignedGroupID)
if valid {
p.lock.Lock()
defer p.lock.Unlock()
p.Groups[group.GroupID] = group
}
}
} else if exists && existingGroup.Owner == group.Owner {
p.lock.Lock()
defer p.lock.Unlock()
p.Groups[group.GroupID] = group
}
// If we are sent an invite or group update by someone who is not an owner
// then we reject the group.
}
// AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
// If successful, adds the message to the group's timeline
func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool, string, *Message, bool) {
func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *Message) {
for _, group := range p.Groups {
success, dgm := group.DecryptMessage(ciphertext)
if success {
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, ciphertext, signature)
// Assert that we know the owner of the group
owner, ok := p.Contacts[group.Owner]
if ok {
valid := ed25519.Verify(owner.Ed25519PublicKey, []byte(group.GroupID+group.GroupServer), dgm.SignedGroupId)
// If we can decrypt the message, but the group id is wrong that means that
// this message is from someone who was not invited to the group.
// As such this group has been compromised, probably by one of the other members.
// We set the flag to be handled by the UX and reject the message.
if !valid {
group.Compromised()
return false, nil
}
}
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)
// Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised.
if !verified {
group.Compromised()
return false, group.GroupID, nil, false
return false, nil
}
message, seen := group.AddMessage(dgm, signature)
return true, group.GroupID, message, seen
return true, group.AddMessage(dgm, signature)
}
}
// If we couldn't find a group to decrypt the message with we just return false. This is an expected case
return false, "", nil, false
return false, nil
}
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)
}
}
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
// profile
func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte, []byte, error) {
if len(message) > MaxGroupMessageLength {
return nil, nil, errors.New("group message is too long")
}
group := p.GetGroup(groupID)
group := p.GetGroupByGroupID(groupID)
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)
lenPadding := 1024 - 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[:],
}
ciphertext, err := group.EncryptMessage(dm)
if err != nil {
return nil, nil, err
}
signature := p.SignMessage(groupID + group.GroupServer + string(ciphertext))
group.AddSentMessage(dm, signature)
return ciphertext, signature, nil
}
return nil, nil, errors.New("group does not exist")
}
// GetCopy returns a full deep copy of the Profile struct and its members (timeline inclusion control by arg)
func (p *Profile) GetCopy(timeline bool) *Profile {
p.lock.Lock()
defer p.lock.Unlock()
newp := new(Profile)
bytes, _ := json.Marshal(p)
json.Unmarshal(bytes, &newp)
if timeline {
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

@ -1,6 +1,8 @@
package model
import (
"cwtch.im/cwtch/protocol"
"github.com/golang/protobuf/proto"
"testing"
)
@ -8,8 +10,15 @@ func TestProfileIdentity(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
if alice.Contacts[sarah.Onion].Name != "Sarah" {
message := sarah.GetCwtchIdentityPacket()
ci := &protocol.CwtchPeerPacket{}
err := proto.Unmarshal(message, ci)
if err != nil {
t.Errorf("alice should have added sarah as a contact %v", err)
}
alice.AddCwtchIdentity("sarah.onion", ci.GetCwtchIdentify())
if alice.Contacts["sarah.onion"].Name != "Sarah" {
t.Errorf("alice should have added sarah as a contact %v", alice.Contacts)
}
@ -17,8 +26,8 @@ func TestProfileIdentity(t *testing.T) {
t.Errorf("alice should be only contact: %v", alice.GetContacts())
}
alice.SetAttribute("test", "hello world")
value, _ := alice.GetAttribute("test")
alice.SetCustomAttribute("test", "hello world")
value, _ := alice.GetCustomAttribute("test")
if value != "hello world" {
t.Errorf("value from custom attribute should have been 'hello world', instead was: %v", value)
}
@ -31,9 +40,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 +55,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,14 +75,14 @@ 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)
group := alice.GetGroup(gid)
gid, invite, _ := alice.StartGroup("aaa.onion")
gci := &protocol.CwtchPeerPacket{}
proto.Unmarshal(invite, gci)
sarah.ProcessInvite(gci.GetGroupChatInvite(), alice.Onion)
group := alice.GetGroupByGroupID(gid)
if len(sarah.Groups) == 1 {
if sarah.GetGroup(group.GroupID).Accepted {
if sarah.GetGroupByGroupID(group.GroupID).Accepted {
t.Errorf("Group should not be accepted")
}
sarah.RejectInvite(group.GroupID)
@ -87,48 +100,47 @@ 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("aaa.onion", []byte("Hello World"))
gci := &protocol.CwtchPeerPacket{}
proto.Unmarshal(invite, gci)
sarah.ProcessInvite(gci.GetGroupChatInvite(), alice.Onion)
if len(sarah.GetGroups()) != 1 {
t.Errorf("sarah should only be in 1 group instead: %v", sarah.GetGroups())
}
group := alice.GetGroup(gid)
group := alice.GetGroupByGroupID(gid)
sarah.AcceptInvite(group.GroupID)
c, s1, _ := sarah.EncryptMessageToGroup("Hello World", group.GroupID)
alice.AttemptDecryption(c, s1)
gid2, invite2, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite2)
group2 := alice.GetGroup(gid2)
gid2, invite2, _ := alice.StartGroup("bbb.onion")
gci2 := &protocol.CwtchPeerPacket{}
proto.Unmarshal(invite2, gci2)
sarah.ProcessInvite(gci2.GetGroupChatInvite(), alice.Onion)
group2 := alice.GetGroupByGroupID(gid2)
c2, s2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID)
alice.AttemptDecryption(c2, s2)
_, _, err := sarah.EncryptMessageToGroup(string(make([]byte, MaxGroupMessageLength*2)), group2.GroupID)
if err == nil {
t.Errorf("Overly long message should have returned an error")
sarahGroup := sarah.GetGroupByGroupID(group.GroupID)
im := sarahGroup.GetInitialMessage()
if string(im) != "Hello World" {
t.Errorf("Initial Message was not stored properly: %v", im)
}
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(gci2.GetGroupChatInvite(), alice.Onion)
c3, s3, err := bob.EncryptMessageToGroup("Bobs Message", group2.GroupID)
if err == nil {
ok, _, message, _ := alice.AttemptDecryption(c3, s3)
ok, message := alice.AttemptDecryption(c3, s3)
if !ok {
t.Errorf("Bobs message to the group should be decrypted %v %v", message, ok)
}
eve := GenerateNewProfile("eve")
ok, _, _, _ = eve.AttemptDecryption(c3, s3)
ok, _ = eve.AttemptDecryption(c3, s3)
if ok {
t.Errorf("Eves hould not be able to decrypt Messages!")
t.Errorf("Eves hould not be able to decrypt messages!")
}
} else {
t.Errorf("Bob failed to encrypt a message to the group")

View File

@ -0,0 +1,149 @@
package connections
import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol"
"sync"
"time"
)
// Manager encapsulates all the logic necessary to manage outgoing peer and server connections.
type Manager struct {
peerConnections map[string]*PeerPeerConnection
serverConnections map[string]*PeerServerConnection
lock sync.Mutex
breakChannel chan bool
}
// NewConnectionsManager creates a new instance of Manager.
func NewConnectionsManager() *Manager {
m := new(Manager)
m.peerConnections = make(map[string]*PeerPeerConnection)
m.serverConnections = make(map[string]*PeerServerConnection)
m.breakChannel = make(chan bool)
return m
}
// ManagePeerConnection creates a new PeerConnection for the given Host and Profile.
func (m *Manager) ManagePeerConnection(host string, profile *model.Profile, dataHandler func(string, []byte) []byte) *PeerPeerConnection {
m.lock.Lock()
defer m.lock.Unlock()
_, exists := m.peerConnections[host]
if !exists {
ppc := NewPeerPeerConnection(host, profile, dataHandler)
go ppc.Run()
m.peerConnections[host] = ppc
return ppc
}
return m.peerConnections[host]
}
// ManageServerConnection creates a new ServerConnection for Host with the given callback handler.
func (m *Manager) ManageServerConnection(host string, handler func(string, *protocol.GroupMessage)) {
m.lock.Lock()
_, exists := m.serverConnections[host]
if !exists {
psc := NewPeerServerConnection(host)
go psc.Run()
psc.GroupMessageHandler = handler
m.serverConnections[host] = psc
}
m.lock.Unlock()
}
// GetPeers returns a map of all peer connections with their state
func (m *Manager) GetPeers() map[string]ConnectionState {
rm := make(map[string]ConnectionState)
m.lock.Lock()
for onion, ppc := range m.peerConnections {
rm[onion] = ppc.GetState()
}
m.lock.Unlock()
return rm
}
// GetServers returns a map of all server connections with their state.
func (m *Manager) GetServers() map[string]ConnectionState {
rm := make(map[string]ConnectionState)
m.lock.Lock()
for onion, psc := range m.serverConnections {
rm[onion] = psc.GetState()
}
m.lock.Unlock()
return rm
}
// GetPeerPeerConnectionForOnion safely returns a given peer connection
func (m *Manager) GetPeerPeerConnectionForOnion(host string) (ppc *PeerPeerConnection) {
m.lock.Lock()
ppc = m.peerConnections[host]
m.lock.Unlock()
return
}
// 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
}
// AttemptReconnections repeatedly attempts to reconnect with failed peers and servers.
func (m *Manager) AttemptReconnections() {
timeout := time.Duration(0) // first pass right away
for {
select {
case <-time.After(timeout):
m.lock.Lock()
for _, ppc := range m.peerConnections {
if ppc.GetState() == FAILED {
go ppc.Run()
}
}
m.lock.Unlock()
m.lock.Lock()
for _, psc := range m.serverConnections {
if psc.GetState() == FAILED {
go psc.Run()
}
}
m.lock.Unlock()
// Launch Another Run In 30 Seconds
timeout = time.Duration(30 * time.Second)
case <-m.breakChannel:
return
}
}
}
// ClosePeerConnection closes an existing peer connection
func (m *Manager) ClosePeerConnection(onion string) {
m.lock.Lock()
pc, ok := m.peerConnections[onion]
if ok {
pc.Close()
delete(m.peerConnections, onion)
}
m.lock.Unlock()
}
// Shutdown closes all connections under managment (freeing their goroutines)
func (m *Manager) Shutdown() {
m.breakChannel <- true
m.lock.Lock()
for onion, ppc := range m.peerConnections {
ppc.Close()
delete(m.peerConnections, onion)
}
for onion, psc := range m.serverConnections {
psc.Close()
delete(m.serverConnections, onion)
}
m.lock.Unlock()
}

View File

@ -0,0 +1,10 @@
package connections
import (
"testing"
)
func TestConnectionsManager(t *testing.T) {
// TODO We need to encapsulate connections behind a well defined interface for tesintg
NewConnectionsManager()
}

View File

@ -0,0 +1,151 @@
package connections
import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/peer/peer"
"cwtch.im/cwtch/protocol"
"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"
"log"
"time"
)
// PeerPeerConnection encapsulates a single outgoing Peer->Peer connection
type PeerPeerConnection struct {
connection.AutoConnectionHandler
PeerHostname string
state ConnectionState
connection *connection.Connection
profile *model.Profile
dataHandler func(string, []byte) []byte
}
// NewPeerPeerConnection creates a new peer connection for the given hostname and profile.
func NewPeerPeerConnection(peerhostname string, profile *model.Profile, dataHandler func(string, []byte) []byte) *PeerPeerConnection {
ppc := new(PeerPeerConnection)
ppc.PeerHostname = peerhostname
ppc.profile = profile
ppc.dataHandler = dataHandler
ppc.Init()
return ppc
}
// GetState returns the current connection state
func (ppc *PeerPeerConnection) GetState() ConnectionState {
return ppc.state
}
// ClientIdentity passes the given CwtchIdentity packet to the profile.
func (ppc *PeerPeerConnection) ClientIdentity(ci *protocol.CwtchIdentity) {
ppc.profile.AddCwtchIdentity(ppc.PeerHostname, ci)
}
// HandleGroupInvite passes the given group invite tothe profile
func (ppc *PeerPeerConnection) HandleGroupInvite(gci *protocol.GroupChatInvite) {
ppc.profile.ProcessInvite(gci, ppc.PeerHostname)
}
// GetClientIdentityPacket returns nil to avoid peers constantly sending identity packets to eachother.
func (ppc *PeerPeerConnection) GetClientIdentityPacket() []byte {
return nil
}
// HandlePacket handles data packets on the optional data channel
func (ppc *PeerPeerConnection) HandlePacket(data []byte) []byte {
return ppc.dataHandler(ppc.PeerHostname, data)
}
// SendPacket sends data packets on the optional data channel
func (ppc *PeerPeerConnection) SendPacket(data []byte) {
ppc.WaitTilAuthenticated()
ppc.connection.Do(func() error {
channel := ppc.connection.Channel("im.cwtch.peer.data", channels.Outbound)
if channel != nil {
peerchannel, ok := channel.Handler.(*peer.CwtchPeerDataChannel)
if ok {
log.Printf("Sending packet\n")
peerchannel.SendMessage(data)
}
}
return nil
})
}
// SendGroupInvite sends the given serialized invite packet to the Peer
func (ppc *PeerPeerConnection) SendGroupInvite(invite []byte) {
ppc.WaitTilAuthenticated()
ppc.connection.Do(func() error {
channel := ppc.connection.Channel("im.cwtch.peer", channels.Outbound)
if channel != nil {
peerchannel, ok := channel.Handler.(*peer.CwtchPeerChannel)
if ok {
log.Printf("Sending group invite packet\n")
peerchannel.SendMessage(invite)
}
}
return nil
})
}
// WaitTilAuthenticated waits until the underlying connection is authenticated
func (ppc *PeerPeerConnection) WaitTilAuthenticated() {
for {
if ppc.GetState() == AUTHENTICATED {
break
}
time.Sleep(time.Second * 5)
}
}
// Run manages the setup and teardown of a peer->peer connection
func (ppc *PeerPeerConnection) Run() error {
ppc.state = CONNECTING
rc, err := goricochet.Open(ppc.PeerHostname)
if err == nil {
rc.TraceLog(false)
ppc.connection = rc
ppc.state = CONNECTED
_, err := connection.HandleOutboundConnection(ppc.connection).ProcessAuthAsV3Client(identity.InitializeV3(ppc.profile.Name, &ppc.profile.Ed25519PrivateKey, &ppc.profile.Ed25519PublicKey))
if err == nil {
ppc.state = AUTHENTICATED
go func() {
ppc.connection.Do(func() error {
ppc.connection.RequestOpenChannel("im.cwtch.peer", &peer.CwtchPeerChannel{Handler: ppc})
return nil
})
if ppc.dataHandler != nil {
ppc.connection.Do(func() error {
ppc.connection.RequestOpenChannel("im.cwtch.peer.data", &peer.CwtchPeerDataChannel{Handler: ppc})
return nil
})
}
time.Sleep(time.Second * 1)
ppc.connection.Do(func() error {
channel := ppc.connection.Channel("im.cwtch.peer", channels.Outbound)
if channel != nil {
peerchannel, ok := channel.Handler.(*peer.CwtchPeerChannel)
if ok {
peerchannel.SendMessage(ppc.profile.GetCwtchIdentityPacket())
}
}
return nil
})
}()
ppc.connection.Process(ppc)
}
}
ppc.state = FAILED
return err
}
// Close closes the connection
func (ppc *PeerPeerConnection) Close() {
ppc.state = KILLED
ppc.connection.Conn.Close()
}

View File

@ -0,0 +1,111 @@
package connections
import (
"crypto/rand"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/peer/peer"
"cwtch.im/cwtch/protocol"
"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"
"golang.org/x/crypto/ed25519"
"net"
"testing"
"time"
)
func PeerAuthValid(hostname string, key ed25519.PublicKey) (allowed, known bool) {
return true, true
}
func runtestpeer(t *testing.T, tp *TestPeer, identity identity.Identity) {
ln, _ := net.Listen("tcp", "127.0.0.1:5452")
conn, _ := ln.Accept()
defer conn.Close()
rc, err := goricochet.NegotiateVersionInbound(conn)
if err != nil {
t.Errorf("Negotiate Version Error: %v", err)
}
rc.TraceLog(true)
err = connection.HandleInboundConnection(rc).ProcessAuthAsV3Server(identity, PeerAuthValid)
if err != nil {
t.Errorf("ServerAuth Error: %v", err)
}
tp.RegisterChannelHandler("im.cwtch.peer", func() channels.Handler {
cpc := new(peer.CwtchPeerChannel)
cpc.Handler = tp
return cpc
})
go func() {
alice := model.GenerateNewProfile("alice")
time.Sleep(time.Second * 1)
rc.Do(func() error {
channel := rc.Channel("im.cwtch.peer", channels.Inbound)
if channel != nil {
peerchannel, ok := channel.Handler.(*peer.CwtchPeerChannel)
if ok {
peerchannel.SendMessage(alice.GetCwtchIdentityPacket())
}
}
return nil
})
}()
rc.Process(tp)
}
type TestPeer struct {
connection.AutoConnectionHandler
ReceivedIdentityPacket bool
ReceivedGroupInvite bool
}
func (tp *TestPeer) ClientIdentity(ci *protocol.CwtchIdentity) {
tp.ReceivedIdentityPacket = true
}
func (tp *TestPeer) HandleGroupInvite(gci *protocol.GroupChatInvite) {
tp.ReceivedGroupInvite = true
}
func (tp *TestPeer) GetClientIdentityPacket() []byte {
return nil
}
func TestPeerPeerConnection(t *testing.T) {
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
identity := identity.InitializeV3("", &priv, &pub)
profile := model.GenerateNewProfile("alice")
hostname := identity.Hostname()
ppc := NewPeerPeerConnection("127.0.0.1:5452|"+hostname, profile, nil)
tp := new(TestPeer)
tp.Init()
go runtestpeer(t, tp, identity)
state := ppc.GetState()
if state != DISCONNECTED {
t.Errorf("new connections should start in disconnected state")
}
go ppc.Run()
time.Sleep(time.Second * 5)
state = ppc.GetState()
if state != AUTHENTICATED {
t.Errorf("connection state should be authenticated(3), was instead %v", state)
}
if tp.ReceivedIdentityPacket == false {
t.Errorf("should have received an identity packet")
}
_, invite, _ := profile.StartGroup("aaa.onion")
ppc.SendGroupInvite(invite)
time.Sleep(time.Second * 3)
if tp.ReceivedGroupInvite == false {
t.Errorf("should have received an group invite packet")
}
}

View File

@ -0,0 +1,126 @@
package connections
import (
"crypto/rand"
"cwtch.im/cwtch/peer/fetch"
"cwtch.im/cwtch/peer/listen"
"cwtch.im/cwtch/peer/send"
"cwtch.im/cwtch/protocol"
"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"
"golang.org/x/crypto/ed25519"
"log"
"time"
)
// PeerServerConnection encapsulates a single Peer->Server connection
type PeerServerConnection struct {
connection.AutoConnectionHandler
Server string
state ConnectionState
connection *connection.Connection
GroupMessageHandler func(string, *protocol.GroupMessage)
}
// NewPeerServerConnection creates a new Peer->Server outbound connection
func NewPeerServerConnection(serverhostname string) *PeerServerConnection {
psc := new(PeerServerConnection)
psc.Server = serverhostname
psc.Init()
return psc
}
// GetState returns the current connection state
func (psc *PeerServerConnection) GetState() ConnectionState {
return psc.state
}
// Run manages the setup and teardown of a peer server connection
func (psc *PeerServerConnection) Run() error {
log.Printf("Connecting to %v", psc.Server)
rc, err := goricochet.Open(psc.Server)
if err == nil {
rc.TraceLog(true)
psc.connection = rc
psc.state = CONNECTED
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
if err == nil {
_, err := connection.HandleOutboundConnection(psc.connection).ProcessAuthAsV3Client(identity.InitializeV3("cwtchpeer", &priv, &pub))
if err == nil {
psc.state = 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.state = 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 != AUTHENTICATED {
return errors.New("peer is not yet connected & authenticated 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 * 1)
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.state = KILLED
psc.connection.Conn.Close()
}
// HandleGroupMessage passes the given group message back to the profile.
func (psc *PeerServerConnection) HandleGroupMessage(gm *protocol.GroupMessage) {
log.Printf("Received Group Message: %v", gm)
psc.GroupMessageHandler(psc.Server, gm)
}

View File

@ -0,0 +1,103 @@
package connections
import (
"crypto/rand"
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/server/fetch"
"cwtch.im/cwtch/server/send"
"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"
"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 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, identity identity.Identity) {
ln, _ := net.Listen("tcp", "127.0.0.1:5451")
conn, _ := ln.Accept()
defer conn.Close()
rc, err := goricochet.NegotiateVersionInbound(conn)
if err != nil {
t.Errorf("Negotiate Version Error: %v", err)
}
rc.TraceLog(true)
err = connection.HandleInboundConnection(rc).ProcessAuthAsV3Server(identity, 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) {
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
identity := identity.InitializeV3("", &priv, &pub)
ts := new(TestServer)
ts.Init()
go runtestserver(t, ts, identity)
onionAddr := identity.Hostname()
psc := NewPeerServerConnection("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()
time.Sleep(time.Second * 2)
state = psc.GetState()
if state != AUTHENTICATED {
t.Errorf("connection should now be authed(%v), instead was %v", AUTHENTICATED, state)
}
gm := &protocol.GroupMessage{Ciphertext: []byte("hello"), Signature: []byte{}}
psc.SendGroupMessage(gm)
time.Sleep(time.Second * 2)
if ts.Received == false {
t.Errorf("Should have received a group message in test server")
}
if numcalls != 2 {
t.Errorf("Should have received 2 calls from fetch request, instead received %v", numcalls)
}
}

View File

@ -7,25 +7,17 @@ type ConnectionState int
// DISCONNECTED - No existing connection has been made, or all attempts have failed
// CONNECTING - We are in the process of attempting to connect to a given endpoint
// CONNECTED - We have connected but not yet authenticated
// AUTHENTICATED - im.ricochet.auth-hidden-server has succeeded on the connection.
// SYNCED - we have pulled all the messages for groups from the server and are ready to send
// AUTHENTICATED - im.ricochet.auth-hidden-server has succeeded on thec onnection.
const (
DISCONNECTED ConnectionState = iota
CONNECTING
CONNECTED
AUTHENTICATED
SYNCED
FAILED
KILLED
)
var (
// ConnectionStateName allows conversion of states to their string representations
ConnectionStateName = []string{"Disconnected", "Connecting", "Connected", "Authenticated", "Synced", "Failed", "Killed"}
// ConnectionStateName allows conversaion of states to their string representations
ConnectionStateName = []string{"Disconnected", "Connecting", "Connected", "Authenticated", "Failed", "Killed"}
)
// ConnectionStateToType allows conversion of strings to their state type
func ConnectionStateToType() map[string]ConnectionState {
return map[string]ConnectionState{"Disconnected": DISCONNECTED, "Connecting": CONNECTING,
"Connected": CONNECTED, "Authenticated": AUTHENTICATED, "Synced": SYNCED, "Failed": FAILED, "Killed": KILLED}
}

File diff suppressed because it is too large Load Diff

74
peer/cwtch_peer_test.go Normal file
View File

@ -0,0 +1,74 @@
package peer
import (
"testing"
)
func TestCwtchPeerGenerate(t *testing.T) {
alice, _ := NewCwtchPeer("alice", "testpass", "./alice.json")
alice.Save()
aliceLoaded, err := LoadCwtchPeer("./alice.json", "testpass")
if err != nil || aliceLoaded.GetProfile().Name != "alice" {
t.Errorf("something went wrong saving and loading profiles %v %v", err, aliceLoaded)
}
groupID, _, _ := aliceLoaded.StartGroup("test.server")
exportedGroup, _ := aliceLoaded.ExportGroup(groupID)
t.Logf("Exported Group: %v from %v", exportedGroup, aliceLoaded.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 := "test.server"
alice, _ := NewCwtchPeer("alice", "alicepass", "")
bob, _ := NewCwtchPeer("bob", "bobpass", "")
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,103 @@
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)
}
// Type returns the type string for this channel, e.g. "im.ricochet.server.fetch)
func (cpfc *CwtchPeerFetchChannel) Type() string {
return "im.cwtch.server.fetch"
}
// Closed is called when the channel is closed for any reason.
func (cpfc *CwtchPeerFetchChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch server channels only client can open
func (cpfc *CwtchPeerFetchChannel) OnlyClientCanOpen() bool {
return true
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cpfc *CwtchPeerFetchChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cpfc *CwtchPeerFetchChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch server channels require no auth.
func (cpfc *CwtchPeerFetchChannel) RequiresAuthentication() string {
return "none"
}
// OpenInbound - cwtch server peer implementations shouldnever respond to inbound requests
func (cpfc *CwtchPeerFetchChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
return nil, errors.New("client does not receive inbound listen channels")
}
// OpenOutbound sets up a new cwtch fetch channel
func (cpfc *CwtchPeerFetchChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cpfc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cpfc.Type()), nil
}
// OpenOutboundResult confirms a previous open channel request
func (cpfc *CwtchPeerFetchChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cpfc.channel.Pending = false
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
go cpfc.Handler.HandleGroupMessage(gm)
}
}
}

View File

@ -0,0 +1,92 @@
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
}
func (th *TestHandler) HandleGroupMessage(m *protocol.GroupMessage) {
th.Received = true
}
func TestPeerFetchChannelAttributes(t *testing.T) {
cssc := new(CwtchPeerFetchChannel)
if cssc.Type() != "im.cwtch.server.fetch" {
t.Errorf("cwtch channel type is incorrect %v", cssc.Type())
}
if !cssc.OnlyClientCanOpen() {
t.Errorf("only clients should be able to open im.cwtch.server.Fetch channel")
}
if cssc.Bidirectional() {
t.Errorf("im.cwtch.server.fetch should not be bidirectional")
}
if !cssc.Singleton() {
t.Errorf("im.cwtch.server.fetch should be a Singleton")
}
if cssc.RequiresAuthentication() != "none" {
t.Errorf("cwtch channel required auth is incorrect %v", cssc.RequiresAuthentication())
}
}
func TestPeerFetchChannelOpenInbound(t *testing.T) {
cssc := new(CwtchPeerFetchChannel)
channel := new(channels.Channel)
_, err := cssc.OpenInbound(channel, nil)
if err == nil {
t.Errorf("client implementation of im.cwtch.server.Fetch should never open an inbound channel")
}
}
func TestPeerFetchChannel(t *testing.T) {
pfc := new(CwtchPeerFetchChannel)
th := new(TestHandler)
pfc.Handler = th
channel := new(channels.Channel)
channel.ID = 3
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,86 @@
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()
// 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
go 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)
}

114
peer/peer/peer_channel.go Normal file
View File

@ -0,0 +1,114 @@
package peer
import (
"cwtch.im/cwtch/protocol"
"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"
"log"
)
// CwtchPeerChannel implements the ChannelHandler interface for a channel of
// type "im.ricochet.Cwtch". The channel may be inbound or outbound.
//
// CwtchPeerChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchPeerChannelHandler implementation to handle Cwtch events.
type CwtchPeerChannel struct {
// Methods of Handler are called for Cwtch events on this channel
Handler CwtchPeerChannelHandler
channel *channels.Channel
}
// CwtchPeerChannelHandler is implemented by an application type to receive
// events from a CwtchPeerChannel.
type CwtchPeerChannelHandler interface {
ClientIdentity(*protocol.CwtchIdentity)
HandleGroupInvite(*protocol.GroupChatInvite)
GetClientIdentityPacket() []byte
}
// SendMessage sends a raw message on this channel
func (cpc *CwtchPeerChannel) SendMessage(data []byte) {
cpc.channel.SendMessage(data)
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cpc *CwtchPeerChannel) Type() string {
return "im.cwtch.peer"
}
// Closed is called when the channel is closed for any reason.
func (cpc *CwtchPeerChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cpc *CwtchPeerChannel) OnlyClientCanOpen() bool {
return false
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cpc *CwtchPeerChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are not bidrectional
func (cpc *CwtchPeerChannel) Bidirectional() bool {
return false
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cpc *CwtchPeerChannel) RequiresAuthentication() string {
return "im.ricochet.auth.3dh"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cpc *CwtchPeerChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
cpc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.AckOpenChannel(channel.ID), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cpc *CwtchPeerChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cpc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cpc.Type()), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cpc *CwtchPeerChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cpc.channel.Pending = false
}
}
}
// Packet is called for each raw packet received on this channel.
func (cpc *CwtchPeerChannel) Packet(data []byte) {
cpp := &protocol.CwtchPeerPacket{}
err := proto.Unmarshal(data, cpp)
if err == nil {
if cpp.GetCwtchIdentify() != nil {
cpc.Handler.ClientIdentity(cpp.GetCwtchIdentify())
pkt := cpc.Handler.GetClientIdentityPacket()
if pkt != nil {
cpc.SendMessage(pkt)
}
} else if cpp.GetGroupChatInvite() != nil {
cpc.Handler.HandleGroupInvite(cpp.GetGroupChatInvite())
}
} else {
log.Printf("Error Receivng Packet %v\n", err)
}
}

View File

@ -0,0 +1,121 @@
package peer
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 TestPeerChannelAttributes(t *testing.T) {
cssc := new(CwtchPeerChannel)
if cssc.Type() != "im.cwtch.peer" {
t.Errorf("cwtch channel type is incorrect %v", cssc.Type())
}
if cssc.OnlyClientCanOpen() {
t.Errorf("either side should be able to open im.cwtch.peer channel")
}
if cssc.Bidirectional() {
t.Errorf("im.cwtch.peer should not be bidirectional")
}
if !cssc.Singleton() {
t.Errorf("im.cwtch.server.listen should be a Singleton")
}
if cssc.RequiresAuthentication() != "im.ricochet.auth.3dh" {
t.Errorf("cwtch channel required auth is incorrect %v", cssc.RequiresAuthentication())
}
}
type TestHandler struct {
Received bool
ReceviedGroupInvite bool
}
func (th *TestHandler) ClientIdentity(ci *protocol.CwtchIdentity) {
if ci.GetName() == "hello" {
th.Received = true
}
}
func (th *TestHandler) HandleGroupInvite(ci *protocol.GroupChatInvite) {
///if ci.GetName() == "hello" {
th.ReceviedGroupInvite = true
//}
}
func (th *TestHandler) GetClientIdentityPacket() []byte {
return nil
}
func TestPeerChannel(t *testing.T) {
th := new(TestHandler)
cpc := new(CwtchPeerChannel)
cpc.Handler = th
channel := new(channels.Channel)
channel.ID = 3
result, err := cpc.OpenOutbound(channel)
if err != nil {
t.Errorf("should have send open channel request instead %v, %v", result, err)
}
cpc2 := new(CwtchPeerChannel)
channel2 := new(channels.Channel)
channel2.ID = 3
sent := false
channel2.SendMessage = func(message []byte) {
sent = true
}
control := new(Protocol_Data_Control.Packet)
proto.Unmarshal(result[:], control)
ack, err := cpc2.OpenInbound(channel2, control.GetOpenChannel())
if err != nil {
t.Errorf("should have ack open channel request instead %v, %v", ack, err)
}
ackpacket := new(Protocol_Data_Control.Packet)
proto.Unmarshal(ack[:], ackpacket)
cpc.OpenOutboundResult(nil, ackpacket.GetChannelResult())
if channel.Pending != false {
t.Errorf("Channel should no longer be pending")
}
gm := &protocol.CwtchIdentity{
Name: "hello",
Ed25519PublicKey: []byte{},
}
cpp := &protocol.CwtchPeerPacket{
CwtchIdentify: gm,
}
packet, _ := proto.Marshal(cpp)
cpc.Packet(packet)
if th.Received == false {
t.Errorf("Should have sent packet to handler")
}
cpc2.SendMessage(packet)
if sent == false {
t.Errorf("Should have sent packet to channel")
}
gci := &protocol.GroupChatInvite{
GroupName: "hello",
GroupSharedKey: []byte{},
ServerHost: "abc.onion",
}
cpp = &protocol.CwtchPeerPacket{
GroupChatInvite: gci,
}
packet, _ = proto.Marshal(cpp)
cpc.Packet(packet)
if th.ReceviedGroupInvite == false {
t.Errorf("Should have sent invite packet to handler")
}
}

View File

@ -0,0 +1,98 @@
package peer
import (
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
)
// CwtchPeerDataChannel implements the ChannelHandler interface for a channel of
// type "im.cwtch.peer.data The channel may be inbound or outbound.
//
// CwtchPeerChannel implements protocol-level sanity and state validation, but
// does not handle or acknowledge Cwtch messages. The application must provide
// a CwtchPeerChannelHandler implementation to handle Cwtch events.
type CwtchPeerDataChannel struct {
// Methods of Handler are called for Cwtch events on this channel
Handler CwtchPeerDataHandler
channel *channels.Channel
}
// CwtchPeerDataHandler is implemented by an application type to receive
// events from a CwtchPeerChannel.
type CwtchPeerDataHandler interface {
HandlePacket([]byte) []byte
}
// SendMessage sends a raw message on this channel
func (cpc *CwtchPeerDataChannel) SendMessage(data []byte) {
cpc.channel.SendMessage(data)
}
// Type returns the type string for this channel, e.g. "im.ricochet.Cwtch".
func (cpc *CwtchPeerDataChannel) Type() string {
return "im.cwtch.peer.data"
}
// Closed is called when the channel is closed for any reason.
func (cpc *CwtchPeerDataChannel) Closed(err error) {
}
// OnlyClientCanOpen - for Cwtch channels any side can open
func (cpc *CwtchPeerDataChannel) OnlyClientCanOpen() bool {
return false
}
// Singleton - for Cwtch channels there can only be one instance per direction
func (cpc *CwtchPeerDataChannel) Singleton() bool {
return true
}
// Bidirectional - for Cwtch channels are bidirectional
func (cpc *CwtchPeerDataChannel) Bidirectional() bool {
return true
}
// RequiresAuthentication - Cwtch channels require hidden service auth
func (cpc *CwtchPeerDataChannel) RequiresAuthentication() string {
return "im.ricochet.auth.3dh"
}
// OpenInbound is the first method called for an inbound channel request.
// If an error is returned, the channel is rejected. If a RawMessage is
// returned, it will be sent as the ChannelResult message.
func (cpc *CwtchPeerDataChannel) OpenInbound(channel *channels.Channel, raw *Protocol_Data_Control.OpenChannel) ([]byte, error) {
cpc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.AckOpenChannel(channel.ID), nil
}
// OpenOutbound is the first method called for an outbound channel request.
// If an error is returned, the channel is not opened. If a RawMessage is
// returned, it will be sent as the OpenChannel message.
func (cpc *CwtchPeerDataChannel) OpenOutbound(channel *channels.Channel) ([]byte, error) {
cpc.channel = channel
messageBuilder := new(utils.MessageBuilder)
return messageBuilder.OpenChannel(channel.ID, cpc.Type()), nil
}
// OpenOutboundResult is called when a response is received for an
// outbound OpenChannel request. If `err` is non-nil, the channel was
// rejected and Closed will be called immediately afterwards. `raw`
// contains the raw protocol message including any extension data.
func (cpc *CwtchPeerDataChannel) OpenOutboundResult(err error, crm *Protocol_Data_Control.ChannelResult) {
if err == nil {
if crm.GetOpened() {
cpc.channel.Pending = false
}
}
}
// Packet is called for each raw packet received on this channel.
func (cpc *CwtchPeerDataChannel) Packet(data []byte) {
ret := cpc.Handler.HandlePacket(data)
if len(ret) >= 0 {
cpc.channel.SendMessage(ret)
}
}

View File

@ -0,0 +1,95 @@
package send
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/protocol/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/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,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

@ -1,515 +0,0 @@
package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol/groups"
"encoding/base64"
"encoding/json"
"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"
"golang.org/x/crypto/ed25519"
"strconv"
"sync"
"time"
)
type engine struct {
queue event.Queue
// Engine Attributes
identity primitives.Identity
acn connectivity.ACN
// Authorization list of contacts to authorization status
authorizations sync.Map // string(onion) => model.Authorization
// Block Unknown Contacts
blockUnknownContacts bool
// Pointer to the Global Event Manager
eventManager event.Manager
// 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
shuttingDown bool
}
// 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
Shutdown()
}
// 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 {
engine := new(engine)
engine.identity = identity
engine.privateKey = privateKey
engine.queue = event.NewQueue()
go engine.eventHandler()
engine.acn = acn
// Init the Server running the Simple App.
engine.service = new(tor.BaseOnionService)
engine.service.Init(acn, privateKey, &identity)
engine.eventManager = eventManager
engine.eventManager.Subscribe(event.ProtocolEngineStartListen, engine.queue)
engine.eventManager.Subscribe(event.PeerRequest, engine.queue)
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.BlockUnknownPeers, engine.queue)
engine.eventManager.Subscribe(event.AllowUnknownPeers, engine.queue)
for peer, authorization := range peerAuthorizations {
engine.authorizations.Store(peer, authorization)
}
return engine
}
func (e *engine) ACN() connectivity.ACN {
return e.acn
}
func (e *engine) EventManager() event.Manager {
return e.eventManager
}
// eventHandler process events from other subsystems
func (e *engine) eventHandler() {
for {
ev := e.queue.Next()
switch ev.EventType {
case event.StatusRequest:
e.eventManager.Publish(event.Event{EventType: event.ProtocolEngineStatus, EventID: ev.EventID})
case event.PeerRequest:
if torProvider.IsValidHostname(ev.Data[event.RemotePeer]) {
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])
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])
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.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)
case event.SendMessageToPeer:
// TODO: remove this passthrough once the UI is integrated.
context, ok := ev.Data[event.EventContext]
if !ok {
context = event.ContextRaw
}
err := e.sendMessageToPeer(ev.EventID, ev.Data[event.RemotePeer], context, []byte(ev.Data[event.Data]))
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.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()
default:
return
}
}
}
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.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
}
// Listen sets up an onion listener to process incoming cwtch messages
func (e *engine) listenFn() {
err := e.service.Listen(e.createPeerTemplate())
if !e.shuttingDown {
e.eventManager.Publish(event.NewEvent(event.ProtocolEngineStopped, map[event.Field]string{event.Identity: e.identity.Hostname(), event.Error: err.Error()}))
}
}
// Shutdown tears down the eventHandler goroutine
func (e *engine) Shutdown() {
e.shuttingDown = true
e.service.Shutdown()
e.queue.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) {
e.ignoreOnShutdown(e.peerConnecting)(onion)
connected, err := e.service.Connect(onion, e.createPeerTemplate())
// 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 := e.service.GetConnection(onion)
if err == nil {
if conn.HasCapability(cwtchCapability) {
e.ignoreOnShutdown(e.peerAuthed)(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.peerDisconnected)(onion)
}
}
}
// 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 {
f(x)
}
}
}
func (e *engine) ignoreOnShutdown2(f func(string, string)) func(string, string) {
return func(x, y string) {
if !e.shuttingDown {
f(x, y)
}
}
}
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],
}))
}
func (e *engine) peerConnecting(onion string) {
e.eventManager.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{
event.RemotePeer: string(onion),
event.ConnectionState: ConnectionStateName[CONNECTING],
}))
}
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,
event.RemotePeer: onion,
}))
}
func (e *engine) peerDisconnected(onion string) {
e.eventManager.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{
event.RemotePeer: string(onion),
event.ConnectionState: ConnectionStateName[DISCONNECTED],
}))
}
// 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)
if err == nil {
peerApp, ok := (conn.App()).(*PeerApp)
if ok {
peerApp.SendMessage(PeerMessage{eventID, context, message})
return nil
}
return errors.New("failed type assertion conn.App != PeerApp")
}
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 {
conn.Close()
}
}
// receiveGroupMessage is a callback function that processes GroupMessages from a given server
func (e *engine) receiveGroupMessage(server string, gm *groups.EncryptedGroupMessage) {
// 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)}))
}
// 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
}
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
}
}
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) {
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)
}
} 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

@ -1,126 +0,0 @@
package connections
import (
"cwtch.im/cwtch/event"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/openprivacy/log"
"sync"
)
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)
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
type PeerMessage struct {
ID string // A unique Message ID (primarily used for acknowledgments)
Context string // A unique context identifier i.e. im.cwtch.chat
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 {
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()
pa.OnClose(connection.Hostname())
} else {
pa.OnAuth(connection.Hostname())
go pa.listen()
}
} else {
// The auth protocol wasn't completed, we can safely shutdown the connection
connection.Close()
}
}
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")
pa.OnClose(pa.connection.Hostname())
return
}
var peerMessage PeerMessage
err := json.Unmarshal(message, &peerMessage)
if err == nil {
switch peerMessage.Context {
case 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)
// Acknowledge the message
pa.SendMessage(PeerMessage{peerMessage.ID, event.ContextAck, []byte{}})
}
}
} else {
log.Errorf("Error unmarshalling PeerMessage package: %x %v", message, err)
}
}
}
// 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))
}
serialized, _ := json.Marshal(message)
pa.connection.Send(serialized)
}

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,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
}

100
protocol/spam/spamguard.go Normal file
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
sum := sha256.Sum256([]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

@ -1,18 +1,8 @@
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"
"os"
"os/signal"
"syscall"
"time"
"log"
)
const (
@ -20,81 +10,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)})
if err != nil {
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)
}
defer acn.Close()
serverConfig := cwtchserver.LoadConfig(serverConfigFile)
server := new(cwtchserver.Server)
log.Infoln("starting cwtch server...")
log.Printf("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
server.Run(serverConfig)
}

View File

@ -1,33 +0,0 @@
#!/bin/sh
set -o errexit
chmod_files() { find $2 -type f -exec chmod -v $1 {} \;
}
chmod_dirs() { find $2 -type d -exec chmod -v $1 {} \;
}
chown ${TOR_USER}:${TOR_USER} /run/tor/
chmod 770 /run/tor
chown -Rv ${TOR_USER}:${TOR_USER} /var/lib/tor
chmod_dirs 700 /var/lib/tor
chmod_files 600 /var/lib/tor
echo -e "\n========================================================"
# Display OS version, Tor version & torrc in log
echo -e "Alpine Version: \c" && cat /etc/alpine-release
tor --version
#cat /etc/tor/torrc
echo -e "========================================================\n"
tor -f /etc/tor/torrc
#Cwtch will crash and burn if 9051 isn't ready
sleep 15
if [ -z "${CWTCH_CONFIG_DIR}" ]; then
CWTCH_CONFIG_DIR=/etc/cwtch/
fi
#Run cwtch (or whatever the user passed)
CWTCH_CONFIG_DIR=$CWTCH_CONFIG_DIR exec "$@"

View File

@ -1,27 +0,0 @@
User _tor
DataDirectory /var/lib/tor
ORPort 0
ExitRelay 0
IPv6Exit 0
#We need this running in the background as the server doesn't launch it itself
RunAsDaemon 1
ClientOnly 1
SocksPort 9050
ControlPort 9051
ControlSocket /run/tor/control
ControlSocketsGroupWritable 1
CookieAuthentication 1
CookieAuthFile /run/tor/control.authcookie
CookieAuthFileGroupReadable 1
#HashedControlPassword 16:B4C8EE980C085EE460AEA9094350DAA9C2B5F841400E9BBA247368400A
# Run as a relay only (change policy to enable exit node)
ExitPolicy reject *:* # no exits allowed
ExitPolicy reject6 *:*
# Additional config built by the entrypoint will go here

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

@ -9,20 +9,13 @@ func TestCounter(t *testing.T) {
starttime := time.Now()
c := NewCounter()
max := 100
done := make(chan bool, max)
// slightly stress test atomic nature of metric by flooding with threads Add()ing
for i := 0; i < max; i++ {
for i := 0; i < 100; i++ {
go func() {
c.Add(1)
done <- true
}()
}
for i := 0; i < max; i++ {
<-done
}
time.Sleep(2 * time.Second)
val := c.Count()
if val != 100 {
@ -32,6 +25,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,10 @@ package metrics
import (
"bufio"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/openprivacy/log"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"github.com/struCoder/pidusage"
"log"
"os"
"path"
"sync"
"time"
)
@ -18,51 +16,26 @@ 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
}
// Start initializes a Monitors's monitors
func (mp *Monitors) Start(ts tapir.Service, configDir string, log bool) {
func (mp *Monitors) Start(ra *application.RicochetApplication, 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()
@ -81,9 +54,9 @@ func (mp *Monitors) run() {
}
func (mp *Monitors) report() {
f, err := os.Create(path.Join(mp.configDir, reportFile))
f, err := os.Create(reportFile)
if err != nil {
log.Errorf("Could not open monitor reporting file: %v", err)
log.Println("ERROR: Could not open monitor reporting file: ", err)
return
}
defer f.Close()
@ -92,7 +65,7 @@ func (mp *Monitors) report() {
fmt.Fprintf(w, "Uptime: %v\n\n", time.Now().Sub(mp.starttime))
fmt.Fprintln(w, "messages:")
fmt.Fprintln(w, "Messages:")
mp.Messages.Report(w)
fmt.Fprintln(w, "\nClient Connections:")

View File

@ -0,0 +1,99 @@
package send
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/protocol/spam"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"git.openprivacy.ca/openprivacy/libricochet-go/wire/control"
"github.com/golang/protobuf/proto"
"log"
)
// 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.Printf("[ERROR] Failed to validate spamguard %v\n", gm)
}
}
} else {
log.Printf("[ERROR] 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/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,79 @@
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/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"
"cwtch.im/cwtch/server/send"
"cwtch.im/cwtch/storage"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
"log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
)
// 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
}
// 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
func (s *Server) Run(serverConfig Config) {
s.config = serverConfig
cwtchserver := new(application.RicochetApplication)
s.metricsPack.Start(cwtchserver, s.config.ServerReporting.LogMetricsToFile)
l, err := application.SetupOnionV3("127.0.0.1:9051", "tcp4", "", s.config.PrivateKey, utils.GetTorV3Hostname(s.config.PublicKey), 9878)
ms := new(storage.MessageStore)
err := ms.Init(s.config.ConfigDir, s.config.MaxBufferLines, s.metricsPack.MessageCounter)
if err != nil {
return err
log.Fatalf("error setting up onion service: %v", err)
}
// Needed because we only collect metrics on a per-session basis
// TODO fix metrics so they persist across sessions?
s.existingMessageCount = len(ms.FetchMessages())
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
}()
s.lock.Lock()
s.running = true
s.lock.Unlock()
return nil
}
// 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
}
// 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)
af := application.ApplicationInstanceFactory{}
af.Init()
ms := new(storage.MessageStore)
err = ms.Init(".", s.config.MaxBufferLines, s.metricsPack.MessageCounter)
if err != nil {
log.Fatal(err)
}
return s.running, nil
af.AddHandler("im.cwtch.server.listen", func(rai *application.ApplicationInstance) func() channels.Handler {
return func() channels.Handler {
cslc := new(listen.CwtchServerListenChannel)
return cslc
}
})
af.AddHandler("im.cwtch.server.fetch", func(rai *application.ApplicationInstance) func() channels.Handler {
si := new(Instance)
si.Init(rai, cwtchserver, ms)
return func() channels.Handler {
cssc := new(fetch.CwtchServerFetchChannel)
cssc.Handler = si
return cssc
}
})
af.AddHandler("im.cwtch.server.send", func(rai *application.ApplicationInstance) func() channels.Handler {
si := new(Instance)
si.Init(rai, cwtchserver, ms)
return func() channels.Handler {
cssc := new(send.CwtchServerSendChannel)
cssc.Handler = si
return cssc
}
})
cwtchserver.InitV3("cwtch server for "+l.Addr().String(), s.config.Identity(), af, new(application.AcceptAllContactManager))
log.Printf("cwtch server running on cwtch:%s", l.Addr().String())
s.app = cwtchserver
s.app.Run(l)
}
// 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.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,12 +3,10 @@ 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"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"path"
"log"
)
// Reporting is a struct for storing a the config a server needs to be a peer, and connect to a group to report
@ -20,79 +18,42 @@ 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"`
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
func (config *Config) Save(dir, filename string) {
log.Infof("Saving config to %s\n", path.Join(dir, filename))
func (config *Config) Save(filename string) {
bytes, _ := json.MarshalIndent(config, "", "\t")
ioutil.WriteFile(path.Join(dir, filename), bytes, 0600)
ioutil.WriteFile(filename, bytes, 0600)
}
// LoadConfig loads a Config from a json file specified by filename
func LoadConfig(configDir, filename string) Config {
log.Infof("Loading config from %s\n", path.Join(configDir, filename))
func LoadConfig(filename string) Config {
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
raw, err := ioutil.ReadFile(path.Join(configDir, filename))
config.ServerReporting.LogMetricsToFile = false
raw, err := ioutil.ReadFile(filename)
if err == nil {
err = json.Unmarshal(raw, &config)
if err != nil {
log.Errorf("reading config: %v", err)
log.Println("Error reading config: ", 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)
config.Save(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/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.ApplicationInstance
ra *application.RicochetApplication
msi storage.MessageStoreInterface
}
// Init sets up a Server Instance
func (si *Instance) Init(rai *application.ApplicationInstance, 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.ApplicationInstance) {
rai.Connection.Do(func() error {
channel := rai.Connection.Channel("im.cwtch.server.listen", channels.Inbound)
if channel != nil {
cslc, ok := channel.Handler.(*listen.CwtchServerListenChannel)
if ok {
cslc.SendGroupMessage(gm)
}
}
return nil
})
})
}

View File

@ -0,0 +1,37 @@
package server
import (
"cwtch.im/cwtch/protocol"
"cwtch.im/cwtch/server/metrics"
"cwtch.im/cwtch/storage"
"git.openprivacy.ca/openprivacy/libricochet-go/application"
"os"
"testing"
"time"
)
func TestServerInstance(t *testing.T) {
si := new(Instance)
ai := new(application.ApplicationInstance)
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,13 +2,12 @@ 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"
"log"
"os"
"path"
"sync"
)
@ -20,8 +19,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 +29,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 +44,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 {
@ -58,10 +57,10 @@ func (ms *MessageStore) initAndLoadFiles() error {
ms.activeLogFile = nil
for i := fileStorePartitions - 1; i >= 0; i-- {
ms.filePos = 0
filename := path.Join(ms.storeDirectory, fmt.Sprintf("%s.%d", fileStoreFilename, i))
filename := fmt.Sprintf("%s%s.%d", ms.storeDirectory, fileStoreFilename, i)
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
if err != nil {
log.Errorf("MessageStore could not open: %v: %v", filename, err)
log.Printf("Error: MessageStore could not open: %v: %v", filename, err)
continue
}
ms.activeLogFile = f
@ -70,7 +69,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,10 +82,10 @@ 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)
log.Printf("[ERROR] Failed to unmarshal group message %v\n", err)
}
fmt.Fprintf(ms.activeLogFile, "%s\n", s)
ms.filePos++
@ -97,15 +96,15 @@ func (ms *MessageStore) updateFile(gm *groups.EncryptedGroupMessage) {
func (ms *MessageStore) rotateFileStore() {
ms.activeLogFile.Close()
os.Remove(path.Join(ms.storeDirectory, fmt.Sprintf("%s.%d", fileStoreFilename, fileStorePartitions-1)))
os.Remove(fmt.Sprintf("%s%s.%d", ms.storeDirectory, fileStoreFilename, fileStorePartitions-1))
for i := fileStorePartitions - 2; i >= 0; i-- {
os.Rename(path.Join(ms.storeDirectory, fmt.Sprintf("%s.%d", fileStoreFilename, i)), path.Join(ms.storeDirectory, fmt.Sprintf("%s.%d", fileStoreFilename, i+1)))
os.Rename(fmt.Sprintf("%s%s.%d", ms.storeDirectory, fileStoreFilename, i), fmt.Sprintf("%s%s.%d", ms.storeDirectory, fileStoreFilename, i+1))
}
f, err := os.OpenFile(path.Join(ms.storeDirectory, fmt.Sprintf("%s.%d", fileStoreFilename, 0)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
f, err := os.OpenFile(fmt.Sprintf("%s%s.%d", ms.storeDirectory, fileStoreFilename, 0), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
if err != nil {
log.Errorf("Could not open new message store file in: %s", ms.storeDirectory)
log.Printf("ERROR: Could not open new message store file in: %s", ms.storeDirectory)
}
ms.filePos = 0
ms.activeLogFile = f
@ -113,12 +112,12 @@ func (ms *MessageStore) rotateFileStore() {
// Init sets up a MessageStore of size maxBufferLines (# of messages) backed by filename
func (ms *MessageStore) Init(appDirectory string, maxBufferLines int, messageCounter metrics.Counter) error {
ms.storeDirectory = path.Join(appDirectory, directory)
ms.storeDirectory = appDirectory + "/" + directory + "/"
os.Mkdir(ms.storeDirectory, 0700)
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 +126,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 +141,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

@ -1,93 +1 @@
package storage
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/storage/v0"
"cwtch.im/cwtch/storage/v1"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"path"
"strconv"
)
const profileFilename = "profile"
const versionFile = "VERSION"
const currentVersion = 1
// ProfileStore is an interface to managing the storage of Cwtch Profiles
type ProfileStore interface {
Shutdown()
Delete()
GetProfileCopy(timeline bool) *model.Profile
GetNewPeerMessage() *event.Event
GetStatusMessages() []*event.Event
}
// CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them
// directory should be $appDir/profiles/$rand
func CreateProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) ProfileStore {
return v1.CreateProfileWriterStore(eventManager, directory, password, profile)
}
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
// directory should be $appDir/profiles/$rand
func LoadProfileWriterStore(eventManager event.Manager, directory, password string) (ProfileStore, error) {
versionCheckUpgrade(directory, password)
return v1.LoadProfileWriterStore(eventManager, directory, password)
}
// ReadProfile reads a profile from storage and returns the profile
// Should only be called for cache refresh of the profile after a ProfileWriterStore has opened
// (and upgraded) the store, and thus supplied the key/salt
func ReadProfile(directory string, key [32]byte, salt [128]byte) (*model.Profile, error) {
return v1.ReadProfile(directory, key, salt)
}
// NewProfile creates a new profile for use in the profile store.
func NewProfile(name string) *model.Profile {
profile := model.GenerateNewProfile(name)
return profile
}
// ********* Versioning and upgrade **********
func detectVersion(directory string) int {
vnumberStr, err := ioutil.ReadFile(path.Join(directory, versionFile))
if err != nil {
return 0
}
vnumber, err := strconv.Atoi(string(vnumberStr))
if err != nil {
log.Errorf("Could not parse VERSION file contents: '%v' - %v\n", vnumber, err)
return -1
}
return vnumber
}
func upgradeV0ToV1(directory, password string) error {
log.Debugln("Attempting storage v0 to v1: Reading v0 profile...")
profile, err := v0.ReadProfile(directory, password)
if err != nil {
return err
}
log.Debugln("Attempting storage v0 to v1: Writing v1 profile...")
return v1.UpgradeV0Profile(profile, directory, password)
}
func versionCheckUpgrade(directory, password string) {
version := detectVersion(directory)
log.Debugf("versionCheck: %v\n", version)
if version == -1 {
return
}
if version == 0 {
err := upgradeV0ToV1(directory, password)
if err != nil {
return
}
//version = 1
}
}

View File

@ -1,76 +0,0 @@
// Known race issue with event bus channel closure
package storage
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/storage/v0"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"os"
"testing"
"time"
)
const testingDir = "./testing"
const filenameBase = "testStream"
const password = "asdfqwer"
const line1 = "Hello from storage!"
const testProfileName = "Alice"
const testKey = "key"
const testVal = "value"
const testInitialMessage = "howdy"
const testMessage = "Hello from storage"
func TestProfileStoreUpgradeV0toV1(t *testing.T) {
log.SetLevel(log.LevelDebug)
os.RemoveAll(testingDir)
eventBus := event.NewEventManager()
queue := event.NewQueue()
eventBus.Subscribe(event.ChangePasswordSuccess, queue)
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")
if err != nil {
t.Errorf("Creating group: %v\n", err)
}
if err != nil {
t.Errorf("Creating group invite: %v\n", err)
}
ps1.AddGroup(invite)
fmt.Println("Sending 200 messages...")
for i := 0; i < 200; i++ {
ps1.AddGroupMessage(groupid, time.Now().Format(time.RFC3339Nano), time.Now().Format(time.RFC3339Nano), profile.Onion, testMessage)
}
fmt.Println("Shutdown v0 profile store...")
ps1.Shutdown()
fmt.Println("New v1 Profile store...")
ps2, err := LoadProfileWriterStore(eventBus, testingDir, password)
if err != nil {
t.Errorf("Error createing new profileStore with new password: %v\n", err)
return
}
profile2 := ps2.GetProfileCopy(true)
if profile2.Groups[groupid] == nil {
t.Errorf("Failed to load group %v\n", groupid)
return
}
if len(profile2.Groups[groupid].Timeline.Messages) != 200 {
t.Errorf("Failed to load group's 200 messages, instead got %v\n", len(profile2.Groups[groupid].Timeline.Messages))
}
}

View File

@ -1,70 +0,0 @@
package v0
import (
"crypto/rand"
"errors"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"io/ioutil"
"path"
)
// createKey derives a key from a password
func createKey(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)
return [32]byte{}, salt, err
}
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr, salt, nil
}
//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 {
log.Errorf("Cannot read from random: %v\n", err)
return nil, err
}
encrypted := secretbox.Seal(nonce[:], data, &nonce, &key)
return encrypted, nil
}
//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)
if ok {
return decrypted, nil
}
return nil, errors.New("Failed to decrypt")
}
// Load instantiates a cwtchPeer from the file store
func readEncryptedFile(directory, filename, password string) ([]byte, error) {
encryptedbytes, err := ioutil.ReadFile(path.Join(directory, filename))
if err == nil && len(encryptedbytes) > 128 {
var dkr [32]byte
//Separate the salt from the encrypted bytes, then generate the derived key
salt, encryptedbytes := encryptedbytes[0:128], encryptedbytes[128:]
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
copy(dkr[:], dk)
data, err := decryptFile(encryptedbytes, dkr)
if err == nil {
return data, nil
}
return nil, err
}
return nil, err
}

View File

@ -1,46 +0,0 @@
package v0
import (
"io/ioutil"
"path"
)
// fileStore stores a cwtchPeer in an encrypted file
type fileStore struct {
directory string
filename string
password string
}
// FileStore is a primitive around storing encrypted files
type FileStore interface {
Read() ([]byte, error)
Write(data []byte) error
}
// NewFileStore instantiates a fileStore given a filename and a password
func NewFileStore(directory string, filename string, password string) FileStore {
filestore := new(fileStore)
filestore.password = password
filestore.filename = filename
filestore.directory = directory
return filestore
}
func (fps *fileStore) Read() ([]byte, error) {
return readEncryptedFile(fps.directory, fps.filename, fps.password)
}
// write serializes a cwtchPeer to a file
func (fps *fileStore) Write(data []byte) error {
key, salt, _ := createKey(fps.password)
encryptedbytes, err := encryptFileData(data, key)
if err != nil {
return err
}
// the salt for the derived key is appended to the front of the file
encryptedbytes = append(salt[:], encryptedbytes...)
err = ioutil.WriteFile(path.Join(fps.directory, fps.filename), encryptedbytes, 0600)
return err
}

Some files were not shown because too many files have changed in this diff Show More