diff --git a/app/app.go b/app/app.go index 51d38a9..d9a569e 100644 --- a/app/app.go +++ b/app/app.go @@ -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() } diff --git a/go.mod b/go.mod index 324ff8a..9340f4f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index d628ee0..38aef9b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/model/conversation.go b/model/conversation.go new file mode 100644 index 0000000..558709c --- /dev/null +++ b/model/conversation.go @@ -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 +} diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 254a15b..47ee4c3 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -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: diff --git a/peer/cwtchprofilestorage.go b/peer/cwtchprofilestorage.go new file mode 100644 index 0000000..f2f96f0 --- /dev/null +++ b/peer/cwtchprofilestorage.go @@ -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() + } +} diff --git a/peer/profile_interface.go b/peer/profile_interface.go new file mode 100644 index 0000000..46634e9 --- /dev/null +++ b/peer/profile_interface.go @@ -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) +} diff --git a/peer/sql_statements.go b/peer/sql_statements.go new file mode 100644 index 0000000..42ae396 --- /dev/null +++ b/peer/sql_statements.go @@ -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 +} diff --git a/peer/storage.go b/peer/storage.go new file mode 100644 index 0000000..1aecd7d --- /dev/null +++ b/peer/storage.go @@ -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 +} diff --git a/testing/cwtch_peer_server_integration_test.go b/testing/cwtch_peer_server_integration_test.go index 6969416..8d23ce5 100644 --- a/testing/cwtch_peer_server_integration_test.go +++ b/testing/cwtch_peer_server_integration_test.go @@ -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) diff --git a/testing/encryptedstorage/encrypted_storage_integration_test.go b/testing/encryptedstorage/encrypted_storage_integration_test.go new file mode 100644 index 0000000..47251f4 --- /dev/null +++ b/testing/encryptedstorage/encrypted_storage_integration_test.go @@ -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.") + } + +}