Interim Work - P2P now Works on New Storage Model
continuous-integration/drone/push Build is pending Details

This commit is contained in:
Sarah Jamie Lewis 2021-11-09 15:47:33 -08:00
parent e2bba41a9a
commit 8c80340a3d
11 changed files with 1182 additions and 535 deletions

View File

@ -1,16 +1,16 @@
package app
import (
"crypto/rand"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/storage"
"encoding/hex"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
@ -32,7 +32,6 @@ type application struct {
appletPeers
appletACN
appletPlugins
storage map[string]storage.ProfileStore
engines map[string]connections.Engine
appBus event.Manager
appmutex sync.Mutex
@ -41,7 +40,6 @@ type application struct {
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
type Application interface {
LoadProfiles(password string)
CreatePeer(name string, password string)
CreateTaggedPeer(name string, password string, tag string)
DeletePeer(onion string, currentPassword string)
AddPeerPlugin(onion string, pluginID plugins.PluginID)
@ -61,7 +59,7 @@ type Application interface {
}
// LoadProfileFn is the function signature for a function in an app that loads a profile
type LoadProfileFn func(profile *model.Profile, store storage.ProfileStore)
type LoadProfileFn func(profile peer.CwtchPeer)
func newAppCore(appDirectory string) *applicationCore {
appCore := &applicationCore{eventBuses: make(map[string]event.Manager), directory: appDirectory}
@ -72,33 +70,13 @@ func newAppCore(appDirectory string) *applicationCore {
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
func NewApp(acn connectivity.ACN, appDirectory string) Application {
log.Debugf("NewApp(%v)\n", appDirectory)
app := &application{storage: make(map[string]storage.ProfileStore), engines: make(map[string]connections.Engine), applicationCore: *newAppCore(appDirectory), appBus: event.NewEventManager()}
app := &application{engines: make(map[string]connections.Engine), applicationCore: *newAppCore(appDirectory), appBus: event.NewEventManager()}
app.appletPeers.init()
app.appletACN.init(acn, app.getACNStatusHandler())
return app
}
// CreatePeer creates a new Peer with a given name and core required accessories (eventbus)
func (ac *applicationCore) CreatePeer(name string) (*model.Profile, error) {
log.Debugf("CreatePeer(%v)\n", name)
profile := storage.NewProfile(name)
ac.coremutex.Lock()
defer ac.coremutex.Unlock()
_, exists := ac.eventBuses[profile.Onion]
if exists {
return nil, fmt.Errorf("error: profile for onion %v already exists", profile.Onion)
}
eventBus := event.NewEventManager()
ac.eventBuses[profile.Onion] = eventBus
return profile, nil
}
func (ac *applicationCore) DeletePeer(onion string) {
ac.coremutex.Lock()
defer ac.coremutex.Unlock()
@ -107,38 +85,35 @@ func (ac *applicationCore) DeletePeer(onion string) {
delete(ac.eventBuses, onion)
}
// GenerateRandomID generates a random 16 byte hex id code
func GenerateRandomID() string {
randBytes := make([]byte, 16)
rand.Read(randBytes)
return path.Join(hex.EncodeToString(randBytes))
}
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
profile, err := app.applicationCore.CreatePeer(name)
profileDirectory := path.Join(app.directory, "profiles", GenerateRandomID())
profile, err := peer.CreateEncryptedStorePeer(profileDirectory, name, password)
if err != nil {
log.Errorf("Error Creating Peer: %v", err)
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
return
}
profileStore := storage.CreateProfileWriterStore(app.eventBuses[profile.Onion], path.Join(app.directory, "profiles", profile.LocalID), password, profile)
app.storage[profile.Onion] = profileStore
pc := app.storage[profile.Onion].GetProfileCopy(true)
p := peer.FromProfile(pc)
p.Init(app.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
// TODO: Would be nice if ProtocolEngine did not need to explicitly be given the Private Key.
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
app.peers[profile.Onion] = p
app.engines[profile.Onion] = engine
eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus
profile.Init(app.eventBuses[profile.GetOnion()])
app.peers[profile.GetOnion()] = profile
app.engines[profile.GetOnion()] = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()])
if tag != "" {
p.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag)
profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag)
}
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.True}))
}
// CreatePeer creates a new Peer with the given name and required accessories (eventbus, storage, protocol engine)
func (app *application) CreatePeer(name string, password string) {
app.CreateTaggedPeer(name, password, "")
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True}))
}
func (app *application) DeletePeer(onion string, password string) {
@ -146,27 +121,23 @@ func (app *application) DeletePeer(onion string, password string) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
if app.storage[onion].CheckPassword(password) {
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.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.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
return
}
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))
}
@ -186,27 +157,40 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf
}
for _, file := range files {
eventBus := event.NewEventManager()
profileStore, err := storage.LoadProfileWriterStore(eventBus, path.Join(ac.directory, "profiles", file.Name()), password)
// Attempt to load an encrypted database
profileDirectory := path.Join(ac.directory, "profiles", file.Name())
profile, err := peer.FromEncryptedDatabase(profileDirectory, password)
if err == nil {
// return the load the profile...
loadProfileFn(profile)
}
// On failure
if err != nil {
continue
eventBus := event.NewEventManager()
profileStore, err := storage.LoadProfileWriterStore(eventBus, profileDirectory, password)
if err != nil {
continue
}
profile := profileStore.GetProfileCopy(timeline)
_, exists := ac.eventBuses[profile.Onion]
if exists {
profileStore.Shutdown()
eventBus.Shutdown()
log.Errorf("profile for onion %v already exists", profile.Onion)
continue
}
ac.coremutex.Lock()
ac.eventBuses[profile.Onion] = eventBus
ac.coremutex.Unlock()
}
profile := profileStore.GetProfileCopy(timeline)
_, exists := ac.eventBuses[profile.Onion]
if exists {
profileStore.Shutdown()
eventBus.Shutdown()
log.Errorf("profile for onion %v already exists", profile.Onion)
continue
}
ac.coremutex.Lock()
ac.eventBuses[profile.Onion] = eventBus
ac.coremutex.Unlock()
loadProfileFn(profile, profileStore)
}
return nil
}
@ -214,19 +198,15 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
func (app *application) LoadProfiles(password string) {
count := 0
app.applicationCore.LoadProfiles(password, true, func(profile *model.Profile, profileStore storage.ProfileStore) {
peer := peer.FromProfile(profile)
peer.Init(app.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
app.applicationCore.LoadProfiles(password, true, func(profile peer.CwtchPeer) {
eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus
profile.Init(app.eventBuses[profile.GetOnion()])
app.appmutex.Lock()
app.peers[profile.Onion] = peer
app.storage[profile.Onion] = profileStore
app.engines[profile.Onion] = engine
app.peers[profile.GetOnion()] = profile
app.engines[profile.GetOnion()] = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()])
app.appmutex.Unlock()
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.False}))
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
count++
})
if count == 0 {
@ -280,8 +260,6 @@ func (app *application) ShutdownPeer(onion string) {
delete(app.peers, onion)
app.engines[onion].Shutdown()
delete(app.engines, onion)
app.storage[onion].Shutdown()
delete(app.storage, onion)
app.appletPlugins.Shutdown()
}
@ -293,8 +271,6 @@ func (app *application) Shutdown() {
app.appletPlugins.ShutdownPeer(id)
log.Debugf("Shutting Down Engines for %v", id)
app.engines[id].Shutdown()
log.Debugf("Shutting Down Storage for %v", id)
app.storage[id].Shutdown()
log.Debugf("Shutting Down Bus for %v", id)
app.eventBuses[id].Shutdown()
}

1
go.mod
View File

@ -7,6 +7,7 @@ require (
git.openprivacy.ca/openprivacy/connectivity v1.5.0
git.openprivacy.ca/openprivacy/log v1.0.3
github.com/gtank/ristretto255 v0.1.2
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect

3
go.sum
View File

@ -22,11 +22,14 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=

54
model/conversation.go Normal file
View File

@ -0,0 +1,54 @@
package model
import "encoding/json"
// AccessControl is a type determining client assigned authorization to a peer
type AccessControl struct {
Blocked bool // Any attempts from this handle to connect are blocked
Read bool // Allows a handle to access the conversation
Append bool // Allows a handle to append new messages to the conversation
}
// DefaultP2PAccessControl - because in the year 2021, go does not support constant structs...
func DefaultP2PAccessControl() AccessControl {
return AccessControl{Read: true, Append: true, Blocked: false}
}
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
// functions
type AccessControlList map[string]AccessControl
func (acl *AccessControlList) Serialize() []byte {
data, _ := json.Marshal(acl)
return data
}
func DeserializeAccessControlList(data []byte) AccessControlList {
var acl AccessControlList
json.Unmarshal(data, &acl)
return acl
}
type Attributes map[string]string
func (a *Attributes) Serialize() []byte {
data, _ := json.Marshal(a)
return data
}
func DeserializeAttributes(data []byte) Attributes {
var attributes Attributes
json.Unmarshal(data, &attributes)
return attributes
}
// Conversation encapsulates high-level information about a conversation, including the
// handle, any set attributes, the access control list associated with the message tree and the
// accepted status of the conversation (whether the user has consented into the conversation).
type Conversation struct {
ID int
Handle string
Attributes Attributes
ACL AccessControlList
Accepted bool
}

View File

@ -2,11 +2,12 @@ package peer
import (
"cwtch.im/cwtch/model/constants"
"encoding/base32"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"runtime"
"strconv"
"strings"
@ -50,53 +51,78 @@ type cwtchPeer struct {
mutex sync.Mutex
shutdown bool
listenStatus bool
storage *CwtchProfileStorage
state map[string]connections.ConnectionState
queue event.Queue
eventBus event.Manager
}
// GenerateProtocolEngine
// Status: New in 1.5
func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager) connections.Engine {
return connections.NewProtocolEngine(cp.GetIdentity(), cp.Profile.Ed25519PrivateKey, acn, bus, cp.Profile.ContactsAuthorizations())
}
// GetIdentity
// Status: New in 1.5
func (cp *cwtchPeer) GetIdentity() primitives.Identity {
return primitives.InitializeIdentity("", &cp.Profile.Ed25519PrivateKey, &cp.Profile.Ed25519PublicKey)
}
// SendScopedZonedGetValToContact
// Status: No change in 1.5
func (cp *cwtchPeer) SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, path string) {
ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, handle, event.Scope, string(scope), event.Path, string(zone.ConstructZonedPath(path)))
cp.eventBus.Publish(ev)
}
// GetScopedZonedAttribute
// Status: Ready for 1.5
func (cp *cwtchPeer) GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))
log.Debugf("looking up attribute %v %v %v (%v)", scope, zone, key, scopedZonedKey)
value, err := cp.storage.LoadProfileKeyValue(TypeAttribute, scopedZonedKey.ToString())
if val, exists := cp.Profile.GetAttribute(scopedZonedKey.ToString()); exists {
return val, true
log.Debugf("found [%v] %v", string(value), err)
if err != nil {
return "", false
}
return "", false
return string(value), true
}
// SetScopedZonedAttribute
// Status: Ready for 1.5
func (cp *cwtchPeer) SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))
log.Debugf("storing attribute: %v = %v", scopedZonedKey, value)
cp.Profile.SetAttribute(scopedZonedKey.ToString(), value)
defer cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{
event.Key: scopedZonedKey.ToString(),
event.Data: value,
}))
err := cp.storage.StoreProfileKeyValue(TypeAttribute, scopedZonedKey.ToString(), []byte(value))
if err != nil {
log.Errorf("error setting attribute %v")
}
}
// SendMessage is a higher level that merges sending messages to contacts and group handles
// If you try to send a message to a handle that doesn't exist, malformed or an incorrect type then
// this function will error
// Status: TODO
func (cp *cwtchPeer) SendMessage(handle string, message string) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
var ev event.Event
// Group Handles are always 32 bytes in length, but we forgo any further testing here
// and delegate the group existence check to EncryptMessageToGroup
if len(handle) == 32 {
cp.mutex.Lock()
defer cp.mutex.Unlock()
group := cp.Profile.GetGroup(handle)
if group == nil {
return errors.New("invalid group id")
@ -112,14 +138,15 @@ func (cp *cwtchPeer) SendMessage(handle string, message string) error {
ev = event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupID: handle, event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)})
} else if tor.IsValidHostname(handle) {
// We assume we are sending to a Contact.
// (Servers are technically Contacts)
contact, exists := cp.Profile.GetContact(handle)
contact, exists := cp.FetchConversationInfo(handle)
ev = event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.RemotePeer: handle, event.Data: message})
// If the contact exists replace the event id wih the index of this message in the contacts timeline...
// Otherwise assume we don't log the message in the timeline...
if exists {
ev.EventID = strconv.Itoa(contact.Timeline.Len())
cp.Profile.AddSentMessageToContactTimeline(handle, message, time.Now(), ev.EventID)
if exists != nil {
//ev.EventID = strconv.Itoa(contact.Timeline.Len())
cp.mutex.Lock()
defer cp.mutex.Unlock()
cp.storage.InsertMessage(contact.ID, 0, message, model.Attributes{"ack": event.False, "sent": time.Now().String()})
}
// Regardless we publish the send message to peer event for the protocol engine to execute on...
// We assume this is always successful as it is always valid to attempt to
@ -132,6 +159,8 @@ func (cp *cwtchPeer) SendMessage(handle string, message string) error {
return nil
}
// UpdateMessageFlags
// Status: TODO
func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
@ -142,162 +171,60 @@ func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
// BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname
// known to peer.
// Status: Ready for 1.5
func (cp *cwtchPeer) BlockUnknownConnections() {
cp.eventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{}))
}
// AllowUnknownConnections will permit connections from unknown contacts.
// Status: Ready for 1.5
func (cp *cwtchPeer) AllowUnknownConnections() {
cp.eventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{}))
}
// ReadContacts is a meta-interface intended to restrict callers to read-only access to contacts
type ReadContacts interface {
GetContacts() []string
GetContact(string) *model.PublicProfile
GetContactAttribute(string, string) (string, bool)
}
// ModifyContacts is a meta-interface intended to restrict callers to modify-only access to contacts
type ModifyContacts interface {
AddContact(nick, onion string, authorization model.Authorization)
SetContactAuthorization(string, model.Authorization) error
SetContactAttribute(string, string, string)
DeleteContact(string)
}
// AccessPeeringState provides access to functions relating to the underlying connections of a peer.
type AccessPeeringState interface {
GetPeerState(string) (connections.ConnectionState, bool)
}
// ModifyPeeringState is a meta-interface intended to restrict callers to modify-only access to connection peers
type ModifyPeeringState interface {
BlockUnknownConnections()
AllowUnknownConnections()
PeerWithOnion(string)
JoinServer(string) error
}
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
// and peers.
type ModifyContactsAndPeers interface {
ReadContacts
ModifyContacts
ModifyPeeringState
}
// ReadServers provides access to the servers
type ReadServers interface {
GetServers() []string
}
// ReadGroups provides read-only access to group state
type ReadGroups interface {
GetGroup(string) *model.Group
GetGroupState(string) (connections.ConnectionState, bool)
GetGroups() []string
GetGroupAttribute(string, string) (string, bool)
ExportGroup(string) (string, error)
}
// ModifyGroups provides write-only access add/edit/remove new groups
type ModifyGroups interface {
ImportGroup(string) (string, error)
StartGroup(string) (string, string, error)
AcceptInvite(string) error
RejectInvite(string)
DeleteGroup(string)
SetGroupAttribute(string, string, string)
}
// ModifyServers provides write-only access to servers
type ModifyServers interface {
AddServer(string) error
ResyncServer(onion string) error
}
// SendMessages enables a caller to sender messages to a contact
type SendMessages interface {
SendMessage(handle string, message string) error
// Deprecated: is unsafe
SendGetValToPeer(string, string, string)
SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, key string)
// TODO
// Deprecated use overlays instead
InviteOnionToGroup(string, string) error
}
// ModifyMessages enables a caller to modify the messages in a timeline
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()
// GetOnion is deprecated. If you find yourself needing to rely on this method it is time
// to consider replacing this with a GetAddress(es) function that can fully expand cwtch beyond the boundaries
// of tor v3 onion services.
// Deprecated
GetOnion() string
// SetScopedZonedAttribute allows the setting of an attribute by scope and zone
// scope.zone.key = value
SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string)
// GetScopedZonedAttribute allows the retrieval of an attribute by scope and zone
// scope.zone.key = value
GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool)
ReadContacts
ModifyContacts
AccessPeeringState
ModifyPeeringState
ReadGroups
ModifyGroups
ReadServers
ModifyServers
SendMessages
ModifyMessages
ShareFile(fileKey string, serializedManifest string)
}
// NewCwtchPeer creates and returns a new cwtchPeer with the given name.
func NewCwtchPeer(name string) CwtchPeer {
// NewProfileWithEncryptedStorage instantiates a new Cwtch Profile from encrypted storage
func NewProfileWithEncryptedStorage(name string, cps *CwtchProfileStorage) CwtchPeer {
cp := new(cwtchPeer)
cp.Profile = model.GenerateNewProfile(name)
cp.shutdown = false
cp.storage = cps
cp.state = make(map[string]connections.ConnectionState)
cp.Profile = model.GenerateNewProfile(name)
// Store all the Necessary Base Attributes In The Database
cp.storage.StoreProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)).ToString(), []byte(name))
cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", cp.Profile.Ed25519PrivateKey)
cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", cp.Profile.Ed25519PublicKey)
return cp
}
// FromEncryptedStorage loads an existing Profile from Encrypted Storage
func FromEncryptedStorage(cps *CwtchProfileStorage) CwtchPeer {
cp := new(cwtchPeer)
cp.shutdown = false
cp.storage = cps
cp.state = make(map[string]connections.ConnectionState)
return cp
}
// FromProfile generates a new peer from a profile.
func FromProfile(profile *model.Profile) CwtchPeer {
// Deprecated - Only to be used for importing new profiles
func FromProfile(profile *model.Profile, cps *CwtchProfileStorage) CwtchPeer {
cp := new(cwtchPeer)
cp.Profile = profile
cp.shutdown = false
cp.storage = cps
// Store all the Necessary Base Attributes In The Database
cp.storage.StoreProfileKeyValue(TypeAttribute, "public.profile.name", []byte(profile.Name))
cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", cp.Profile.Ed25519PrivateKey)
cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", cp.Profile.Ed25519PublicKey)
return cp
}
// Init instantiates a cwtchPeer
// Status: Ready for 1.5
func (cp *cwtchPeer) Init(eventBus event.Manager) {
cp.InitForEvents(eventBus, DefaultEventsToHandle)
@ -354,6 +281,8 @@ func (cp *cwtchPeer) Init(eventBus event.Manager) {
}
}
// InitForEvents
// Status: Ready for 1.5
func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) {
cp.queue = event.NewQueue()
go cp.eventHandler()
@ -363,6 +292,7 @@ func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.T
}
// AutoHandleEvents sets an event (if able) to be handled by this peer
// Status: Ready for 1.5
func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) {
for _, ev := range events {
if _, exists := autoHandleableEvents[ev]; exists {
@ -374,6 +304,7 @@ func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) {
}
// ImportGroup initializes a group from an imported source rather than a peer invite
// Status: TODO
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
@ -386,7 +317,65 @@ func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) {
return gid, err
}
// NewContactConversation create a new p2p conversation with the given acl applied to the handle.
func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessControl, accepted bool) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, model.AccessControlList{handle: acl}, accepted)
}
// AcceptConversation looks up a conversation by `handle` and sets the Accepted status to `true`
// This will cause Cwtch to auto connect to this conversation on start up
func (cp *cwtchPeer) AcceptConversation(handle string) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.AcceptConversation(handle)
}
// FetchConversationInfo returns information about the given conversation referenced by the handle
func (cp *cwtchPeer) FetchConversationInfo(handle string) (*model.Conversation, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.GetConversation(handle)
}
// DeleteConversation purges all data about the conversation, including message timelines, referenced by the handle
func (cp *cwtchPeer) DeleteConversation(handle string) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.DeleteConversation(handle)
}
// SetConversationAttribute sets the conversation attribute at path to value
func (cp *cwtchPeer) SetConversationAttribute(handle string, path attr.ScopedZonedPath, value string) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.SetConversationAttribute(handle, path, value)
}
// GetConversationAttribute is a shortcut method for retrieving the value of a given path
func (cp *cwtchPeer) GetConversationAttribute(handle string, path attr.ScopedZonedPath) (string, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
ci, err := cp.storage.GetConversation(handle)
if err != nil {
return "", err
}
val, exists := ci.Attributes[path.ToString()]
if !exists {
return "", fmt.Errorf("%v does not exist for conversation %v", path.ToString(), handle)
}
return val, nil
}
func (cp *cwtchPeer) GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.GetChannelMessage(conversation, channel, id)
}
// ExportGroup serializes a group invite so it can be given offline
// Status: TODO
func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
@ -398,6 +387,7 @@ func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
}
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
// Status: TODO
func (cp *cwtchPeer) StartGroup(server string) (string, string, error) {
cp.mutex.Lock()
groupID, invite, err := cp.Profile.StartGroup(server)
@ -421,6 +411,7 @@ func (cp *cwtchPeer) StartGroup(server string) (string, string, error) {
}
// GetGroups returns an unordered list of all group IDs.
// Status: TODO
func (cp *cwtchPeer) GetGroups() []string {
cp.mutex.Lock()
defer cp.mutex.Unlock()
@ -428,133 +419,105 @@ func (cp *cwtchPeer) GetGroups() []string {
}
// GetGroup returns a pointer to a specific group, nil if no group exists.
// Status: TODO
func (cp *cwtchPeer) GetGroup(groupID string) *model.Group {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.Profile.GetGroup(groupID)
}
func (cp *cwtchPeer) AddContact(nick, onion string, authorization model.Authorization) {
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
pp := &model.PublicProfile{Name: nick, Ed25519PublicKey: decodedPub, Authorization: authorization, Onion: onion, Attributes: map[string]string{"nick": nick}}
cp.Profile.AddContact(onion, pp)
pd, _ := json.Marshal(pp)
cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
event.Data: string(pd),
event.RemotePeer: onion,
}))
cp.eventBus.Publish(event.NewEventList(event.SetPeerAuthorization, event.RemotePeer, onion, event.Authorization, string(authorization)))
// Default to Deleting Peer History
cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, event.SaveHistoryKey, event.DeleteHistoryDefault))
}
// AddServer takes in a serialized server specification (a bundle of related keys) and adds a contact for the
// server assuming there are no errors and the contact doesn't already exist.
// TODO in the future this function should also integrate with a trust provider to validate the key bundle.
// Status: TODO
func (cp *cwtchPeer) AddServer(serverSpecification string) error {
// This confirms that the server did at least sign the bundle
keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification))
if err != nil {
return err
}
log.Debugf("Got new key bundle %v", keyBundle.Serialize())
// TODO if the key bundle is incomplete then error out. In the future we may allow servers to attest to new
// keys or subsets of keys, but for now they must commit only to a complete set of keys required for Cwtch Groups
// (that way we can be assured that the keybundle we store is a valid one)
if !keyBundle.HasKeyType(model.KeyTypeTokenOnion) || !keyBundle.HasKeyType(model.KeyTypeServerOnion) || !keyBundle.HasKeyType(model.KeyTypePrivacyPass) {
return errors.New("keybundle is incomplete")
}
if keyBundle.HasKeyType(model.KeyTypeServerOnion) {
onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion)
onion := string(onionKey)
// Add the contact if we don't already have it
if cp.GetContact(onion) == nil {
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
ab := keyBundle.AttributeBundle()
pp := &model.PublicProfile{Name: onion, Ed25519PublicKey: decodedPub, Authorization: model.AuthUnknown, Onion: onion, Attributes: ab}
// The only part of this function that actually modifies the profile...
cp.mutex.Lock()
cp.Profile.AddContact(onion, pp)
cp.mutex.Unlock()
pd, _ := json.Marshal(pp)
// Sync the Storage Engine
cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
event.Data: string(pd),
event.RemotePeer: onion,
}))
}
// At this point we know the server exists
server := cp.GetContact(onion)
ab := keyBundle.AttributeBundle()
// Check server bundle for consistency if we have different keys stored than in the tofu bundle then we
// abort...
for k, v := range ab {
val, exists := server.GetAttribute(k)
if exists {
if val != v {
// this is inconsistent!
return model.InconsistentKeyBundleError
}
}
// we haven't seen this key associated with the server before
}
// Store the key bundle for the server so we can reconstruct a tofubundle invite
cp.SetContactAttribute(onion, string(model.BundleType), serverSpecification)
// If we have gotten to this point we can assume this is a safe key bundle signed by the
// server with no conflicting keys. So we are going to publish all the keys
for k, v := range ab {
log.Debugf("Server (%v) has %v key %v", onion, k, v)
cp.SetContactAttribute(onion, k, v)
}
return nil
}
return err
}
// GetContacts returns an unordered list of onions
func (cp *cwtchPeer) GetContacts() []string {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.Profile.GetContacts()
//// This confirms that the server did at least sign the bundle
//keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification))
//if err != nil {
// return err
//}
//log.Debugf("Got new key bundle %v", keyBundle.Serialize())
//
//// TODO if the key bundle is incomplete then error out. In the future we may allow servers to attest to new
//// keys or subsets of keys, but for now they must commit only to a complete set of keys required for Cwtch Groups
//// (that way we can be assured that the keybundle we store is a valid one)
//if !keyBundle.HasKeyType(model.KeyTypeTokenOnion) || !keyBundle.HasKeyType(model.KeyTypeServerOnion) || !keyBundle.HasKeyType(model.KeyTypePrivacyPass) {
// return errors.New("keybundle is incomplete")
//}
//
//if keyBundle.HasKeyType(model.KeyTypeServerOnion) {
// onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion)
// onion := string(onionKey)
//
// // Add the contact if we don't already have it
// if cp.GetContact(onion) == nil {
// decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
// ab := keyBundle.AttributeBundle()
// pp := &model.PublicProfile{Name: onion, Ed25519PublicKey: decodedPub, Authorization: model.AuthUnknown, Onion: onion, Attributes: ab}
//
// // The only part of this function that actually modifies the profile...
// cp.mutex.Lock()
// cp.Profile.AddContact(onion, pp)
// cp.mutex.Unlock()
//
// pd, _ := json.Marshal(pp)
//
// // Sync the Storage Engine
// cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
// event.Data: string(pd),
// event.RemotePeer: onion,
// }))
// }
//
// // At this point we know the server exists
// server := cp.GetContact(onion)
// ab := keyBundle.AttributeBundle()
//
// // Check server bundle for consistency if we have different keys stored than in the tofu bundle then we
// // abort...
// for k, v := range ab {
// val, exists := server.GetAttribute(k)
// if exists {
// if val != v {
// // this is inconsistent!
// return model.InconsistentKeyBundleError
// }
// }
// // we haven't seen this key associated with the server before
// }
//
// // Store the key bundle for the server so we can reconstruct a tofubundle invite
// cp.SetContactAttribute(onion, string(model.BundleType), serverSpecification)
//
// // If we have gotten to this point we can assume this is a safe key bundle signed by the
// // server with no conflicting keys. So we are going to publish all the keys
// for k, v := range ab {
// log.Debugf("Server (%v) has %v key %v", onion, k, v)
// cp.SetContactAttribute(onion, k, v)
// }
//
// return nil
//}
return nil
}
// GetServers returns an unordered list of servers
// Status: TODO
func (cp *cwtchPeer) GetServers() []string {
contacts := cp.Profile.GetContacts()
var servers []string
for _, contact := range contacts {
if cp.GetContact(contact).IsServer() {
servers = append(servers, contact)
}
}
return servers
}
// GetContact returns a given contact, nil is no such contact exists
func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
cp.mutex.Lock()
defer cp.mutex.Unlock()
contact, _ := cp.Profile.GetContact(onion)
return contact
}
// GetOnion
// Status: Deprecated in 1.5
func (cp *cwtchPeer) GetOnion() string {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.Profile.Onion
}
// GetPeerState
// Status: TODO
func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
@ -564,6 +527,8 @@ func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bo
return connections.DISCONNECTED, false
}
// GetGroupState
// Status: TODO
func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
@ -573,33 +538,14 @@ func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState,
return connections.DISCONNECTED, false
}
// PeerWithOnion is the entry point for cwtchPeer relationships
// PeerWithOnion initiates a request to the Protocol Engine to set up Cwtch Session with a given tor v3 onion
// address.
func (cp *cwtchPeer) PeerWithOnion(onion string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if _, exists := cp.Profile.GetContact(onion); !exists {
cp.AddContact(onion, onion, model.AuthApproved)
}
cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion}))
}
// DeleteContact deletes a peer from the profile, storage, and handling
func (cp *cwtchPeer) DeleteContact(onion string) {
cp.mutex.Lock()
cp.Profile.DeleteContact(onion)
defer cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEventList(event.DeleteContact, event.RemotePeer, onion))
}
// DeleteGroup deletes a Group from the profile, storage, and handling
func (cp *cwtchPeer) DeleteGroup(groupID string) {
cp.mutex.Lock()
cp.Profile.DeleteGroup(groupID)
defer cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEventList(event.DeleteGroup, event.GroupID, groupID))
}
// InviteOnionToGroup kicks off the invite process
// Status: TODO
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
cp.mutex.Lock()
group := cp.Profile.GetGroup(groupid)
@ -616,73 +562,47 @@ func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
}
// JoinServer manages a new server connection with the given onion address
// Status: TODO
func (cp *cwtchPeer) JoinServer(onion string) error {
if cp.GetContact(onion) != nil {
tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
if yExists && onionExists {
signature, exists := cp.GetContactAttribute(onion, lastKnownSignature)
if !exists {
signature = base64.StdEncoding.EncodeToString([]byte{})
}
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
return nil
}
}
//if cp.GetContact(onion) != nil {
// tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
// tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
// if yExists && onionExists {
// signature, exists := cp.GetContactAttribute(onion, lastKnownSignature)
// if !exists {
// signature = base64.StdEncoding.EncodeToString([]byte{})
// }
// cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
// return nil
// }
//}
return errors.New("no keys found for server connection")
}
// ResyncServer completely tears down and resyncs a new server connection with the given onion address
// Status: TODO
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
}
}
//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")
}
// SendGetValToPeer
// Status: Ready for 1.5
func (cp *cwtchPeer) SendGetValToPeer(onion string, scope string, path string) {
ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, onion, event.Scope, scope, event.Path, path)
cp.eventBus.Publish(ev)
}
// BlockPeer blocks an existing peer relationship.
func (cp *cwtchPeer) SetContactAuthorization(peer string, authorization model.Authorization) error {
cp.mutex.Lock()
err := cp.Profile.SetContactAuthorization(peer, authorization)
cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.SetPeerAuthorization, map[event.Field]string{event.RemotePeer: peer, event.Authorization: string(authorization)}))
return err
}
// AcceptInvite accepts a given existing group invite
func (cp *cwtchPeer) AcceptInvite(groupID string) error {
cp.mutex.Lock()
err := cp.Profile.AcceptInvite(groupID)
cp.mutex.Unlock()
if err != nil {
return err
}
cp.eventBus.Publish(event.NewEvent(event.AcceptGroupInvite, map[event.Field]string{event.GroupID: groupID}))
err = cp.JoinServer(cp.Profile.Groups[groupID].GroupServer)
return err
}
// RejectInvite rejects a given group invite.
func (cp *cwtchPeer) RejectInvite(groupID string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
cp.Profile.RejectInvite(groupID)
cp.eventBus.Publish(event.NewEvent(event.RejectGroupInvite, map[event.Field]string{event.GroupID: groupID}))
}
// Listen makes the peer open a listening port to accept incoming connections (and be detectably online)
// Status: Ready for 1.5
func (cp *cwtchPeer) Listen() {
cp.mutex.Lock()
defer cp.mutex.Unlock()
@ -696,96 +616,62 @@ func (cp *cwtchPeer) Listen() {
}
// StartPeersConnections attempts to connect to peer connections
// Status: Ready for 1.5
func (cp *cwtchPeer) StartPeersConnections() {
for _, contact := range cp.GetContacts() {
if !cp.GetContact(contact).IsServer() {
cp.PeerWithOnion(contact)
}
}
//for _, contact := range cp.GetContacts() {
// if !cp.GetContact(contact).IsServer() {
// cp.PeerWithOnion(contact)
// }
//}
}
// StartServerConnections attempts to connect to all server connections
// Status: Ready for 1.5
func (cp *cwtchPeer) StartServerConnections() {
for _, contact := range cp.GetContacts() {
if cp.GetContact(contact).IsServer() {
err := cp.JoinServer(contact)
if err != nil {
// Almost certainly a programming error so print it..
log.Errorf("error joining server %v", err)
}
}
}
}
// SetContactAttribute sets an attribute for the indicated contact and emits an event
func (cp *cwtchPeer) SetContactAttribute(onion string, key string, val string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if contact, ok := cp.Profile.GetContact(onion); ok {
contact.SetAttribute(key, val)
cp.eventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{
event.RemotePeer: onion,
event.Key: key,
event.Data: val,
}))
}
}
// GetContactAttribute gets an attribute for the indicated contact
func (cp *cwtchPeer) GetContactAttribute(onion string, key string) (string, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if contact, ok := cp.Profile.GetContact(onion); ok {
if val, exists := contact.GetAttribute(key); exists {
return val, true
}
}
return "", false
}
// SetGroupAttribute sets an attribute for the indicated group and emits an event
func (cp *cwtchPeer) SetGroupAttribute(gid string, key string, val string) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if group := cp.Profile.GetGroup(gid); group != nil {
group.SetAttribute(key, val)
cp.eventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{
event.GroupID: gid,
event.Key: key,
event.Data: val,
}))
}
}
// GetGroupAttribute gets an attribute for the indicated group
func (cp *cwtchPeer) GetGroupAttribute(gid string, key string) (string, bool) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if group := cp.Profile.GetGroup(gid); group != nil {
if val, exists := group.GetAttribute(key); exists {
return val, true
}
}
return "", false
//for _, contact := range cp.GetContacts() {
// if cp.GetContact(contact).IsServer() {
// err := cp.JoinServer(contact)
// if err != nil {
// // Almost certainly a programming error so print it..
// log.Errorf("error joining server %v", err)
// }
// }
//}
}
// Shutdown kills all connections and cleans up all goroutines for the peer
// Status: Ready for 1.5
func (cp *cwtchPeer) Shutdown() {
cp.mutex.Lock()
defer cp.mutex.Unlock()
cp.shutdown = true
cp.queue.Shutdown()
if cp.storage != nil {
cp.storage.Close()
}
}
func (cp *cwtchPeer) storeMessage(onion string, messageTxt string, sent time.Time) {
if cp.GetContact(onion) == nil {
cp.AddContact(onion, onion, model.AuthUnknown)
// Status: TODO
func (cp *cwtchPeer) storeMessage(handle string, message string, sent time.Time) error {
// TOOD maybe atomize this?
ci, err := cp.FetchConversationInfo(handle)
if err != nil {
err := cp.NewContactConversation(handle, model.DefaultP2PAccessControl(), false)
if err != nil {
return err
}
ci, err = cp.FetchConversationInfo(handle)
if err != nil {
return err
}
}
cp.mutex.Lock()
cp.Profile.AddMessageToContactTimeline(onion, messageTxt, sent)
cp.mutex.Unlock()
defer cp.mutex.Unlock()
return cp.storage.InsertMessage(ci.ID, 0, message, model.Attributes{"ack": event.True, "sent": sent.String()})
}
// ShareFile begins hosting the given serialized manifest
// Status: Ready for 1.5
func (cp *cwtchPeer) ShareFile(fileKey string, serializedManifest string) {
cp.eventBus.Publish(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: fileKey, event.SerializedManifest: serializedManifest}))
}
@ -813,7 +699,7 @@ func (cp *cwtchPeer) eventHandler() {
// 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).
// store the base64 encoded signature for later use
cp.SetContactAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature])
cp.SetConversationAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature])
cp.mutex.Lock()
ok, groupID, message, index := cp.Profile.AttemptDecryption(ciphertext, signature)
@ -867,8 +753,8 @@ func (cp *cwtchPeer) eventHandler() {
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
remotePeer := cp.GetContact(onion)
if remotePeer != nil && remotePeer.Authorization == model.AuthApproved {
conversationInfo, _ := cp.FetchConversationInfo(onion)
if conversationInfo != nil && conversationInfo.Accepted {
scope := attr.IntoScope(scope)
if scope.IsPublic() || scope.IsConversation() {
zone, zpath := attr.ParseZone(path)
@ -965,27 +851,16 @@ func (cp *cwtchPeer) eventHandler() {
// Allow public profile parameters to be added as peer specific attributes...
if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone {
cp.SetContactAttribute(onion, attr.GetPeerScope(path), val)
cp.SetConversationAttribute(onion, attr.Scope(scope).ConstructScopedZonedPath(zone.ConstructZonedPath(path)), val)
}
}
case event.PeerStateChange:
cp.mutex.Lock()
if _, exists := cp.Profile.Contacts[ev.Data[event.RemotePeer]]; exists {
cp.Profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState]
}
cp.state[ev.Data[event.RemotePeer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
case event.ServerStateChange:
cp.mutex.Lock()
// We update both the server contact status, as well as the groups the server belongs to
log.Debugf("Got Server State Change %v", ev)
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.state[ev.Data[event.GroupServer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
default:

323
peer/cwtchprofilestorage.go Normal file
View File

@ -0,0 +1,323 @@
package peer
import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"database/sql"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/log"
)
// StorageKeyType is an interface wrapper around storage key types
type StorageKeyType string
const (
// TypeAttribute for Profile Scoped and Zoned Attributes
TypeAttribute = StorageKeyType("Attribute")
// TypePrivateKey for Profile Private Keys
TypePrivateKey = StorageKeyType("PrivateKey")
// TypePublicKey for Profile Public Keys
TypePublicKey = StorageKeyType("PublicKey")
)
// CwtchProfileStorage encapsulates common datastore requests so as to not pollute the main cwtch profile
// struct with database knowledge
type CwtchProfileStorage struct {
// Note: Statements are thread safe..
// Profile related statements
insertProfileKeyValueStmt *sql.Stmt
selectProfileKeyValueStmt *sql.Stmt
// Conversation related statements
insertConversationStmt *sql.Stmt
selectConversationStmt *sql.Stmt
acceptConversationStmt *sql.Stmt
deleteConversationStmt *sql.Stmt
setConversationAttributesStmt *sql.Stmt
channelInsertStmts map[ChannelID]*sql.Stmt
channelGetMessageStmts map[ChannelID]*sql.Stmt
db *sql.DB
}
type ChannelID struct {
Conversation int
Channel int
}
const insertProfileKeySQLStmt = `insert into profile_kv(KeyType, KeyName, KeyValue) values(?,?,?);`
const selectProfileKeySQLStmt = `select KeyValue from profile_kv where KeyType=(?) and KeyName=(?);`
const insertConversationSQLStmt = `insert into conversations(Handle, Attributes, ACL, Accepted) values(?,?,?,?);`
const selectConversationSQLStmt = `select ID, Handle, Attributes, ACL, Accepted from conversations where Handle=(?);`
const acceptedConversationSQLStmt = `update conversations set Accepted=true where Handle=(?);`
const setConversationAttributesSQLStmt = `update conversations set Attributes=(?) where Handle=(?) ;`
const deleteConversationSQLStmt = `delete from conversations where Handle=(?);`
// createTableConversationMessagesSQLStmt is a template for creating conversation based tables...
const createTableConversationMessagesSQLStmt = `create table if not exists channel_%d_0_chat (ID integer unique primary key autoincrement, Body text, Attributes []byte, Expiry datetime);`
// insertMessageIntoConversationSQLStmt is a template for creating conversation based tables...
const insertMessageIntoConversationSQLStmt = `insert into channel_%d_%d_chat (Body, Attributes) values(?,?);`
// getMessageFromConversationSQLStmt is a template for creating conversation based tables...
const getMessageFromConversationSQLStmt = `select Body, Attributes from channel_%d_%d_chat where ID=(?);`
// NewCwtchProfileStorage constructs a new CwtchProfileStorage from a database. It is also responsible for
// Preparing commonly used SQL Statements
func NewCwtchProfileStorage(db *sql.DB) (*CwtchProfileStorage, error) {
if db == nil {
return nil, errors.New("cannot construct cwtch profile storage with a nil database")
}
insertProfileKeyValueStmt, err := db.Prepare(insertProfileKeySQLStmt)
if err != nil {
log.Errorf("error preparing query: %v %v", insertProfileKeySQLStmt, err)
return nil, err
}
selectProfileKeyStmt, err := db.Prepare(selectProfileKeySQLStmt)
if err != nil {
log.Errorf("error preparing query: %v %v", selectProfileKeySQLStmt, err)
return nil, err
}
insertConversationStmt, err := db.Prepare(insertConversationSQLStmt)
if err != nil {
log.Errorf("error preparing query: %v %v", insertConversationSQLStmt, err)
return nil, err
}
selectConversationStmt, err := db.Prepare(selectConversationSQLStmt)
if err != nil {
log.Errorf("error preparing query: %v %v", selectConversationSQLStmt, err)
return nil, err
}
acceptConversationStmt, err := db.Prepare(acceptedConversationSQLStmt)
if err != nil {
log.Errorf("error preparing query: %v %v", acceptedConversationSQLStmt, err)
return nil, err
}
deleteConversationStmt, err := db.Prepare(deleteConversationSQLStmt)
if err != nil {
log.Errorf("error preparing query: %v %v", deleteConversationSQLStmt, err)
return nil, err
}
setConversationAttributesStmt, err := db.Prepare(setConversationAttributesSQLStmt)
if err != nil {
log.Errorf("error preparing query: %v %v", setConversationAttributesSQLStmt, err)
return nil, err
}
return &CwtchProfileStorage{db: db, insertProfileKeyValueStmt: insertProfileKeyValueStmt, selectProfileKeyValueStmt: selectProfileKeyStmt, insertConversationStmt: insertConversationStmt, selectConversationStmt: selectConversationStmt, acceptConversationStmt: acceptConversationStmt, deleteConversationStmt: deleteConversationStmt, setConversationAttributesStmt: setConversationAttributesStmt, channelInsertStmts: map[ChannelID]*sql.Stmt{}, channelGetMessageStmts: map[ChannelID]*sql.Stmt{}}, nil
}
// StoreProfileKeyValue allows storing of typed Key/Value attribute in the Storage Engine
func (cps *CwtchProfileStorage) StoreProfileKeyValue(keyType StorageKeyType, key string, value []byte) error {
_, err := cps.insertProfileKeyValueStmt.Exec(keyType, key, value)
if err != nil {
log.Errorf("error executing query: %v", err)
return err
}
return nil
}
// LoadProfileKeyValue allows fetching of typed values via a known Key from the Storage Engine
func (cps *CwtchProfileStorage) LoadProfileKeyValue(keyType StorageKeyType, key string) ([]byte, error) {
rows, err := cps.selectProfileKeyValueStmt.Query(keyType, key)
if err != nil {
log.Errorf("error executing query: %v", err)
return nil, err
}
result := rows.Next()
if !result {
return nil, errors.New("no result found")
}
var keyValue []byte
err = rows.Scan(&keyValue)
if err != nil {
log.Errorf("error fetching rows: %v", err)
rows.Close()
return nil, err
}
rows.Close()
return keyValue, nil
}
// NewConversation stores a new conversation in the data store
func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model.Attributes, acl model.AccessControlList, accepted bool) error {
tx, err := cps.db.Begin()
if err != nil {
log.Errorf("error executing transaction: %v", err)
return err
}
result, err := tx.Stmt(cps.insertConversationStmt).Exec(handle, attributes.Serialize(), acl.Serialize(), accepted)
if err != nil {
log.Errorf("error executing transaction: %v", err)
return tx.Rollback()
}
id, err := result.LastInsertId()
if err != nil {
log.Errorf("error executing transaction: %v", err)
return tx.Rollback()
}
_, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id))
if err != nil {
log.Errorf("error executing transaction: %v", err)
return tx.Rollback()
}
return tx.Commit()
}
// GetConversation looks up a particular conversation by handle
func (cps *CwtchProfileStorage) GetConversation(handle string) (*model.Conversation, error) {
rows, err := cps.selectConversationStmt.Query(handle)
if err != nil {
log.Errorf("error executing query: %v", err)
return nil, err
}
result := rows.Next()
if !result {
return nil, errors.New("no result found")
}
var id int
var rhandle string
var acl []byte
var attributes []byte
var accepted bool
err = rows.Scan(&id, &rhandle, &attributes, &acl, &accepted)
if err != nil {
log.Errorf("error fetching rows: %v", err)
rows.Close()
return nil, err
}
rows.Close()
return &model.Conversation{ID: id, Handle: rhandle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
}
// AcceptConversation sets the accepted status of a conversation to true in the backing datastore
func (cps *CwtchProfileStorage) AcceptConversation(handle string) error {
_, err := cps.acceptConversationStmt.Exec(handle)
if err != nil {
log.Errorf("error executing query: %v", err)
return err
}
return nil
}
// DeleteConversation purges the conversation and any associated message history from the conversation store.
func (cps *CwtchProfileStorage) DeleteConversation(handle string) error {
_, err := cps.deleteConversationStmt.Exec(handle)
if err != nil {
log.Errorf("error executing query: %v", err)
return err
}
return nil
}
func (cps *CwtchProfileStorage) SetConversationAttribute(handle string, path attr.ScopedZonedPath, value string) error {
ci, err := cps.GetConversation(handle)
if err != nil {
return err
}
ci.Attributes[path.ToString()] = value
_, err = cps.setConversationAttributesStmt.Exec(ci.Attributes.Serialize(), handle)
if err != nil {
log.Errorf("error executing query: %v", err)
return err
}
return nil
}
func (cps *CwtchProfileStorage) InsertMessage(conversation int, channel int, body string, attributes model.Attributes) error {
channelID := ChannelID{Conversation: conversation, Channel: channel}
_, exists := cps.channelInsertStmts[channelID]
if !exists {
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(insertMessageIntoConversationSQLStmt, conversation, channel))
if err != nil {
log.Errorf("error executing transaction: %v", err)
return err
}
cps.channelInsertStmts[channelID] = conversationStmt
}
_, err := cps.channelInsertStmts[channelID].Exec(body, attributes.Serialize())
if err != nil {
log.Errorf("error inserting message: %v", err)
return err
}
return nil
}
func (cps *CwtchProfileStorage) GetChannelMessage(conversation int, channel int, messageID int) (string, model.Attributes, error) {
channelID := ChannelID{Conversation: conversation, Channel: channel}
_, exists := cps.channelGetMessageStmts[channelID]
if !exists {
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(getMessageFromConversationSQLStmt, conversation, channel))
if err != nil {
log.Errorf("error executing transaction: %v", err)
return "", nil, err
}
cps.channelGetMessageStmts[channelID] = conversationStmt
}
rows, err := cps.channelGetMessageStmts[channelID].Query(messageID)
if err != nil {
log.Errorf("error executing query: %v", err)
return "", nil, err
}
result := rows.Next()
if !result {
return "", nil, errors.New("no result found")
}
// Deserialize the Row
var body string
var attributes []byte
err = rows.Scan(&body, &attributes)
if err != nil {
log.Errorf("error fetching rows: %v", err)
rows.Close()
return "", nil, err
}
rows.Close()
return body, model.DeserializeAttributes(attributes), nil
}
// Close closes the underlying database and prepared statements
func (cps *CwtchProfileStorage) Close() {
if cps.db != nil {
cps.insertProfileKeyValueStmt.Close()
cps.selectProfileKeyValueStmt.Close()
cps.db.Close()
}
}

128
peer/profile_interface.go Normal file
View File

@ -0,0 +1,128 @@
package peer
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
)
// AccessPeeringState provides access to functions relating to the underlying connections of a peer.
type AccessPeeringState interface {
GetPeerState(string) (connections.ConnectionState, bool)
}
// ModifyPeeringState is a meta-interface intended to restrict callers to modify-only access to connection peers
type ModifyPeeringState interface {
BlockUnknownConnections()
AllowUnknownConnections()
PeerWithOnion(string)
JoinServer(string) error
}
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
// and peers.
type ModifyContactsAndPeers interface {
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
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)
}
// 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
type SendMessages interface {
SendMessage(handle string, message string) error
// Deprecated: is unsafe
SendGetValToPeer(string, string, string)
SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, key string)
// TODO
// Deprecated use overlays instead
InviteOnionToGroup(string, string) error
}
// ModifyMessages enables a caller to modify the messages in a timeline
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)
GetIdentity() primitives.Identity
GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager) connections.Engine
AutoHandleEvents(events []event.Type)
Listen()
StartPeersConnections()
StartServerConnections()
Shutdown()
// GetOnion is deprecated. If you find yourself needing to rely on this method it is time
// to consider replacing this with a GetAddress(es) function that can fully expand cwtch beyond the boundaries
// of tor v3 onion services.
// Deprecated
GetOnion() string
// SetScopedZonedAttribute allows the setting of an attribute by scope and zone
// scope.zone.key = value
SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string)
// GetScopedZonedAttribute allows the retrieval of an attribute by scope and zone
// scope.zone.key = value
GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool)
AccessPeeringState
ModifyPeeringState
ReadGroups
ModifyGroups
ReadServers
ModifyServers
SendMessages
ModifyMessages
// New Unified Conversation Interfaces
NewContactConversation(handle string, acl model.AccessControl, accepted bool) error
FetchConversationInfo(handle string) (*model.Conversation, error)
AcceptConversation(handle string) error
SetConversationAttribute(handle string, path attr.ScopedZonedPath, value string) error
GetConversationAttribute(handle string, path attr.ScopedZonedPath) (string, error)
DeleteConversation(handle string) error
GetChannelMessage(converstion int, channel int, id int) (string, model.Attributes, error)
ShareFile(fileKey string, serializedManifest string)
}

29
peer/sql_statements.go Normal file
View File

@ -0,0 +1,29 @@
package peer
import (
"database/sql"
"fmt"
)
// SQLCreateTableProfileKeyValue creates the Profile Key Value Table
const SQLCreateTableProfileKeyValue = `create table if not exists profile_kv (KeyType text, KeyName text, KeyValue blob);`
// SQLCreateTableConversations creates the Profile Key Value Table
const SQLCreateTableConversations = `create table if not exists conversations (ID integer unique primary key autoincrement, Handle text, Attributes blob, ACL blob, Accepted bool);`
// initializeDatabase executes all the sql statements necessary to construct the base of the database.
// db must be open
func initializeDatabase(db *sql.DB) error {
_, err := db.Exec(SQLCreateTableProfileKeyValue)
if err != nil {
return fmt.Errorf("error On Executing Query: %v %v", SQLCreateTableProfileKeyValue, err)
}
_, err = db.Exec(SQLCreateTableConversations)
if err != nil {
return fmt.Errorf("error On Executing Query: %v %v", SQLCreateTableConversations, err)
}
return nil
}

133
peer/storage.go Normal file
View File

@ -0,0 +1,133 @@
package peer
import (
"crypto/rand"
"database/sql"
"fmt"
"git.openprivacy.ca/openprivacy/log"
// Import SQL Cipher
_ "github.com/mutecomm/go-sqlcipher/v4"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
)
const versionFile = "VERSION"
const version = "2"
const saltFile = "SALT"
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
var salt [128]byte
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
return [32]byte{}, salt, err
}
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr, salt, nil
}
// createKey derives a key from a password and salt
func createKey(password string, salt []byte) [32]byte {
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr
}
func initV2Directory(directory, password string) ([32]byte, [128]byte, error) {
os.Mkdir(directory, 0700)
key, salt, err := CreateKeySalt(password)
if err != nil {
log.Errorf("Could not create key for profile store from password: %v\n", err)
return [32]byte{}, [128]byte{}, err
}
if err = ioutil.WriteFile(path.Join(directory, versionFile), []byte(version), 0600); err != nil {
log.Errorf("Could not write version file: %v", err)
return [32]byte{}, [128]byte{}, err
}
if err = ioutil.WriteFile(path.Join(directory, saltFile), salt[:], 0600); err != nil {
log.Errorf("Could not write salt file: %v", err)
return [32]byte{}, [128]byte{}, err
}
return key, salt, nil
}
func openEncryptedDatabase(profileDirectory string, password string) (*sql.DB, error) {
salt, err := ioutil.ReadFile(path.Join(profileDirectory, saltFile))
if err != nil {
return nil, err
}
key := createKey(password, salt)
dbPath := filepath.Join(profileDirectory, "db")
dbname := fmt.Sprintf("%v?_pragma_key=x'%x'&_pragma_cipher_page_size=8192", dbPath, key)
db, err := sql.Open("sqlite3", dbname)
if err != nil {
log.Errorf("could not open encrypted database", err)
return nil, err
}
return db, nil
}
// CreateEncryptedStorePeer creates a *new* Cwtch Profile backed by an encrypted datastore
func CreateEncryptedStorePeer(profileDirectory string, name string, password string) (CwtchPeer, error) {
log.Debugf("Initializing Encrypted Storage Directory")
_, _, err := initV2Directory(profileDirectory, password)
if err != nil {
return nil, err
}
log.Debugf("Opening Encrypted Database")
db, err := openEncryptedDatabase(profileDirectory, password)
if db == nil || err != nil {
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
}
log.Debugf("Initializing Database")
err = initializeDatabase(db)
if err != nil {
db.Close()
return nil, err
}
log.Debugf("Creating Cwtch Profile Backed By Encrypted Database")
cps, err := NewCwtchProfileStorage(db)
if err != nil {
db.Close()
return nil, err
}
return NewProfileWithEncryptedStorage(name, cps), nil
}
// FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database
func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) {
log.Debugf("Loading Encrypted Profile")
db, err := openEncryptedDatabase(profileDirectory, password)
if db == nil || err != nil {
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
}
log.Debugf("Initializing Profile from Encrypted Storage")
cps, err := NewCwtchProfileStorage(db)
if err != nil {
db.Close()
return nil, err
}
return FromEncryptedStorage(cps), nil
}

View File

@ -139,13 +139,13 @@ func TestCwtchPeerIntegration(t *testing.T) {
// ***** cwtchPeer setup *****
fmt.Println("Creating Alice...")
app.CreatePeer("alice", "asdfasdf")
app.CreateTaggedPeer("alice", "asdfasdf", "test")
fmt.Println("Creating Bob...")
app.CreatePeer("bob", "asdfasdf")
app.CreateTaggedPeer("bob", "asdfasdf", "test")
fmt.Println("Creating Carol...")
app.CreatePeer("carol", "asdfasdf")
app.CreateTaggedPeer("carol", "asdfasdf", "test")
alice := utils.WaitGetPeer(app, "alice")
fmt.Println("Alice created:", alice.GetOnion())
@ -200,14 +200,12 @@ func TestCwtchPeerIntegration(t *testing.T) {
// Need to add contact else SetContactAuth fails on peer peer doesnt exist
// Normal flow would be Bob app monitors for the new connection (a new connection state change to Auth
// and the adds the user to peer, and then approves or blocks it
bob.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
bob.NewContactConversation("alice?", model.DefaultP2PAccessControl(), true)
bob.AddServer(string(serverKeyBundle))
bob.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
waitForPeerPeerConnection(t, alice, carol)
carol.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
carol.NewContactConversation("alice?", model.DefaultP2PAccessControl(), true)
carol.AddServer(string(serverKeyBundle))
carol.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
fmt.Println("Alice and Bob getVal public.name...")
@ -221,26 +219,26 @@ func TestCwtchPeerIntegration(t *testing.T) {
// Probably related to latency/throughput problems in the underlying tor network.
time.Sleep(30 * time.Second)
aliceName, exists := bob.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || aliceName != "Alice" {
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v\n", exists)
aliceName, err := bob.GetConversationAttribute(alice.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || aliceName != "Alice" {
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v: %v\n", aliceName, err)
}
fmt.Printf("Bob has alice's name as '%v'\n", aliceName)
bobName, exists := alice.GetContactAttribute(bob.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || bobName != "Bob" {
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name\n")
bobName, err := alice.GetConversationAttribute(bob.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || bobName != "Bob" {
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name %v: %v \n", bobName, err)
}
fmt.Printf("Alice has bob's name as '%v'\n", bobName)
aliceName, exists = carol.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || aliceName != "Alice" {
t.Fatalf("carol GetKeyVal error for alice peer.name %v\n", exists)
aliceName, err = carol.GetConversationAttribute(alice.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || aliceName != "Alice" {
t.Fatalf("carol GetKeyVal error for alice peer.name %v: %v\n", aliceName, err)
}
carolName, exists := alice.GetContactAttribute(carol.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || carolName != "Carol" {
t.Fatalf("alice GetKeyVal error, carol peer.name\n")
carolName, err := alice.GetConversationAttribute(carol.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || carolName != "Carol" {
t.Fatalf("alice GetKeyVal error, carol peer.name: %v: %v\n", carolName, err)
}
fmt.Printf("Alice has carol's name as '%v'\n", carolName)

View File

@ -0,0 +1,127 @@
package encryptedstorage
import (
"crypto/rand"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/utils"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"encoding/base64"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
mrand "math/rand"
"os"
"path"
"path/filepath"
"testing"
"time"
)
func TestEncryptedStorage(t *testing.T) {
os.Mkdir("tordir", 0700)
dataDir := filepath.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
_, err := rand.Read(key)
if err != nil {
panic(err)
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "..", "tor"), controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
cwtchDir := path.Join(".", "encrypted_storage_profiles")
os.RemoveAll(cwtchDir)
os.Mkdir(cwtchDir, 0700)
fmt.Println("Creating Alice...")
log.SetLevel(log.LevelDebug)
defer acn.Close()
acn.WaitTillBootstrapped()
app := app2.NewApp(acn, cwtchDir)
app.CreateTaggedPeer("alice", "password", constants.ProfileTypeV1Password)
app.CreateTaggedPeer("bob", "password", constants.ProfileTypeV1Password)
alice := utils.WaitGetPeer(app, "alice")
bob := utils.WaitGetPeer(app, "bob")
alice.Listen()
bob.Listen()
// To keep this large test organized, we will break it down into sub tests...
subTestAliceAddAndDeleteBob(t, alice, bob)
alice.PeerWithOnion(bob.GetOnion())
time.Sleep(time.Second * 30)
alice.SendMessage(bob.GetOnion(), "Hello Bob")
time.Sleep(time.Second * 30)
ci,_ := bob.FetchConversationInfo(alice.GetOnion())
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
if body != "Hello Bob" || err != nil {
t.Fatalf("unexpected message in conversation channel %v %v", body, err)
} else {
t.Logf("succesfully found message in conversation channel %v", body)
}
}
// Sub Test testing that Alice can add Bob, delete the conversation associated with Bob, and then add Bob again
// Under a different conversation identifier.
func subTestAliceAddAndDeleteBob(t *testing.T, alice peer.CwtchPeer, bob peer.CwtchPeer) {
t.Logf("Starting Sub Test AliceAddAndDeleteBob")
alice.NewContactConversation(bob.GetOnion(), model.AccessControl{Read: true, Append: true, Blocked: false}, true)
// Test Basic Fetching
bobCI, err := alice.FetchConversationInfo(bob.GetOnion())
if bobCI == nil || err != nil {
t.Fatalf("alice should have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
} else {
t.Logf("Bobs Conversation Info fetched successfully: %v", bobCI)
}
oldID := bobCI.ID
alice.DeleteConversation(bob.GetOnion())
// Test Basic Fetching
bobCI, err = alice.FetchConversationInfo(bob.GetOnion())
if bobCI != nil {
t.Fatalf("alice should **not** have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
} else {
t.Logf("expected error fetching deleted conversation info: %v", err)
}
alice.NewContactConversation(bob.GetOnion(), model.AccessControl{Read: true, Append: true, Blocked: false}, true)
// Test Basic Fetching
bobCI, err = alice.FetchConversationInfo(bob.GetOnion())
if bobCI == nil || err != nil {
t.Fatalf("alice should have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
} else {
t.Logf("Bobs Conversation Info fetched successfully: %v", bobCI)
}
if oldID == bobCI.ID {
t.Fatalf("bob should have a different conversation ID. Instead it is the same as the old conversation id, meaning something has gone wrong in the storage engine.")
}
}