Merge branch 'master' of git.openprivacy.ca:cwtch.im/cwtch into countersync
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
erinn 2021-06-17 14:40:21 -07:00
commit e5ccb5522d
45 changed files with 1484 additions and 776 deletions

View File

@ -7,8 +7,8 @@ import (
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/storage"
"cwtch.im/tapir/primitives"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
@ -44,7 +44,7 @@ type Application interface {
LoadProfiles(password string)
CreatePeer(name string, password string)
CreateTaggedPeer(name string, password string, tag string)
DeletePeer(onion string)
DeletePeer(onion string, currentPassword string)
AddPeerPlugin(onion string, pluginID plugins.PluginID)
ChangePeerPassword(onion, oldpass, newpass string)
LaunchPeers()
@ -52,6 +52,7 @@ type Application interface {
GetPrimaryBus() event.Manager
GetEventBus(onion string) event.Manager
QueryACNStatus()
QueryACNVersion()
ShutdownPeer(string)
Shutdown()
@ -90,7 +91,7 @@ func (ac *applicationCore) CreatePeer(name string) (*model.Profile, error) {
_, exists := ac.eventBuses[profile.Onion]
if exists {
return nil, fmt.Errorf("Error: profile for onion %v already exists", profile.Onion)
return nil, fmt.Errorf("error: profile for onion %v already exists", profile.Onion)
}
eventBus := event.NewEventManager()
@ -133,7 +134,7 @@ func (app *application) CreateTaggedPeer(name string, password string, tag strin
p.SetAttribute(AttributeTag, tag)
}
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion}))
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)
@ -141,28 +142,33 @@ func (app *application) CreatePeer(name string, password string) {
app.CreateTaggedPeer(name, password, "")
}
func (app *application) DeletePeer(onion string) {
func (app *application) DeletePeer(onion string, password string) {
log.Infof("DeletePeer called on %v\n", onion)
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.appletPlugins.ShutdownPeer(onion)
app.plugins.Delete(onion)
if app.storage[onion].CheckPassword(password) {
app.appletPlugins.ShutdownPeer(onion)
app.plugins.Delete(onion)
app.peers[onion].Shutdown()
delete(app.peers, onion)
app.peers[onion].Shutdown()
delete(app.peers, onion)
app.engines[onion].Shutdown()
delete(app.engines, onion)
app.engines[onion].Shutdown()
delete(app.engines, onion)
app.storage[onion].Shutdown()
app.storage[onion].Delete()
delete(app.storage, 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.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
app.applicationCore.DeletePeer(onion)
log.Debugf("Delete peer for %v Done\n", onion)
app.applicationCore.DeletePeer(onion)
log.Debugf("Delete peer for %v Done\n", onion)
app.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
return
}
app.appBus.Publish(event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion))
}
func (app *application) ChangePeerPassword(onion, oldpass, newpass string) {
@ -177,7 +183,7 @@ func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProfileFn LoadProfileFn) error {
files, err := ioutil.ReadDir(path.Join(ac.directory, "profiles"))
if err != nil {
return fmt.Errorf("Error: cannot read profiles directory: %v", err)
return fmt.Errorf("error: cannot read profiles directory: %v", err)
}
for _, file := range files {
@ -221,7 +227,7 @@ func (app *application) LoadProfiles(password string) {
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}))
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.False}))
count++
})
if count == 0 {
@ -247,9 +253,9 @@ 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.Progreess, progStr, event.Status, status))
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.Progreess, progStr, event.Status, status))
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
}
app.peerLock.Unlock()
}
@ -260,6 +266,11 @@ func (app *application) QueryACNStatus() {
app.getACNStatusHandler()(prog, status)
}
func (app *application) QueryACNVersion() {
version := app.acn.GetVersion()
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (app *application) ShutdownPeer(onion string) {
app.appmutex.Lock()

View File

@ -45,8 +45,9 @@ func (ac *applicationClient) handleEvent(ev *event.Event) {
key := ev.Data[event.Key]
salt := ev.Data[event.Salt]
reload := ev.Data[event.Status] == event.StorageRunning
ac.newPeer(localID, key, salt, reload)
case event.DeletePeer:
created := ev.Data[event.Created]
ac.newPeer(localID, key, salt, reload, created)
case event.PeerDeleted:
onion := ev.Data[event.Identity]
ac.handleDeletedPeer(onion)
case event.PeerError:
@ -55,12 +56,14 @@ func (ac *applicationClient) handleEvent(ev *event.Event) {
ac.appBus.Publish(*ev)
case event.ACNStatus:
ac.appBus.Publish(*ev)
case event.ACNVersion:
ac.appBus.Publish(*ev)
case event.ReloadDone:
ac.appBus.Publish(*ev)
}
}
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool) {
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool, created string) {
var keyBytes [32]byte
var saltBytes [128]byte
copy(keyBytes[:], key)
@ -87,9 +90,9 @@ func (ac *applicationClient) newPeer(localID, key, salt string, reload bool) {
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})
npEvent := event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: created})
if reload {
npEvent.Data[event.Status] = "running"
npEvent.Data[event.Status] = event.StorageRunning
}
ac.appBus.Publish(npEvent)
@ -109,9 +112,9 @@ func (ac *applicationClient) CreateTaggedPeer(name, password, tag string) {
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})}
// DeletePeer messages the service to delete a peer
func (ac *applicationClient) DeletePeer(onion string, password string) {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.DeletePeer, map[event.Field]string{event.Identity: onion, event.Password: password})}
ac.bridge.Write(&message)
}
@ -128,6 +131,7 @@ func (ac *applicationClient) handleDeletedPeer(onion string) {
ac.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
ac.applicationCore.DeletePeer(onion)
ac.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
}
func (ac *applicationClient) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
@ -146,6 +150,11 @@ func (ac *applicationClient) QueryACNStatus() {
ac.bridge.Write(&message)
}
func (ac *applicationClient) QueryACNVersion() {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNVersion, map[event.Field]string{})}
ac.bridge.Write(&message)
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (ac *applicationClient) ShutdownPeer(onion string) {
ac.acmutex.Lock()

View File

@ -6,7 +6,7 @@ import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/storage"
"cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"path"
@ -52,7 +52,8 @@ func (as *applicationService) handleEvent(ev *event.Event) {
as.createPeer(profileName, password, tag)
case event.DeletePeer:
onion := ev.Data[event.Identity]
as.deletePeer(onion)
password := ev.Data[event.Password]
as.deletePeer(onion, password)
message := event.IPCMessage{Dest: DestApp, Message: *ev}
as.bridge.Write(&message)
@ -67,6 +68,7 @@ func (as *applicationService) handleEvent(ev *event.Event) {
for _, storage := range as.storage {
peerMsg := *storage.GetNewPeerMessage()
peerMsg.Data[event.Status] = event.StorageRunning
peerMsg.Data[event.Created] = event.False
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
as.bridge.Write(&message)
}
@ -84,6 +86,9 @@ func (as *applicationService) handleEvent(ev *event.Event) {
case event.GetACNStatus:
prog, status := as.acn.GetBootstrapStatus()
as.getACNStatusHandler()(prog, status)
case event.GetACNVersion:
version := as.acn.GetVersion()
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNVersion, event.Data, version)})
case event.ShutdownPeer:
onion := ev.Data[event.Identity]
as.ShutdownPeer(onion)
@ -116,6 +121,7 @@ func (as *applicationService) createPeer(name, password, tag string) {
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)
@ -135,6 +141,7 @@ func (as *applicationService) loadProfiles(password string) {
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)
@ -149,30 +156,41 @@ func (as *applicationService) loadProfiles(password string) {
func (as *applicationService) getACNStatusHandler() func(int, string) {
return func(progress int, status string) {
progStr := strconv.Itoa(progress)
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status)})
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.Progreess, progStr, event.Status, status))
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
}
}
}
func (as *applicationService) deletePeer(onion string) {
func (as *applicationService) deletePeer(onion, password string) {
as.asmutex.Lock()
defer as.asmutex.Unlock()
as.appletPlugins.ShutdownPeer(onion)
as.plugins.Delete(onion)
if as.storage[onion].CheckPassword(password) {
as.appletPlugins.ShutdownPeer(onion)
as.plugins.Delete(onion)
as.engines[onion].Shutdown()
delete(as.engines, onion)
as.engines[onion].Shutdown()
delete(as.engines, onion)
as.storage[onion].Shutdown()
as.storage[onion].Delete()
delete(as.storage, onion)
as.storage[onion].Shutdown()
as.storage[onion].Delete()
delete(as.storage, onion)
as.applicationCore.DeletePeer(onion)
as.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
as.applicationCore.DeletePeer(onion)
log.Debugf("Delete peer for %v Done\n", onion)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.PeerDeleted, event.Identity, onion)}
as.bridge.Write(&message)
return
}
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion)}
as.bridge.Write(&message)
}
func (as *applicationService) ShutdownPeer(onion string) {

View File

@ -69,7 +69,7 @@ func (ap *appletPeers) ListPeers() map[string]string {
ap.peerLock.Lock()
defer ap.peerLock.Unlock()
for k, p := range ap.peers {
keys[k] = p.GetName()
keys[k] = p.GetOnion()
}
return keys
}
@ -113,7 +113,7 @@ func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event
pluginsinf, _ := ap.plugins.Load(peerid)
peerPlugins := pluginsinf.([]plugins.Plugin)
newp := plugins.Get(id, bus, acn)
newp := plugins.Get(id, bus, acn, peerid)
newp.Start()
peerPlugins = append(peerPlugins, newp)
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)

View File

@ -70,7 +70,7 @@ func main() {
timeout := 1 * time.Second
timeElapsed := 0 * time.Second
for {
err := botPeer.SendMessageToGroup(groupID, timeout.String())
_, 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)

View File

@ -3,6 +3,7 @@ package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/log"
"sync"
"time"
)
@ -30,44 +31,55 @@ 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) Plugin {
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool), connections: sync.Map{}, networkUp: false}
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() {
go cr.run()
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]]
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
peer := e.Data[event.RemotePeer]
cr.handleEvent(peer, state, peerConn)
case event.ServerStateChange:
state := connections.ConnectionStateToType[e.Data[event.ConnectionState]]
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
server := e.Data[event.GroupServer]
cr.handleEvent(server, state, serverConn)
case event.ACNStatus:
prog := e.Data[event.Progreess]
if prog == "100" && cr.networkUp == false {
prog := e.Data[event.Progress]
if prog == "100" && !cr.networkUp {
cr.networkUp = true
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
@ -76,6 +88,9 @@ func (cr *contactRetry) run() {
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" {
@ -84,32 +99,40 @@ func (cr *contactRetry) run() {
}
case <-time.After(tickTime):
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}))
}
}
}
}
return true
})
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: 1, ticks: 0, ctype: ctype}
p := &contact{id: id, state: connections.DISCONNECTED, backoff: 0, ticks: 0, ctype: ctype}
cr.connections.Store(id, p)
return
}
@ -118,7 +141,9 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
p := pinf.(*contact)
if state == connections.DISCONNECTED || state == connections.FAILED || state == connections.KILLED {
p.state = connections.DISCONNECTED
if p.backoff < maxBakoff {
if p.backoff == 0 {
p.backoff = 1
} else if p.backoff < maxBakoff {
p.backoff *= 2
}
p.ticks = 0
@ -126,10 +151,11 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
p.state = state
} else if state == connections.AUTHENTICATED {
p.state = state
p.backoff = 1
p.backoff = 0
}
}
func (cr *contactRetry) Shutdown() {
cr.breakChan <- true
}

View File

@ -10,12 +10,18 @@ import (
"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 []string
onionsToCheck sync.Map // onion:string => true:bool
breakChan chan bool
running bool
offline bool
@ -34,12 +40,15 @@ func (nc *networkCheck) Start() {
func (nc *networkCheck) run() {
nc.running = true
nc.offline = true
nc.bus.Subscribe(event.ProtocolEngineStartListen, nc.queue)
nc.bus.Subscribe(event.NewMessageFromPeer, nc.queue)
nc.bus.Subscribe(event.PeerAcknowledgement, nc.queue)
nc.bus.Subscribe(event.EncryptedGroupMessage, nc.queue)
nc.bus.Subscribe(event.PeerStateChange, nc.queue)
nc.bus.Subscribe(event.ServerStateChange, nc.queue)
nc.bus.Subscribe(event.NewGetValMessageFromPeer, nc.queue)
nc.bus.Subscribe(event.NewRetValMessageFromPeer, nc.queue)
var lastMessageReceived time.Time
for {
select {
@ -52,18 +61,26 @@ func (nc *networkCheck) run() {
// and then we will wait a minute and check the connection for the first time (the onion should be up)
// under normal operating circumstances
case event.ProtocolEngineStartListen:
log.Debugf("initiating connection check for %v", e.Data[event.Onion])
nc.onionsToCheck = append(nc.onionsToCheck, e.Data[event.Onion])
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 nc.offline && (connectionState == connections.ConnectionStateName[connections.AUTHENTICATED] || connectionState == connections.ConnectionStateName[connections.CONNECTED]) {
if connectionState == connections.ConnectionStateName[connections.AUTHENTICATED] || connectionState == connections.ConnectionStateName[connections.CONNECTED] {
lastMessageReceived = time.Now()
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: "Success"}))
nc.offline = false
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:
@ -74,17 +91,15 @@ func (nc *networkCheck) run() {
lastMessageReceived = time.Now()
nc.offlineLock.Lock()
if nc.offline {
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: "Success"}))
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.Now().Sub(lastMessageReceived) > time.Minute {
for _, onion := range nc.onionsToCheck {
go nc.checkConnection(onion)
}
if time.Since(lastMessageReceived) > time.Minute {
nc.selfTest()
}
}
}
@ -98,8 +113,20 @@ func (nc *networkCheck) Shutdown() {
}
}
func (nc *networkCheck) selfTest() {
nc.onionsToCheck.Range(func(key, val interface{}) bool {
go nc.checkConnection(key.(string))
return true
})
}
//
func (nc *networkCheck) checkConnection(onion string) {
prog, _ := nc.acn.GetBootstrapStatus()
if prog != 100 {
return
}
// we want to definitively time these actions out faster than tor will, because these onions should definitely be
// online
ClientTimeout := TimeoutPolicy(time.Second * 60)
@ -114,13 +141,13 @@ func (nc *networkCheck) checkConnection(onion string) {
defer nc.offlineLock.Unlock()
// regardless of the outcome we want to report a status to let anyone who might care know that we did do a check
if err != nil {
log.Debugf("publishing network error for %v", onion)
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: err.Error(), event.Status: "Error"}))
nc.offline = false
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: "Success"}))
nc.offline = true
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: "", event.Status: NetworkCheckSuccess}))
nc.offline = false
}
}
@ -138,9 +165,9 @@ func (tp *TimeoutPolicy) ExecuteAction(action func() error) error {
c <- action()
}()
tick := time.Tick(time.Duration(*tp))
tick := time.NewTicker(time.Duration(*tp))
select {
case <-tick:
case <-tick.C:
return fmt.Errorf("ActionTimedOutError")
case err := <-c:
return err

View File

@ -21,10 +21,10 @@ type Plugin interface {
}
// Get is a plugin factory for the requested plugin
func Get(id PluginID, bus event.Manager, acn connectivity.ACN) Plugin {
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) Plugin {
switch id {
case CONNECTIONRETRY:
return NewConnectionRetry(bus)
return NewConnectionRetry(bus, onion)
case NETWORKCHECK:
return NewNetworkCheck(bus, acn)
}

View File

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

View File

@ -169,10 +169,7 @@ func (pb *pipeBridge) threeShakeClient() bool {
if string(resp) == synack {
stop <- true
err := pb.writeString([]byte(ack))
if err != nil {
return false
}
return true
return err == nil
}
}
}

View File

@ -19,6 +19,11 @@ const (
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
RetryPeerRequest = Type("RetryPeerRequest")
// RetryServerRequest
// Asks CwtchPeer to retry a server connection...
// GroupServer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
RetryServerRequest = Type("RetryServerRequest")
// RemotePeer
// Authorization(model.peer.Auth_...)
SetPeerAuthorization = Type("UpdatePeerAuthorization")
@ -30,6 +35,9 @@ const (
// GroupServer
JoinServer = Type("JoinServer")
// attributes GroupServer - the onion of the server to leave
LeaveServer = Type("LeaveServer")
ProtocolEngineStartListen = Type("ProtocolEngineStartListen")
ProtocolEngineStopped = Type("ProtocolEngineStopped")
@ -43,8 +51,13 @@ const (
// 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")
@ -53,6 +66,10 @@ const (
//TimestampReceived, TimestampSent, Data(Message), GroupID, Signature, PreviousSignature, RemotePeer
NewMessageFromGroup = Type("NewMessageFromGroup")
// Sent if a Group Key is detected as being used outside of expected parameters (e.g. with tampered signatures)
// GroupID: The ID of the Group that is presumed compromised
GroupCompromised = Type("GroupCompromised")
// an error was encountered trying to send a particular Message to a group
// attributes:
// GroupServer: The server the Message was sent to
@ -81,6 +98,24 @@ const (
// 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
@ -153,12 +188,14 @@ const (
// ProfileName, Password, Data(tag)
CreatePeer = Type("CreatePeer")
// service -> client: Identity(localId), Password, [Status(new/default=blank || from reload='running')]
// app -> Key, Salt
// 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)
PeerDeleted = Type("PeerDeleted")
// Identity(onion), Data(pluginID)
AddPeerPlugin = Type("AddPeerPlugin")
@ -186,17 +223,25 @@ const (
// Error(err)
AppError = Type("AppError")
GetACNStatus = Type("GetACNStatus")
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")
@ -233,6 +278,8 @@ const (
Password = Field("Password")
NewPassword = Field("NewPassword")
Created = Field("Created")
ConnectionState = Field("ConnectionState")
Key = Field("Key")
@ -245,10 +292,17 @@ const (
Error = Field("Error")
Progreess = Field("Progress")
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")
@ -256,11 +310,14 @@ const (
// Indicate whether an event was triggered by a user import
Imported = Field("Imported")
Source = Field("Source")
)
// Defining Common errors
const (
AppErrLoaded0 = "Loaded 0 profiles"
PasswordMatchError = "Password did not match"
)
// Values to be suplied in event.NewPeer for Status
@ -292,3 +349,9 @@ const (
SaveHistoryConfirmed = "SaveHistory"
DeleteHistoryConfirmed = "DeleteHistoryConfirmed"
)
// Bool strings
const (
True = "true"
False = "false"
)

View File

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

View File

@ -2,12 +2,19 @@ package event
import (
"crypto/rand"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"math"
"math/big"
"os"
"runtime"
"strings"
"sync"
)
// Event is a structure which binds a given set of data to an Type
// 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
@ -47,10 +54,11 @@ func NewEventList(eventType Type, args ...interface{}) Event {
// 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 Event
events chan []byte
mapMutex sync.Mutex
internal chan bool
closed bool
trace bool
}
// Manager is an interface for an event bus
@ -72,9 +80,12 @@ func NewEventManager() Manager {
// Initialize sets up the Manager.
func (em *manager) initialize() {
em.subscribers = make(map[Type][]Queue)
em.events = make(chan Event)
em.events = make(chan []byte)
em.internal = make(chan bool)
em.closed = false
_, em.trace = os.LookupEnv("CWTCH_EVENT_SOURCE")
go em.eventBus()
}
@ -88,8 +99,27 @@ func (em *manager) Subscribe(eventType Type, queue Queue) {
// Publish takes an Event and sends it to the internal eventBus where it is distributed to all Subscribers
func (em *manager) Publish(event Event) {
if event.EventType != "" && em.closed != true {
em.events <- 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
}
}
@ -101,13 +131,21 @@ func (em *manager) PublishLocal(event Event) {
// eventBus is an internal function that is used to distribute events to all subscribers
func (em *manager) eventBus() {
for {
event := <-em.events
eventJSON := <-em.events
// In the case on an empty event. Teardown the Queue
if event.EventType == "" {
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]
@ -115,7 +153,10 @@ func (em *manager) eventBus() {
// Send the event to any subscribers to that event type
for _, subscriber := range subscribers {
subscriber.Publish(event)
// Deep Copy for Each Subscriber
var eventCopy Event
json.Unmarshal(eventJSON, &eventCopy)
subscriber.Publish(eventCopy)
}
}
@ -125,7 +166,7 @@ func (em *manager) eventBus() {
// Shutdown triggers, and waits for, the internal eventBus goroutine to finish
func (em *manager) Shutdown() {
em.events <- Event{}
em.events <- []byte{}
em.closed = true
// wait for eventBus to finish
<-em.internal

10
go.mod
View File

@ -3,13 +3,15 @@ module cwtch.im/cwtch
go 1.14
require (
cwtch.im/tapir v0.2.0
git.openprivacy.ca/openprivacy/connectivity v1.3.0
git.openprivacy.ca/openprivacy/log v1.0.1
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/net v0.0.0-20201010224723-4f7140c49acb
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
)

90
go.sum
View File

@ -1,27 +1,46 @@
cwtch.im/tapir v0.2.0 h1:7MkoR5+uEuPW34/O0GZRidnIjq/01Cfm8nl5IRuqpGc=
cwtch.im/tapir v0.2.0/go.mod h1:xzzZ28adyUXNkYL1YodcHsAiTt3IJ8Loc29YVn9mIEQ=
git.openprivacy.ca/openprivacy/bine v0.0.3 h1:PSHUmNqaW7BZUX8n2eTDeNbjsuRe+t5Ae0Og+P+jDM0=
git.openprivacy.ca/openprivacy/bine v0.0.3/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/connectivity v1.2.0 h1:dbZ5CRl11vg3BNHdzRKSlDP8OUtDB+mf6FkxMVf73qw=
git.openprivacy.ca/openprivacy/connectivity v1.2.0/go.mod h1:B7vzuVmChJtSKoh0ezph5vu6DQ0gIk0zHUNG6IgXCcA=
git.openprivacy.ca/openprivacy/connectivity v1.3.0 h1:e2EeV6CaMNwOb+PzAjF0hGCeOqAPagRaDL4en5ITf7U=
git.openprivacy.ca/openprivacy/connectivity v1.3.0/go.mod h1:s0/QhONuUqJQfYTAgUlu+ya7G3Ov6bKgpT5QkOhVxDI=
git.openprivacy.ca/openprivacy/log v1.0.0 h1:Rvqm1weUdR4AOnJ79b1upHCc9vC/QF1rhSD2Um7sr1Y=
git.openprivacy.ca/openprivacy/log v1.0.0/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
git.openprivacy.ca/openprivacy/log v1.0.1 h1:NWV5oBTatvlSzUE6wtB+UQCulgyMOtm4BXGd34evMys=
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=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca h1:Q2r7AxHdJwWfLtBZwvW621M3sPqxPc6ITv2j1FGsYpw=
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
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/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -31,54 +50,61 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY=
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-20190911185100-cd5d95a43a6e/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 h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
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.0.0-20200625195345-7480c7b4547d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
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-20191204190536-9bdfabe68543/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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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,39 +1,48 @@
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"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"io"
"strings"
"sync"
"time"
)
// CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date
const CurrentGroupVersion = 2
const CurrentGroupVersion = 3
// GroupInvitePrefix identifies a particular string as being a serialized group invite.
const GroupInvitePrefix = "torv3"
// Group defines and encapsulates Cwtch's conception of group chat. Which are sessions
// tied to a server under a given group key. Each group has a set of Messages.
type Group struct {
// GroupID is now derived from the GroupKey and the GroupServer
GroupID string
SignedGroupID []byte
GroupKey [32]byte
GroupServer string
Timeline Timeline `json:"-"`
Accepted bool
Owner string
IsCompromised bool
InitialMessage []byte
Attributes map[string]string
lock sync.Mutex
LocalID string
State string `json:"-"`
unacknowledgedMessages []Message
UnacknowledgedMessages []Message
Version int
}
@ -42,9 +51,9 @@ func NewGroup(server string) (*Group, error) {
group := new(Group)
group.Version = CurrentGroupVersion
group.LocalID = GenerateRandomID()
if tor.IsValidHostname(server) == false {
return nil, errors.New("Server is not a valid v3 onion")
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
@ -62,48 +71,49 @@ func NewGroup(server string) (*Group, error) {
return nil, err
}
copy(group.GroupKey[:], groupKey[:])
group.Owner = "self"
// 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
return group, nil
}
// SignGroup adds a signature to the group.
func (g *Group) SignGroup(signature []byte) {
g.SignedGroupID = signature
copy(g.Timeline.SignedGroupID[:], g.SignedGroupID)
// 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)
}
// Compromised should be called if we detect a a groupkey leak.
// 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
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(initialMessage []byte) ([]byte, error) {
if g.SignedGroupID == nil {
return nil, errors.New("group isn't signed")
}
g.InitialMessage = initialMessage[:]
func (g *Group) Invite() (string, error) {
gci := &groups.GroupInvite{
GroupName: g.GroupID,
SharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
SignedGroupID: g.SignedGroupID[:],
InitialMessage: initialMessage[:],
GroupID: g.GroupID,
GroupName: g.Attributes[attr.GetLocalScope("name")],
SharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
}
invite, err := json.Marshal(gci)
return invite, err
serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite))
return serializedInvite, err
}
// AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
@ -119,7 +129,7 @@ func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte
PreviousMessageSig: message.PreviousMessageSig,
ReceivedByServer: false,
}
g.unacknowledgedMessages = append(g.unacknowledgedMessages, timelineMessage)
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages, timelineMessage)
return timelineMessage
}
@ -130,10 +140,10 @@ func (g *Group) ErrorSentMessage(sig []byte, error string) bool {
var message *Message
// Delete the message from the unack'd buffer if it exists
for i, unAckedMessage := range g.unacknowledgedMessages {
for i, unAckedMessage := range g.UnacknowledgedMessages {
if compareSignatures(unAckedMessage.Signature, sig) {
message = &unAckedMessage
g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...)
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
message.Error = error
g.Timeline.Insert(message)
@ -150,9 +160,9 @@ func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*
defer g.lock.Unlock()
// Delete the message from the unack'd buffer if it exists
for i, unAckedMessage := range g.unacknowledgedMessages {
for i, unAckedMessage := range g.UnacknowledgedMessages {
if compareSignatures(unAckedMessage.Signature, sig) {
g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...)
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
break
}
}
@ -166,6 +176,7 @@ func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*
PreviousMessageSig: message.PreviousMessageSig,
ReceivedByServer: true,
Error: "",
Acknowledged: true,
}
seen := g.Timeline.Insert(timelineMessage)
@ -176,7 +187,7 @@ func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*
func (g *Group) GetTimeline() (timeline []Message) {
g.lock.Lock()
defer g.lock.Unlock()
return append(g.Timeline.GetMessages(), g.unacknowledgedMessages...)
return append(g.Timeline.GetMessages(), g.UnacknowledgedMessages...)
}
//EncryptMessage takes a message and encrypts the message under the group key.
@ -226,3 +237,41 @@ func (g *Group) GetAttribute(name string) (value string, exists bool) {
value, exists = g.Attributes[name]
return
}
// ValidateInvite takes in a serialized invite and returns the invite structure if it is cryptographically valid
// and an error if it is not
func ValidateInvite(invite string) (*groups.GroupInvite, error) {
// We prefix invites for groups with torv3
if strings.HasPrefix(invite, GroupInvitePrefix) {
data, err := base64.StdEncoding.DecodeString(invite[len(GroupInvitePrefix):])
if err == nil {
// First attempt to unmarshal the json...
var gci groups.GroupInvite
err := json.Unmarshal(data, &gci)
if err == nil {
// Validate the Invite by first checking that the server is a valid v3 onion
if !tor.IsValidHostname(gci.ServerHost) {
return nil, errors.New("server is not a valid v3 onion")
}
// Validate the length of the shared key...
if len(gci.SharedKey) != 32 {
return nil, errors.New("key length is not 32 bytes")
}
// Derive the servers public key (we can ignore the error checking here because it's already been
// done by IsValidHostname, and check that we derive the same groupID...
derivedGroupID := deriveGroupID(gci.SharedKey, gci.ServerHost)
if derivedGroupID != gci.GroupID {
return nil, errors.New("group id is invalid")
}
// Replace the original with the derived, this should be a no-op at this point but defense in depth...
gci.GroupID = derivedGroupID
return &gci, nil
}
}
}
return nil, errors.New("invite has invalid structure")
}

View File

@ -1,7 +1,10 @@
package model
import (
"crypto/sha256"
"cwtch.im/cwtch/protocol/groups"
"strings"
"sync"
"testing"
"time"
)
@ -16,6 +19,23 @@ func TestGroup(t *testing.T) {
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!" {
@ -36,3 +56,60 @@ func TestGroupErr(t *testing.T) {
t.Errorf("Group Setup Should Have Failed")
}
}
// Test various group invite validation failures...
func TestGroupValidation(t *testing.T) {
group := &Group{
GroupID: "",
GroupKey: [32]byte{},
GroupServer: "",
Timeline: Timeline{},
Accepted: false,
IsCompromised: false,
Attributes: nil,
lock: sync.Mutex{},
LocalID: "",
State: "",
UnacknowledgedMessages: nil,
Version: 0,
}
invite, _ := group.Invite()
_, err := ValidateInvite(invite)
if err == nil {
t.Fatalf("Group with empty group id should have been an error")
}
t.Logf("Error: %v", err)
// Generate a valid group but replace the group server...
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
invite, _ = group.Invite()
_, err = ValidateInvite(invite)
if err == nil {
t.Fatalf("Group with empty group id should have been an error")
}
t.Logf("Error: %v", err)
// Generate a valid group but replace the group key...
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group.GroupKey = sha256.Sum256([]byte{})
invite, _ = group.Invite()
_, err = ValidateInvite(invite)
if err == nil {
t.Fatalf("Group with different group key should have errored")
}
t.Logf("Error: %v", err)
// mangle the invite
_, err = ValidateInvite(strings.ReplaceAll(invite, GroupInvitePrefix, ""))
if err == nil {
t.Fatalf("Group with different group key should have errored")
}
t.Logf("Error: %v", err)
}

View File

@ -2,10 +2,10 @@ package model
import (
"crypto/ed25519"
"cwtch.im/tapir/primitives"
"encoding/base32"
"encoding/json"
"errors"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"strings"
)
@ -13,6 +13,10 @@ import (
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
@ -80,7 +84,7 @@ func DeserializeAndVerify(bundle []byte) (*KeyBundle, error) {
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 {
if ed25519.Verify(decodedPub[:32], keyBundle.Serialize(), signature) { // == true
return keyBundle, nil
}
}

View File

@ -1,7 +1,7 @@
package model
import (
"cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"testing"
)

View File

@ -26,6 +26,8 @@ type Message struct {
ReceivedByServer bool // messages sent to a server
Acknowledged bool // peer to peer
Error string `json:",omitempty"`
// Application specific flags, useful for storing small amounts of metadata
Flags uint64
}
// MessageBaseSize is a rough estimate of the base number of bytes the struct uses before strings are populated
@ -103,7 +105,6 @@ func (t *Timeline) Less(i, j int) bool {
}
// Sort sorts the timeline in a canonical order.
// TODO: There is almost definitely a more efficient way of doing things that involve not calling this method on every timeline load.
func (t *Timeline) Sort() {
t.lock.Lock()
defer t.lock.Unlock()

View File

@ -16,7 +16,7 @@ func TestMessagePadding(t *testing.T) {
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(string(invite), alice.Onion)
sarah.ProcessInvite(invite)
group := alice.GetGroup(gid)
@ -47,9 +47,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("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(string(invite), alice.Onion)
sarah.ProcessInvite(invite)
group := alice.GetGroup(gid)

View File

@ -2,11 +2,13 @@ package model
import (
"crypto/rand"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/groups"
"encoding/base32"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519"
"io"
@ -40,7 +42,7 @@ type PublicProfile struct {
LocalID string // used by storage engine
State string `json:"-"`
lock sync.Mutex
unacknowledgedMessages map[string]Message
UnacknowledgedMessages map[string]int
}
// Profile encapsulates all the attributes necessary to be a Cwtch Peer.
@ -66,7 +68,7 @@ func (p *PublicProfile) init() {
if p.Attributes == nil {
p.Attributes = make(map[string]string)
}
p.unacknowledgedMessages = make(map[string]Message)
p.UnacknowledgedMessages = make(map[string]int)
p.LocalID = GenerateRandomID()
}
@ -111,13 +113,33 @@ func GenerateNewProfile(name string) *Profile {
func (p *Profile) AddContact(onion string, profile *PublicProfile) {
p.lock.Lock()
profile.init()
// TODO: More Robust V3 Onion Handling
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32])
p.Contacts[onion] = profile
// 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.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()
@ -150,10 +172,11 @@ func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt strin
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
message := &Message{PeerID: p.Onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: false}
if contact.unacknowledgedMessages == nil {
contact.unacknowledgedMessages = make(map[string]Message)
if contact.UnacknowledgedMessages == nil {
contact.UnacknowledgedMessages = make(map[string]int)
}
contact.unacknowledgedMessages[eventID] = *message
contact.Timeline.Insert(message)
contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1
return message
}
return nil
@ -176,45 +199,47 @@ func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, s
}
// ErrorSentMessageToPeer sets a sent message's error message and removes it from the unacknowledged list
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) {
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 {
message, ok := contact.unacknowledgedMessages[eventID]
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
message.Error = error
contact.Timeline.Insert(&message) // TODO: do we want a non timeline.Insert way to handle errors
delete(contact.unacknowledgedMessages, eventID)
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) {
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) int {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
message, ok := contact.unacknowledgedMessages[eventID]
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
message.Acknowledged = true
contact.Timeline.Insert(&message)
delete(contact.unacknowledgedMessages, eventID)
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(groupServer string, signature string, error string) {
for _, group := range p.Groups {
if group.GroupServer == groupServer {
if group.ErrorSentMessage([]byte(signature), error) {
break
}
}
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)
}
}
@ -297,20 +322,34 @@ func (p *Profile) GetContact(onion string) (*PublicProfile, bool) {
return contact, ok
}
// 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 {
// 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 {
group := p.GetGroup(groupID)
if group == nil {
return false
}
// We use our group id, a known reference server and the ciphertext of the message.
m := groupID + group.GroupServer + string(ciphertext)
// If the message is ostensibly from us then we check it against our public key...
if onion == p.Onion {
m := groupID + group.GroupServer + string(ciphertext)
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
}
m := groupID + group.GroupServer + string(ciphertext)
// Otherwise we derive the public key from the sender and check it against that.
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
if err == nil && len(decodedPub) >= 32 {
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
@ -326,22 +365,13 @@ 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 []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) {
func (p *Profile) StartGroup(server string) (groupID string, invite string, err error) {
group, err := NewGroup(server)
if err != nil {
return "", nil, err
return "", "", err
}
groupID = group.GroupID
group.Owner = p.Onion
signedGroupID := p.SignMessage(groupID + server)
group.SignGroup(signedGroupID)
invite, err = group.Invite(initialMessage)
invite, err = group.Invite()
p.lock.Lock()
defer p.lock.Unlock()
p.Groups[group.GroupID] = group
@ -356,34 +386,35 @@ func (p *Profile) GetGroup(groupID string) (g *Group) {
return
}
// ProcessInvite adds a new group invite to the profile. returns the new group ID
func (p *Profile) ProcessInvite(invite string, peerHostname string) (string, error) {
var gci groups.GroupInvite
err := json.Unmarshal([]byte(invite), &gci)
// 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.GroupName
group.GroupID = gci.GroupID
group.LocalID = GenerateRandomID()
group.SignedGroupID = gci.SignedGroupID
copy(group.GroupKey[:], gci.SharedKey[:])
group.GroupServer = gci.ServerHost
group.InitialMessage = []byte(gci.InitialMessage)
group.Accepted = false
group.Owner = peerHostname
group.Attributes = make(map[string]string)
group.Attributes[attr.GetLocalScope("name")] = gci.GroupName
p.AddGroup(group)
return group.GroupID, nil
return gci.GroupID, nil
}
return "", err
}
// AddGroup is a convenience method for adding a group to a profile.
func (p *Profile) AddGroup(group *Group) {
p.lock.Lock()
defer p.lock.Unlock()
_, exists := p.Groups[group.GroupID]
if !exists {
p.lock.Lock()
defer p.lock.Unlock()
p.Groups[group.GroupID] = group
}
}
@ -394,7 +425,7 @@ func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool,
for _, group := range p.Groups {
success, dgm := group.DecryptMessage(ciphertext)
if success {
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, dgm.Text, int32(dgm.Timestamp), ciphertext, signature)
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, 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)
@ -434,21 +465,26 @@ func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte,
if group != nil {
timestamp := time.Now().Unix()
// Select the latest message from the timeline as a reference point.
var prevSig []byte
if len(group.Timeline.Messages) > 0 {
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
} else {
prevSig = group.SignedGroupID
prevSig = []byte(group.GroupID)
}
lenPadding := MaxGroupMessageLength - len(message)
padding := make([]byte, lenPadding)
getRandomness(&padding)
hexGroupID, err := hex.DecodeString(group.GroupID)
if err != nil {
return nil, nil, err
}
dm := &groups.DecryptedGroupMessage{
Onion: p.Onion,
Text: message,
SignedGroupID: group.SignedGroupID[:],
SignedGroupID: hexGroupID,
Timestamp: uint64(timestamp),
PreviousMessageSig: prevSig,
Padding: padding[:],

View File

@ -62,9 +62,11 @@ 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(string(invite), alice.Onion)
sarah.ProcessInvite(invite)
group := alice.GetGroup(gid)
if len(sarah.Groups) == 1 {
if sarah.GetGroup(group.GroupID).Accepted {
@ -85,8 +87,11 @@ func TestProfileGroup(t *testing.T) {
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
gid, invite, _ := alice.StartGroupWithMessage("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", []byte("Hello World"))
sarah.ProcessInvite(string(invite), alice.Onion)
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)
if len(sarah.GetGroups()) != 1 {
t.Errorf("sarah should only be in 1 group instead: %v", sarah.GetGroups())
}
@ -97,17 +102,11 @@ func TestProfileGroup(t *testing.T) {
alice.AttemptDecryption(c, s1)
gid2, invite2, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(string(invite2), alice.Onion)
sarah.ProcessInvite(invite2)
group2 := alice.GetGroup(gid2)
c2, s2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID)
alice.AttemptDecryption(c2, s2)
sarahGroup := sarah.GetGroup(group.GroupID)
im := sarahGroup.GetInitialMessage()
if string(im) != "Hello World" {
t.Errorf("Initial Message was not stored properly: %v", im)
}
_, _, err := sarah.EncryptMessageToGroup(string(make([]byte, MaxGroupMessageLength*2)), group2.GroupID)
if err == nil {
t.Errorf("Overly long message should have returned an error")
@ -115,7 +114,10 @@ func TestProfileGroup(t *testing.T) {
bob := GenerateNewProfile("bob")
bob.AddContact(alice.Onion, &alice.PublicProfile)
bob.ProcessInvite(string(invite2), alice.Onion)
// 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)
c3, s3, err := bob.EncryptMessageToGroup("Bobs Message", group2.GroupID)
if err == nil {
ok, _, message, _ := alice.AttemptDecryption(c3, s3)

View File

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

View File

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

View File

@ -4,12 +4,12 @@ import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/tapir"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"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"
@ -27,9 +27,6 @@ type engine struct {
identity primitives.Identity
acn connectivity.ACN
// Engine State
started bool
// Authorization list of contacts to authorization status
authorizations sync.Map // string(onion) => model.Authorization
@ -53,6 +50,8 @@ type engine struct {
// Engine (ProtocolEngine) encapsulates the logic necessary to make and receive Cwtch connections.
// Note: ProtocolEngine doesn't have access to any information necessary to encrypt or decrypt GroupMessages
// Protocol Engine *can* associate Group Identifiers with Group Servers, although we don't currently make use of this fact
// other than to route errors back to the UI.
type Engine interface {
ACN() connectivity.ACN
EventManager() event.Manager
@ -80,6 +79,7 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
engine.eventManager.Subscribe(event.RetryPeerRequest, engine.queue)
engine.eventManager.Subscribe(event.InvitePeerToGroup, engine.queue)
engine.eventManager.Subscribe(event.JoinServer, engine.queue)
engine.eventManager.Subscribe(event.LeaveServer, engine.queue)
engine.eventManager.Subscribe(event.SendMessageToGroup, engine.queue)
engine.eventManager.Subscribe(event.SendMessageToPeer, engine.queue)
engine.eventManager.Subscribe(event.SendGetValMessageToPeer, engine.queue)
@ -126,7 +126,14 @@ func (e *engine) eventHandler() {
case event.InvitePeerToGroup:
e.sendMessageToPeer(ev.EventID, ev.Data[event.RemotePeer], event.ContextInvite, []byte(ev.Data[event.GroupInvite]))
case event.JoinServer:
e.peerWithTokenServer(ev.Data[event.GroupServer], ev.Data[event.ServerTokenOnion], ev.Data[event.ServerTokenY])
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.
@ -135,10 +142,9 @@ func (e *engine) eventHandler() {
case event.DeleteGroup:
// TODO: There isn't a way here to determine if other Groups are using a server connection...
case event.SendMessageToGroup:
err := e.sendMessageToGroup(ev.Data[event.GroupServer], []byte(ev.Data[event.Ciphertext]), []byte(ev.Data[event.Signature]))
if err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupServer: ev.Data[event.GroupServer], event.EventID: ev.EventID, event.Error: err.Error()}))
}
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]
@ -167,8 +173,10 @@ func (e *engine) eventHandler() {
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()
@ -187,18 +195,22 @@ func (e *engine) isBlocked(onion string) bool {
return authorization.(model.Authorization) == model.AuthBlocked
}
func (e *engine) isApproved(onion string) bool {
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
}
return authorization.(model.Authorization) == model.AuthApproved
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.IsApproved = e.isApproved
peerAppTemplate.IsAllowed = e.isAllowed
peerAppTemplate.MessageHandler = e.handlePeerMessage
peerAppTemplate.OnAcknowledgement = e.ignoreOnShutdown2(e.peerAck)
peerAppTemplate.OnAuth = e.ignoreOnShutdown(e.peerAuthed)
@ -214,7 +226,6 @@ func (e *engine) listenFn() {
if !e.shuttingDown {
e.eventManager.Publish(event.NewEvent(event.ProtocolEngineStopped, map[event.Field]string{event.Identity: e.identity.Hostname(), event.Error: err.Error()}))
}
return
}
// Shutdown tears down the eventHandler goroutine
@ -253,10 +264,26 @@ func (e *engine) peerWithOnion(onion string) {
// peerWithTokenServer is the entry point for cwtchPeer - server relationships
// needs to be run in a goroutine as will block on Open.
func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, tokenServerY string) {
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()
@ -264,7 +291,7 @@ func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, toke
Y := ristretto255.NewElement()
Y.UnmarshalText([]byte(tokenServerY))
connected, err := ephemeralService.Connect(onion, NewTokenBoardClient(e.acn, Y, tokenServerOnion, e.receiveGroupMessage, e.serverSynced))
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)
@ -333,7 +360,6 @@ func (e *engine) serverConnected(onion string) {
}
func (e *engine) serverSynced(onion string) {
log.Debugf("SERVER SYNCED: %v", onion)
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: onion,
event.ConnectionState: ConnectionStateName[SYNCED],
@ -407,42 +433,48 @@ func (e *engine) deleteConnection(id string) {
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.Ciphertext: string(gm.Ciphertext), event.Signature: string(gm.Signature)}))
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(server string, ct []byte, sig []byte) error {
func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, sig []byte) {
es, ok := e.ephemeralServices.Load(server)
if !ok {
return fmt.Errorf("no service exists for group %v", 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 {
attempts := 0
for tokenApp.Post(ct, sig) == false {
// TODO This should eventually be wired back into the UI to allow it to error
tokenApp.MakePayment()
time.Sleep(time.Second * 5)
attempts++
if attempts == 5 {
return errors.New("failed to post to token board")
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()
}
return nil
// regardless we return....
return
}
return errors.New("failed type assertion conn.App != TokenBoardClientApp")
}
return err
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.ContextInvite {
e.eventManager.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: hostname, event.GroupInvite: string(message)}))
} else if context == event.ContextGetVal {
if context == event.ContextGetVal {
var getVal peerGetVal
err := json.Unmarshal(message, &getVal)
if err == nil {
@ -472,3 +504,12 @@ func (e *engine) handlePeerRetVal(hostname string, getValData, retValData []byte
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

@ -2,9 +2,9 @@ package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/openprivacy/log"
"sync"
)
@ -18,7 +18,7 @@ type PeerApp struct {
MessageHandler func(string, string, string, []byte)
RetValHandler func(string, []byte, []byte)
IsBlocked func(string) bool
IsApproved func(string) bool
IsAllowed func(string) bool
OnAcknowledgement func(string, string)
OnAuth func(string)
OnClose func(string)
@ -48,7 +48,7 @@ func (pa *PeerApp) NewInstance() tapir.Application {
newApp := new(PeerApp)
newApp.MessageHandler = pa.MessageHandler
newApp.IsBlocked = pa.IsBlocked
newApp.IsApproved = pa.IsApproved
newApp.IsAllowed = pa.IsAllowed
newApp.OnAcknowledgement = pa.OnAcknowledgement
newApp.OnAuth = pa.OnAuth
newApp.OnClose = pa.OnClose
@ -75,7 +75,8 @@ func (pa *PeerApp) Init(connection tapir.Connection) {
go pa.listen()
}
} else {
pa.OnClose(connection.Hostname())
// The auth protocol wasn't completed, we can safely shutdown the connection
connection.Close()
}
}
@ -101,7 +102,7 @@ func (pa *PeerApp) listen() {
pa.getValRequests.Delete(peerMessage.ID)
}
default:
if pa.IsApproved(pa.connection.Hostname()) {
if pa.IsAllowed(pa.connection.Hostname()) {
pa.MessageHandler(pa.connection.Hostname(), peerMessage.ID, peerMessage.Context, peerMessage.Data)
// Acknowledge the message

View File

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

View File

@ -2,20 +2,21 @@ package connections
import (
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"cwtch.im/tapir/primitives/privacypass"
"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, groupMessageHandler func(server string, gm *groups.EncryptedGroupMessage), serverSyncedHandler func(server string)) tapir.Application {
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()
@ -23,6 +24,8 @@ func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenSer
tba.tokenServiceOnion = tokenServiceOnion
tba.receiveGroupMessageHandler = groupMessageHandler
tba.serverSyncedHandler = serverSyncedHandler
tba.serverClosedHandler = serverClosedHandler
tba.lastKnownSignature = lastKnownSignature
return tba
}
@ -32,22 +35,27 @@ type TokenBoardClient struct {
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
tokenService *privacypass.TokenServer
tokenServiceOnion string
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
}
@ -59,6 +67,9 @@ func (ta *TokenBoardClient) Init(connection tapir.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()
@ -72,14 +83,15 @@ func (ta *TokenBoardClient) Listen() {
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
// TODO issue an error so the client is aware
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
}
@ -89,8 +101,8 @@ func (ta *TokenBoardClient) Listen() {
if message.NewMessage != nil {
ta.receiveGroupMessageHandler(ta.connection.Hostname(), &message.NewMessage.EGM)
} else {
// TODO: Send this error to the UI
log.Debugf("Server sent an unexpected NewMessage, closing the connection: %s", data)
ta.serverClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
@ -101,12 +113,21 @@ func (ta *TokenBoardClient) Listen() {
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 {
// TODO: Send this error to the UI
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection: %v", err)
ta.serverClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
@ -119,8 +140,7 @@ func (ta *TokenBoardClient) Listen() {
// Replay posts a Replay Message to the server.
func (ta *TokenBoardClient) Replay() {
// TODO - Allow configurable ranges
data, _ := json.Marshal(groups.Message{MessageType: groups.ReplayRequestMessage, ReplayRequest: &groups.ReplayRequest{LastCommit: []byte{}}})
data, _ := json.Marshal(groups.Message{MessageType: groups.ReplayRequestMessage, ReplayRequest: &groups.ReplayRequest{LastCommit: ta.lastKnownSignature}})
ta.connection.Send(data)
}
@ -130,25 +150,24 @@ func (ta *TokenBoardClient) PurchaseTokens() {
}
// Post sends a Post Request to the server
func (ta *TokenBoardClient) Post(ct []byte, sig []byte) bool {
func (ta *TokenBoardClient) Post(ct []byte, sig []byte) (bool, int) {
egm := groups.EncryptedGroupMessage{Ciphertext: ct, Signature: sig}
token, err := ta.NextToken(egm.ToBytes(), ta.connection.Hostname())
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
return true, numTokens
}
log.Debugf("No Valid Tokens: %v", err)
return false
return false, numTokens
}
// MakePayment uses the PoW based token protocol to obtain more tokens
func (ta *TokenBoardClient) MakePayment() {
func (ta *TokenBoardClient) MakePayment() error {
log.Debugf("Making a Payment %v", ta)
id, sk := primitives.InitializeEphemeralIdentity()
var client tapir.Service
client = new(tor.BaseOnionService)
client := new(tor.BaseOnionService)
client.Init(ta.acn, sk, &id)
tokenApplication := new(applications.TokenApplication)
@ -157,23 +176,34 @@ func (ta *TokenBoardClient) MakePayment() {
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
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, error) {
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{}, errors.New("No more tokens")
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...)), nil
return token.SpendToken(append(data, hostname...)), len(ta.tokens), nil
}

View File

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

View File

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

View File

@ -1,9 +1,9 @@
package groups
import (
"cwtch.im/tapir"
"cwtch.im/tapir/primitives/privacypass"
"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
@ -11,19 +11,23 @@ const CwtchServerSyncedCapability = tapir.Capability("CwtchServerSyncedCapabilit
// GroupInvite provides a structured type for communicating group information to peers
type GroupInvite struct {
GroupName string
SignedGroupID []byte
Timestamp uint64
SharedKey []byte
ServerHost string
InitialMessage []byte
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
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

View File

@ -1,16 +1,17 @@
package main
import (
"crypto/rand"
"cwtch.im/cwtch/model"
cwtchserver "cwtch.im/cwtch/server"
"cwtch.im/tapir/primitives"
"encoding/base64"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
mrand "math/rand"
"crypto/rand"
"os"
"os/signal"
"syscall"
"time"
)
@ -21,6 +22,7 @@ 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" {
@ -45,7 +47,7 @@ func main() {
// 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
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
@ -54,9 +56,10 @@ func main() {
panic(err)
}
os.MkdirAll("tordir/tor",0700)
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)
@ -66,20 +69,32 @@ func main() {
server := new(cwtchserver.Server)
log.Infoln("starting cwtch server...")
// TODO load params from .cwtch/server.conf or command line flag
// TODO: respond to HUP so t.Close is gracefully called
server.Setup(serverConfig)
// TODO create a random group for testing
group, _ := model.NewGroup(tor.GetTorV3Hostname(serverConfig.PublicKey))
group.SignGroup([]byte{})
invite, err := group.Invite([]byte{})
invite, err := group.Invite()
if err != nil {
panic(err)
}
fmt.Printf("%v", "torv3"+base64.StdEncoding.EncodeToString(invite))
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)
}
}

View File

@ -12,6 +12,7 @@ import (
type counter struct {
startTime time.Time
count uint64
total uint64
}
// Counter providers a threadsafe counter to use for storing long running counts
@ -25,7 +26,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}
c := &counter{startTime: time.Now(), count: 0, total: 0}
return c
}

View File

@ -2,8 +2,8 @@ package metrics
import (
"bufio"
"cwtch.im/tapir"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/openprivacy/log"
"github.com/struCoder/pidusage"
"os"
@ -18,15 +18,16 @@ const (
// Monitors is a package of metrics for a Cwtch Server including message count, CPU, Mem, and conns
type Monitors struct {
MessageCounter Counter
Messages MonitorHistory
CPU MonitorHistory
Memory MonitorHistory
ClientConns MonitorHistory
starttime time.Time
breakChannel chan bool
log bool
configDir string
MessageCounter Counter
TotalMessageCounter Counter
Messages MonitorHistory
CPU MonitorHistory
Memory MonitorHistory
ClientConns MonitorHistory
starttime time.Time
breakChannel chan bool
log bool
configDir string
}
// Start initializes a Monitors's monitors
@ -36,7 +37,16 @@ func (mp *Monitors) Start(ts tapir.Service, configDir string, log bool) {
mp.starttime = time.Now()
mp.breakChannel = make(chan bool)
mp.MessageCounter = NewCounter()
mp.Messages = NewMonitorHistory(Count, Cumulative, func() (c float64) { c = float64(mp.MessageCounter.Count()); mp.MessageCounter.Reset(); return })
// 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()

View File

@ -5,51 +5,55 @@ import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/server/metrics"
"cwtch.im/cwtch/server/storage"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
tor2 "cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/persistence"
"cwtch.im/tapir/primitives"
"cwtch.im/tapir/primitives/privacypass"
"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"
"os"
"time"
"path"
"sync"
)
// Server encapsulates a complete, compliant Cwtch server.
type Server struct {
service tapir.Service
config Config
metricsPack metrics.Monitors
closed bool
tokenTapirService tapir.Service
tokenServer *privacypass.TokenServer
tokenService primitives.Identity
tokenServicePrivKey ed25519.PrivateKey
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("./tokens.db")
s.tokenServer = privacypass.NewTokenServerFromStore(bs)
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()
}
// Run starts a server with the given privateKey
// TODO: surface errors
// TODO: handle HUP/KILL signals to exit and close Tor gracefully
// TODO: handle user input to exit
func (s *Server) Run(acn connectivity.ACN) {
s.closed = false
func (s *Server) Run(acn connectivity.ACN) error {
addressIdentity := tor.GetTorV3Hostname(s.config.PublicKey)
//tokenService := privacypass.NewTokenServer()
identity := primitives.InitializeIdentity("", &s.config.PrivateKey, &s.config.PublicKey)
var service tapir.Service
service = new(tor2.BaseOnionService)
@ -61,29 +65,33 @@ func (s *Server) Run(acn connectivity.ACN) {
ms := new(storage.MessageStore)
err := ms.Init(s.config.ConfigDir, s.config.MaxBufferLines, s.metricsPack.MessageCounter)
if err != nil {
log.Errorln(err)
acn.Close()
os.Exit(1)
return 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 = 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)
s.tokenTapirService.Listen(powTokenApp)
s.tokenServiceStopped = true
}()
go func() {
s.service.Listen(NewTokenBoardServer(ms, s.tokenServer))
s.onionServiceStopped = true
}()
for true {
s.service.Listen(NewTokenBoardServer(ms, s.tokenServer))
if s.closed {
return
}
time.Sleep(5 * time.Second)
}
s.lock.Lock()
s.running = true
s.lock.Unlock()
return nil
}
// KeyBundle provides the signed keybundle of the server
@ -97,10 +105,57 @@ func (s *Server) KeyBundle() *model.KeyBundle {
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)
}
return s.running, nil
}
// Shutdown kills the app closing all connections and freeing all goroutines
func (s *Server) Shutdown() {
s.closed = true
s.lock.Lock()
defer s.lock.Unlock()
s.service.Shutdown()
s.tokenTapirService.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

@ -1,9 +1,11 @@
package server
import (
"cwtch.im/tapir/primitives"
"crypto/rand"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"path"
@ -18,13 +20,20 @@ type Reporting struct {
// Config is a struct for storing basic server configuration
type Config struct {
ConfigDir string `json:"-"`
MaxBufferLines int `json:"maxBufferLines"`
PublicKey ed25519.PublicKey `json:"publicKey"`
PrivateKey ed25519.PrivateKey `json:"privateKey"`
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"`
ServerReporting Reporting `json:"serverReporting"`
TokenServiceK ristretto255.Scalar `json:"tokenServiceK"`
ServerReporting Reporting `json:"serverReporting"`
AutoStart bool `json:"autostart"`
}
// Identity returns an encapsulation of the servers keys
@ -61,6 +70,19 @@ func LoadConfig(configDir, filename string) Config {
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))
if err == nil {

View File

@ -3,10 +3,10 @@ package server
import (
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/server/storage"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/primitives/privacypass"
"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"
)

View File

@ -22,6 +22,7 @@ type ProfileStore interface {
GetProfileCopy(timeline bool) *model.Profile
GetNewPeerMessage() *event.Event
GetStatusMessages() []*event.Event
CheckPassword(string) bool
}
// CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them
@ -79,7 +80,7 @@ func upgradeV0ToV1(directory, password string) error {
func versionCheckUpgrade(directory, password string) {
version := detectVersion(directory)
log.Infof("versionCheck: %v\n", version)
log.Debugf("versionCheck: %v\n", version)
if version == -1 {
return
}

View File

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

View File

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

View File

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

View File

@ -3,11 +3,13 @@ package v1
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"encoding/base64"
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"os"
"path"
"strconv"
"time"
)
@ -31,6 +33,12 @@ type ProfileStoreV1 struct {
writer bool
}
// CheckPassword returns true if the given password produces the same key as the current stored key, otherwise false.
func (ps *ProfileStoreV1) CheckPassword(checkpass string) bool {
oldkey := CreateKey(checkpass, ps.salt[:])
return oldkey == ps.key
}
func initV1Directory(directory, password string) ([32]byte, [128]byte, error) {
key, salt, err := CreateKeySalt(password)
if err != nil {
@ -74,15 +82,18 @@ func (ps *ProfileStoreV1) initProfileWriterStore() {
ps.eventManager.Subscribe(event.SetPeerAttribute, ps.queue)
ps.eventManager.Subscribe(event.SetGroupAttribute, ps.queue)
ps.eventManager.Subscribe(event.AcceptGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.NewGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.RejectGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.NewGroup, ps.queue)
ps.eventManager.Subscribe(event.NewMessageFromGroup, ps.queue)
ps.eventManager.Subscribe(event.SendMessageToPeer, ps.queue)
ps.eventManager.Subscribe(event.PeerAcknowledgement, ps.queue)
ps.eventManager.Subscribe(event.NewMessageFromPeer, ps.queue)
ps.eventManager.Subscribe(event.PeerStateChange, ps.queue)
ps.eventManager.Subscribe(event.ServerStateChange, ps.queue)
ps.eventManager.Subscribe(event.DeleteContact, ps.queue)
ps.eventManager.Subscribe(event.DeleteGroup, ps.queue)
ps.eventManager.Subscribe(event.ChangePassword, ps.queue)
ps.eventManager.Subscribe(event.UpdateMessageFlags, ps.queue)
}
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
@ -214,7 +225,11 @@ func (ps *ProfileStoreV1) ChangePassword(oldpass, newpass, eventID string) {
if len(ssid) == groupIDLen {
ps.profile.Groups[ssid].LocalID = newLocalID
} else {
ps.profile.Contacts[ssid].LocalID = newLocalID
if ps.profile.Contacts[ssid] != nil {
ps.profile.Contacts[ssid].LocalID = newLocalID
} else {
log.Errorf("Unknown Contact: %v. This is probably the result of corrupted development data from fuzzing. This contact will not appear in the new profile.", ssid)
}
}
}
@ -236,9 +251,26 @@ func (ps *ProfileStoreV1) save() error {
bytes, _ := json.Marshal(ps.profile)
return ps.fs.Write(bytes)
}
return nil
}
func (ps *ProfileStoreV1) regenStreamStore(messages []model.Message, contact string) {
oldss := ps.streamStores[contact]
newLocalID := model.GenerateRandomID()
newSS := NewStreamStore(ps.directory, newLocalID, ps.key)
newSS.WriteN(messages)
if len(contact) == groupIDLen {
ps.profile.Groups[contact].LocalID = newLocalID
} else {
// We can assume this exists as regen stream store should only happen to *update* a message
ps.profile.Contacts[contact].LocalID = newLocalID
}
ps.streamStores[contact] = newSS
ps.save()
oldss.Delete()
}
// load instantiates a cwtchPeer from the file store
func (ps *ProfileStoreV1) load() error {
decrypted, err := ps.fs.Read()
@ -300,7 +332,6 @@ func (ps *ProfileStoreV1) GetProfileCopy(timeline bool) *model.Profile {
}
func (ps *ProfileStoreV1) eventHandler() {
log.Debugln("eventHandler()!")
for {
ev := ps.queue.Next()
log.Debugf("eventHandler event %v %v\n", ev.EventType, ev.EventID)
@ -372,8 +403,11 @@ func (ps *ProfileStoreV1) eventHandler() {
} else {
log.Errorf("error accepting group invite")
}
case event.NewGroupInvite:
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite], ev.Data[event.RemotePeer])
case event.RejectGroupInvite:
ps.profile.RejectInvite(ev.Data[event.GroupID])
ps.save()
case event.NewGroup:
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite])
if err == nil {
ps.save()
group := ps.profile.Groups[gid]
@ -381,17 +415,35 @@ func (ps *ProfileStoreV1) eventHandler() {
} else {
log.Errorf("error storing new group invite: %v (%v)", err, ev)
}
case event.SendMessageToPeer: // We need this to be able to save the outgoing messages to a peer
ps.attemptSavePeerMessage(ev, false)
case event.SendMessageToPeer: // cache the message till an ack, then it's given to stream store.
// stream store doesn't support updates, so we don't want to commit it till ack'd
ps.profile.AddSentMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now(), ev.EventID)
case event.NewMessageFromPeer:
ps.attemptSavePeerMessage(ev, true)
ps.profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now())
ps.attemptSavePeerMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.TimestampReceived], true)
case event.PeerAcknowledgement:
onion := ev.Data[event.RemotePeer]
eventID := ev.Data[event.EventID]
contact, ok := ps.profile.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
message := contact.Timeline.Messages[mIdx]
ps.attemptSavePeerMessage(onion, message.Message, message.Timestamp.Format(time.RFC3339Nano), false)
}
}
ps.profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
case event.NewMessageFromGroup:
groupid := ev.Data[event.GroupID]
received, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
sent, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampSent])
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: []byte(ev.Data[event.Signature]), PreviousMessageSig: []byte(ev.Data[event.PreviousSignature])}
sig, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
prevsig, _ := base64.StdEncoding.DecodeString(ev.Data[event.PreviousSignature])
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: sig, PreviousMessageSig: prevsig, Acknowledged: true}
ss, exists := ps.streamStores[groupid]
if exists {
// We need to store a local copy of the message...
ps.profile.GetGroup(groupid).Timeline.Insert(&message)
ss.Write(message)
} else {
log.Errorf("error storing new group message: %v stream store does not exist", ev)
@ -428,6 +480,29 @@ func (ps *ProfileStoreV1) eventHandler() {
oldpass := ev.Data[event.Password]
newpass := ev.Data[event.NewPassword]
ps.ChangePassword(oldpass, newpass, ev.EventID)
case event.UpdateMessageFlags:
handle := ev.Data[event.Handle]
mIx, err := strconv.Atoi(ev.Data[event.Index])
if err != nil {
log.Errorf("Invalid Message Index: %v", err)
return
}
flags, err := strconv.ParseUint(ev.Data[event.Flags], 2, 64)
if err != nil {
log.Errorf("Invalid Message Falgs: %v", err)
return
}
ps.profile.UpdateMessageFlags(handle, mIx, flags)
if len(handle) == groupIDLen {
ps.regenStreamStore(ps.profile.GetGroup(handle).Timeline.Messages, handle)
} else if contact, exists := ps.profile.GetContact(handle); exists {
if exists {
val, _ := contact.GetAttribute(event.SaveHistoryKey)
if val == event.SaveHistoryConfirmed {
ps.regenStreamStore(contact.Timeline.Messages, handle)
}
}
}
default:
return
}
@ -438,28 +513,28 @@ func (ps *ProfileStoreV1) eventHandler() {
// attemptSavePeerMessage checks if the peer has been configured to save history from this peer
// and if so the peer saves the message into history. fromPeer is used to control if the message is saved
// as coming from the remote peer or if it was sent by out profile.
func (ps *ProfileStoreV1) attemptSavePeerMessage(ev *event.Event, fromPeer bool) {
contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer])
func (ps *ProfileStoreV1) attemptSavePeerMessage(peerID, messageData, timestampeReceived string, fromPeer bool) {
contact, exists := ps.profile.GetContact(peerID)
if exists {
val, _ := contact.GetAttribute(event.SaveHistoryKey)
switch val {
case event.SaveHistoryConfirmed:
{
peerID := ev.Data[event.RemotePeer]
peerID := peerID
var received time.Time
var message model.Message
if fromPeer {
received, _ = time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
message = model.Message{Received: received, Timestamp: received, Message: ev.Data[event.Data], PeerID: peerID, Signature: []byte{}, PreviousMessageSig: []byte{}}
received, _ = time.Parse(time.RFC3339Nano, timestampeReceived)
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: peerID, Signature: []byte{}, PreviousMessageSig: []byte{}}
} else {
received := time.Now()
message = model.Message{Received: received, Timestamp: received, Message: ev.Data[event.Data], PeerID: ps.profile.Onion, Signature: []byte{}, PreviousMessageSig: []byte{}}
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: ps.profile.Onion, Signature: []byte{}, PreviousMessageSig: []byte{}, Acknowledged: true}
}
ss, exists := ps.streamStores[peerID]
if exists {
ss.Write(message)
} else {
log.Errorf("error storing new peer message: %v stream store does not exist", ev)
log.Errorf("error storing new peer message: %v stream store does not exist", peerID)
}
}
default:
@ -467,7 +542,7 @@ func (ps *ProfileStoreV1) attemptSavePeerMessage(ev *event.Event, fromPeer bool)
}
}
} else {
log.Errorf("error saving message for peer that doesn't exist: %v", ev)
log.Errorf("error saving message for peer that doesn't exist: %v", peerID)
}
}

View File

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

View File

@ -23,6 +23,7 @@ import (
"path"
"runtime"
"runtime/pprof"
"strings"
"testing"
"time"
)
@ -66,23 +67,25 @@ func serverCheck(t *testing.T, serverAddr string) bool {
}
func waitForPeerGroupConnection(t *testing.T, peer peer.CwtchPeer, groupID string) {
peerName, _ := peer.GetAttribute(attr.GetLocalScope("name"))
for {
fmt.Printf("%v checking group connection...\n", peer.GetName())
fmt.Printf("%v checking group connection...\n", peerName)
state, ok := peer.GetGroupState(groupID)
if ok {
fmt.Printf("Waiting for Peer %v to join group %v - state: %v\n", peer.GetName(), groupID, state)
fmt.Printf("Waiting for Peer %v to join group %v - state: %v\n", peerName, groupID, state)
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peer.GetOnion(), groupID)
}
if state != connections.SYNCED {
fmt.Printf("peer %v %v waiting connect to group %v, currently: %v\n", peer.GetName(), peer.GetOnion(), groupID, connections.ConnectionStateName[state])
fmt.Printf("peer %v %v waiting connect to group %v, currently: %v\n", peerName, peer.GetOnion(), groupID, connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
fmt.Printf("peer %v %v CONNECTED to group %v\n", peer.GetName(), peer.GetOnion(), groupID)
fmt.Printf("peer %v %v CONNECTED to group %v\n", peerName, peer.GetOnion(), groupID)
break
}
}
time.Sleep(time.Second * 2)
}
return
}
@ -100,7 +103,9 @@ func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.Cw
time.Sleep(time.Second * 5)
continue
} else {
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peera.GetName(), peerb.GetName())
peerAName, _ := peera.GetAttribute(attr.GetLocalScope("name"))
peerBName, _ := peerb.GetAttribute(attr.GetLocalScope("name"))
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
break
}
}
@ -118,14 +123,14 @@ func TestCwtchPeerIntegration(t *testing.T) {
log.ExcludeFromPattern("event/eventmanager")
log.ExcludeFromPattern("pipeBridge")
log.ExcludeFromPattern("tapir")
os.Mkdir("tordir",0700)
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000)+9051
controlPort := mrand.Intn(1000)+9052
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
@ -139,7 +144,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
pid,err := acn.GetPID()
pid, _ := acn.GetPID()
t.Logf("Tor pid: %v", pid)
// ***** Cwtch Server management *****
@ -300,12 +305,16 @@ func TestCwtchPeerIntegration(t *testing.T) {
time.Sleep(time.Second * 5)
fmt.Println("Bob examining groups and accepting invites...")
for _, groupID := range bob.GetGroups() {
group := bob.GetGroup(groupID)
fmt.Printf("Bob group: %v (Accepted: %v)\n", group.GroupID, group.Accepted)
if group.Accepted == false {
fmt.Printf("Bob received and accepting group invite: %v\n", group.GroupID)
bob.AcceptInvite(group.GroupID)
for _, message := range bob.GetContact(alice.GetOnion()).Timeline.GetMessages() {
fmt.Printf("Found message from Alice: %v", message.Message)
if strings.HasPrefix(message.Message, "torv3") {
gid, err := bob.ImportGroup(message.Message)
if err == nil {
fmt.Printf("Bob found invite...now accepting %v...", gid)
bob.AcceptInvite(gid)
} else {
t.Fatalf("Bob could not accept invite...%v", gid)
}
}
}
@ -319,25 +328,25 @@ func TestCwtchPeerIntegration(t *testing.T) {
fmt.Println("Starting conversation in group...")
// Conversation
fmt.Printf("%v> %v\n", aliceName, aliceLines[0])
err = alice.SendMessageToGroup(groupID, aliceLines[0])
_, err = alice.SendMessageToGroupTracked(groupID, aliceLines[0])
if err != nil {
t.Fatalf("Alice failed to send a message to the group: %v", err)
}
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v\n", bobName, bobLines[0])
err = bob.SendMessageToGroup(groupID, bobLines[0])
_, err = bob.SendMessageToGroupTracked(groupID, bobLines[0])
if err != nil {
t.Fatalf("Bob failed to send a message to the group: %v", err)
}
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v\n", aliceName, aliceLines[1])
alice.SendMessageToGroup(groupID, aliceLines[1])
alice.SendMessageToGroupTracked(groupID, aliceLines[1])
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v\n", bobName, bobLines[1])
bob.SendMessageToGroup(groupID, bobLines[1])
bob.SendMessageToGroupTracked(groupID, bobLines[1])
time.Sleep(time.Second * 10)
fmt.Println("Alice inviting Carol to group...")
@ -347,12 +356,16 @@ func TestCwtchPeerIntegration(t *testing.T) {
}
time.Sleep(time.Second * 60) // Account for some token acquisition in Alice and Bob flows.
fmt.Println("Carol examining groups and accepting invites...")
for _, groupID := range carol.GetGroups() {
group := carol.GetGroup(groupID)
fmt.Printf("Carol group: %v (Accepted: %v)\n", group.GroupID, group.Accepted)
if group.Accepted == false {
fmt.Printf("Carol received and accepting group invite: %v\n", group.GroupID)
carol.AcceptInvite(group.GroupID)
for _, message := range carol.GetContact(alice.GetOnion()).Timeline.GetMessages() {
fmt.Printf("Found message from Alice: %v", message.Message)
if strings.HasPrefix(message.Message, "torv3") {
gid, err := carol.ImportGroup(message.Message)
if err == nil {
fmt.Printf("Carol found invite...now accepting %v...", gid)
carol.AcceptInvite(gid)
} else {
t.Fatalf("Carol could not accept invite...%v", gid)
}
}
}
@ -367,12 +380,12 @@ func TestCwtchPeerIntegration(t *testing.T) {
numGoRotinesPostCarolConnect := runtime.NumGoroutine()
fmt.Printf("%v> %v", bobName, bobLines[2])
bob.SendMessageToGroup(groupID, bobLines[2])
bob.SendMessageToGroupTracked(groupID, bobLines[2])
// Bob should have enough tokens so we don't need to account for
// token acquisition here...
fmt.Printf("%v> %v", carolName, carolLines[0])
carol.SendMessageToGroup(groupID, carolLines[0])
carol.SendMessageToGroupTracked(groupID, carolLines[0])
time.Sleep(time.Second * 30) // we need to account for spam-based token acquisition, but everything should
// be warmed-up and delays should be pretty small.