cwtch/peer/cwtch_peer.go

1220 lines
52 KiB
Go
Raw Normal View History

package peer
2018-03-09 20:44:13 +00:00
import (
"crypto/rand"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/protocol/groups"
"encoding/base64"
2019-02-03 01:18:33 +00:00
"encoding/json"
2018-03-30 21:16:51 +00:00
"errors"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519"
"runtime"
"strconv"
"strings"
2018-03-09 20:44:13 +00:00
"sync"
2019-01-22 19:11:25 +00:00
"time"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/protocol/files"
"git.openprivacy.ca/openprivacy/log"
2018-03-09 20:44:13 +00:00
)
const lastKnownSignature = "LastKnowSignature"
var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true,
event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeer: true,
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToPeerError: true, event.SendMessageToGroupError: true,
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true,
event.ManifestSizeReceived: true, event.ManifestReceived: true}
// DefaultEventsToHandle specifies which events will be subscribed to
// when a peer has its Init() function called
var DefaultEventsToHandle = []event.Type{
event.EncryptedGroupMessage,
event.NewMessageFromPeer,
event.PeerAcknowledgement,
event.NewGroupInvite,
event.PeerError,
event.SendMessageToGroupError,
event.NewGetValMessageFromPeer,
event.ProtocolEngineStopped,
event.RetryServerRequest,
}
2018-10-06 03:50:55 +00:00
// cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch cwtchPeer
type cwtchPeer struct {
mutex sync.Mutex
shutdown bool
listenStatus bool
storage *CwtchProfileStorage
state map[string]connections.ConnectionState
2019-01-04 21:44:21 +00:00
queue event.Queue
eventBus event.Manager
2018-03-09 20:44:13 +00:00
}
// GenerateProtocolEngine
// Status: New in 1.5
func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager) (connections.Engine, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
conversations, _ := cp.storage.FetchConversations()
authorizations := make(map[string]model.Authorization)
for _, conversation := range conversations {
if tor.IsValidHostname(conversation.Handle) {
if conversation.ACL[conversation.Handle].Blocked {
authorizations[conversation.Handle] = model.AuthBlocked
} else {
authorizations[conversation.Handle] = model.AuthApproved
}
}
}
privateKey, err := cp.storage.LoadProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey")
if err != nil {
log.Errorf("error loading private key from storage")
return nil, err
}
publicKey, err := cp.storage.LoadProfileKeyValue(TypePublicKey, "Ed25519PublicKey")
if err != nil {
log.Errorf("error loading public key from storage")
return nil, err
}
identity := primitives.InitializeIdentity("", (*ed25519.PrivateKey)(&privateKey), (*ed25519.PublicKey)(&publicKey))
return connections.NewProtocolEngine(identity, privateKey, acn, bus, authorizations), nil
}
// SendScopedZonedGetValToContact
// Status: No change in 1.5
func (cp *cwtchPeer) SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, path string) {
ci, err := cp.GetConversationInfo(conversationID)
if err == nil {
ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, ci.Handle, event.Scope, string(scope), event.Path, string(zone.ConstructZonedPath(path)))
cp.eventBus.Publish(ev)
} else {
log.Errorf("Error sending scoped zone to contact %v %v", conversationID, err)
}
2021-10-07 22:40:25 +00:00
}
// GetScopedZonedAttribute
// Status: Ready for 1.5
2021-10-07 22:40:25 +00:00
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))
2021-10-07 22:40:25 +00:00
log.Debugf("looking up attribute %v %v %v (%v)", scope, zone, key, scopedZonedKey)
value, err := cp.storage.LoadProfileKeyValue(TypeAttribute, scopedZonedKey.ToString())
2021-10-07 22:40:25 +00:00
log.Debugf("found [%v] %v", string(value), err)
if err != nil {
return "", false
2021-10-07 22:40:25 +00:00
}
return string(value), true
2021-10-07 22:40:25 +00:00
}
// SetScopedZonedAttribute
// Status: Ready for 1.5
2021-10-07 22:40:25 +00:00
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))
2021-10-07 22:40:25 +00:00
log.Debugf("storing attribute: %v = %v", scopedZonedKey, value)
err := cp.storage.StoreProfileKeyValue(TypeAttribute, scopedZonedKey.ToString(), []byte(value))
if err != nil {
log.Errorf("error setting attribute %v")
}
2021-10-07 22:40:25 +00:00
}
// 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(conversation int, message string) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
// We assume we are sending to a Contact.
conversationInfo, err := cp.storage.GetConversation(conversation)
// 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 conversationInfo != nil && err == nil {
if tor.IsValidHostname(conversationInfo.Handle) {
ev := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationInfo.ID), event.RemotePeer: conversationInfo.Handle, event.Data: message})
onion, _ := cp.storage.LoadProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString())
// For p2p messages we store the event id of the message as the "signature" we can then look this up in the database later for acks
err := cp.storage.InsertMessage(conversationInfo.ID, 0, message, model.Attributes{constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}, ev.EventID, model.CalculateContentHash(string(onion), message))
if err != nil {
return err
}
cp.eventBus.Publish(ev)
} else {
group, err := cp.constructGroupFromConversation(conversationInfo)
if err != nil {
log.Errorf("error constructing group")
return err
}
privateKey, err := cp.storage.LoadProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey")
if err != nil {
log.Errorf("error loading private key from storage")
return err
}
publicKey, err := cp.storage.LoadProfileKeyValue(TypePublicKey, "Ed25519PublicKey")
if err != nil {
log.Errorf("error loading public key from storage")
return err
}
identity := primitives.InitializeIdentity("", (*ed25519.PrivateKey)(&privateKey), (*ed25519.PublicKey)(&publicKey))
ct, sig, dm, err := model.EncryptMessageToGroup(message, identity, group)
if err != nil {
return err
}
// Insert the Group Message
err = cp.storage.InsertMessage(conversationInfo.ID, 0, dm.Text, model.Attributes{constants.AttrAck: constants.False, "PreviousSignature": base64.StdEncoding.EncodeToString(dm.PreviousMessageSig), "Author": dm.Onion, constants.AttrSentTimestamp: strconv.Itoa(int(dm.Timestamp))}, base64.StdEncoding.EncodeToString(sig), model.CalculateContentHash(dm.Onion, dm.Text))
if err == nil {
ev := event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationInfo.ID), event.GroupID: conversationInfo.Handle, event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)})
cp.eventBus.Publish(ev)
} else {
return err
}
}
return nil
}
return fmt.Errorf("error sending message to conversation %v", err)
}
// UpdateMessageFlags
// Status: TODO
2021-06-08 22:29:04 +00:00
func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
log.Debugf("Updating Flags for %v %v %v", handle, mIdx, flags)
//cp.Profile.UpdateMessageFlags(handle, mIdx, flags)
2021-06-08 22:29:04 +00:00
cp.eventBus.Publish(event.NewEvent(event.UpdateMessageFlags, map[event.Field]string{event.Handle: handle, event.Index: strconv.Itoa(mIdx), event.Flags: strconv.FormatUint(flags, 2)}))
}
// BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname
// known to peer.
// 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{}))
}
// NewProfileWithEncryptedStorage instantiates a new Cwtch Profile from encrypted storage
func NewProfileWithEncryptedStorage(name string, cps *CwtchProfileStorage) CwtchPeer {
cp := new(cwtchPeer)
cp.shutdown = false
cp.storage = cps
2021-11-17 23:34:14 +00:00
cp.queue = event.NewQueue()
cp.state = make(map[string]connections.ConnectionState)
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
// Store all the Necessary Base Attributes In The Database
cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name)
cp.storage.StoreProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString(), []byte(tor.GetTorV3Hostname(pub)))
cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", priv)
cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", pub)
return cp
}
// FromEncryptedStorage loads an existing Profile from Encrypted Storage
func FromEncryptedStorage(cps *CwtchProfileStorage) CwtchPeer {
cp := new(cwtchPeer)
cp.shutdown = false
cp.storage = cps
2021-11-17 23:34:14 +00:00
cp.queue = event.NewQueue()
cp.state = make(map[string]connections.ConnectionState)
2021-11-17 23:34:14 +00:00
// At some point we may want to populate caches here, for now we will assume hitting the
// database directly is tolerable
2018-10-06 03:50:55 +00:00
return cp
2018-10-15 00:59:53 +00:00
}
2021-11-17 23:34:14 +00:00
// ImportLegacyProfile generates a new peer from a profile.
// Deprecated - Only to be used for importing new profiles
2021-11-17 23:34:14 +00:00
func ImportLegacyProfile(profile *model.Profile, cps *CwtchProfileStorage) CwtchPeer {
2018-10-06 03:50:55 +00:00
cp := new(cwtchPeer)
cp.shutdown = false
cp.storage = cps
2021-11-17 23:34:14 +00:00
cp.queue = event.NewQueue()
// Store all the Necessary Base Attributes In The Database
cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, profile.Name)
2021-11-17 23:34:14 +00:00
cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, profile.Name)
cp.storage.StoreProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString(), []byte(tor.GetTorV3Hostname(profile.Ed25519PublicKey)))
cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", profile.Ed25519PrivateKey)
cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", profile.Ed25519PublicKey)
2021-11-17 23:34:14 +00:00
for k, v := range profile.Attributes {
parts := strings.SplitN(k, ".", 2)
if len(parts) != 2 {
scope := attr.IntoScope(parts[0])
zone, path := attr.ParseZone(parts[1])
cp.SetScopedZonedAttribute(scope, zone, path, v)
} else {
log.Errorf("could not import legacy style attribute %v", k)
}
}
for _, contact := range profile.Contacts {
var conversationID int
var err error
if contact.Authorization == model.AuthApproved {
conversationID, err = cp.NewContactConversation(contact.Onion, model.DefaultP2PAccessControl(), true)
} else if contact.Authorization == model.AuthBlocked {
conversationID, err = cp.NewContactConversation(contact.Onion, model.AccessControl{Blocked: true, Read: false, Append: false}, true)
} else {
conversationID, err = cp.NewContactConversation(contact.Onion, model.DefaultP2PAccessControl(), false)
}
if err == nil {
for key, value := range contact.Attributes {
switch key {
case "name":
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)), value)
case "local.profile.name":
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)), value)
case event.SaveHistoryKey:
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)), value)
case string(model.BundleType):
cp.AddServer(value)
case string(model.KeyTypeTokenOnion):
//ignore
case string(model.KeyTypeServerOnion):
// ignore
case string(model.KeyTypePrivacyPass):
// ignore
default:
log.Errorf("could not import conversation attribute %v %v", key, value)
}
}
for _, message := range contact.Timeline.GetMessages() {
// By definition anything stored in legacy timelines in acknowledged
attr := model.Attributes{constants.AttrAck: event.True, constants.AttrSentTimestamp: message.Timestamp.Format(time.RFC3339Nano)}
if message.Flags&0x01 == 0x01 {
attr[constants.AttrRejected] = event.True
}
if message.Flags&0x02 == 0x02 {
attr[constants.AttrDownloaded] = event.True
}
cp.storage.InsertMessage(conversationID, 0, message.Message, attr, model.GenerateRandomID(), model.CalculateContentHash(message.PeerID, message.Message))
}
}
}
for _, group := range profile.Groups {
invite, err := group.Invite()
if err == nil {
// Automatically grab all the important fields...
// Including Name...
conversationID, err := cp.ImportGroup(invite)
if err == nil {
for _, message := range group.Timeline.GetMessages() {
// By definition anything stored in legacy timelines in acknowledged
attr := model.Attributes{constants.AttrAck: event.True, constants.AttrSentTimestamp: message.Timestamp.Format(time.RFC3339Nano)}
if message.Flags&0x01 == 0x01 {
attr[constants.AttrRejected] = event.True
}
if message.Flags&0x02 == 0x02 {
attr[constants.AttrDownloaded] = event.True
}
cp.storage.InsertMessage(conversationID, 0, message.Message, attr, base64.StdEncoding.EncodeToString(message.Signature), model.CalculateContentHash(message.PeerID, message.Message))
}
}
}
}
2018-10-06 03:50:55 +00:00
return cp
2018-03-09 20:44:13 +00:00
}
2018-10-06 03:50:55 +00:00
// Init instantiates a cwtchPeer
// Status: Ready for 1.5
func (cp *cwtchPeer) Init(eventBus event.Manager) {
cp.InitForEvents(eventBus, DefaultEventsToHandle)
// Upgrade the Cwtch Peer if necessary
// It would be nice to do these checks in the storage engine itself, but it is easier to do them here
// rather than duplicating the logic to construct/reconstruct attributes in storage engine...
2021-10-26 21:48:26 +00:00
// TODO: Remove these checks after Cwtch ~1.5 storage engine is implemented
// TODO: Move this to import script
//if _, exists := cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name); !exists {
// // If public.profile.name does not exist, and we have an existing public.name then:
// // set public.profile.name from public.name
// // set local.profile.name from public.name
// if name, exists := cp.GetAttribute(attr.GetPublicScope(constants.Name)); exists {
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name)
// } else {
// // Otherwise check if local.name exists and set it from that
// // If not, then check the very old unzoned, unscoped name.
// // If not, then set directly from Profile.Name...
// if name, exists := cp.Profile.GetAttribute(attr.GetLocalScope(constants.Name)); exists {
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name)
// } else if name, exists := cp.Profile.GetAttribute(constants.Name); exists {
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name)
// } else {
// // Profile.Name is very deprecated at this point...
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, cp.Profile.Name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, cp.Profile.Name)
// }
// }
//}
2021-10-26 21:48:26 +00:00
// At this point we can safely assume that public.profile.name exists
localName, _ := cp.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
publicName, _ := cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
if localName != publicName {
cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, publicName)
}
// At this point we can safely assume that public.profile.name exists AND is consistent with
// local.profile.name - regardless of whatever Cwtch version we have upgraded from. This will
// be important after Cwtch 1.5 when we purge all previous references to local.profile.name and
// profile-> name - and remove all name processing code from libcwtch-go.
// If local.profile.tag does not exist then set it from deprecated GetAttribute
//if _, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag); !exists {
// if tag, exists := cp.Profile.GetAttribute(constants.Tag); exists {
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag)
// } else {
// // Assume a default password, which will allow the older profile to have it's password reset by the UI
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, constants.ProfileTypeV1DefaultPassword)
// }
//}
}
// InitForEvents
// Status: Ready for 1.5
func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) {
2019-01-04 21:44:21 +00:00
go cp.eventHandler()
cp.eventBus = eventBus
cp.AutoHandleEvents(toBeHandled)
}
// 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 {
cp.eventBus.Subscribe(ev, cp.queue)
} else {
log.Errorf("Peer asked to autohandle event it cannot: %v\n", ev)
}
}
2018-03-09 20:44:13 +00:00
}
// ImportGroup initializes a group from an imported source rather than a peer invite
// Status: TODO
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (int, error) {
gci, err := model.ValidateInvite(exportedInvite)
if err != nil {
return -1, err
}
groupConversationID, err := cp.NewContactConversation(gci.GroupID, model.DefaultP2PAccessControl(), true)
if err == nil {
cp.SetConversationAttribute(groupConversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)), gci.GroupID)
cp.SetConversationAttribute(groupConversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupServer)), gci.ServerHost)
cp.SetConversationAttribute(groupConversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupKey)), base64.StdEncoding.EncodeToString(gci.SharedKey))
cp.SetConversationAttribute(groupConversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.Name)), gci.GroupName)
cp.eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(groupConversationID), event.GroupServer: gci.ServerHost, event.GroupInvite: exportedInvite}))
}
return groupConversationID, 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) (int, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
conversationID, err := cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, model.AccessControlList{handle: acl}, accepted)
cp.eventBus.Publish(event.NewEvent(event.ContactCreated, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationID), event.RemotePeer: handle}))
return conversationID, err
}
// 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(id int) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.AcceptConversation(id)
}
// BlockConversation 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) BlockConversation(id int) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
ci, err := cp.storage.GetConversation(id)
if err != nil {
return err
}
// p2p conversations have a single ACL referencing the remote peer. Set this to blocked...
ci.ACL[ci.Handle] = model.AccessControl{Blocked: true, Read: false, Append: false}
// Send an event in any case to block the protocol engine...
// TODO at some point in the future engine needs to understand ACLs not just legacy auth status
cp.eventBus.Publish(event.NewEvent(event.SetPeerAuthorization, map[event.Field]string{event.RemotePeer: ci.Handle, event.Authorization: string(model.AuthBlocked)}))
return cp.storage.SetConversationACL(id, ci.ACL)
}
func (cp *cwtchPeer) FetchConversations() ([]*model.Conversation, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.FetchConversations()
}
func (cp *cwtchPeer) GetConversationInfo(conversation int) (*model.Conversation, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.GetConversation(conversation)
}
// 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.GetConversationByHandle(handle)
}
// DeleteConversation purges all data about the conversation, including message timelines, referenced by the handle
func (cp *cwtchPeer) DeleteConversation(id int) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.DeleteConversation(id)
}
// SetConversationAttribute sets the conversation attribute at path to value
func (cp *cwtchPeer) SetConversationAttribute(id int, path attr.ScopedZonedPath, value string) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
log.Debugf("setting %v %v on conversation %v", path, value, id)
return cp.storage.SetConversationAttribute(id, path, value)
}
// GetConversationAttribute is a shortcut method for retrieving the value of a given path
func (cp *cwtchPeer) GetConversationAttribute(id int, path attr.ScopedZonedPath) (string, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
ci, err := cp.storage.GetConversation(id)
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(), id)
}
return val, nil
}
// GetChannelMessage returns a message from a conversation channel referenced by the absolute ID.
// Note: This should note be used to index a list as the ID is not expected to be tied to absolute position
// in the table (e.g. deleted messages, expired messages, etc.)
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)
}
// GetChannelMessageCount returns the absolute number of messages in a given conversation channel
func (cp *cwtchPeer) GetChannelMessageCount(conversation int, channel int) (int, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.GetChannelMessageCount(conversation, channel)
}
// GetMostRecentMessages returns a selection of messages, ordered by most recently inserted
func (cp *cwtchPeer) GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.GetMostRecentMessages(conversation, channel, offset, limit)
}
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
// Status: TODO change server handle to conversation id...?
func (cp *cwtchPeer) StartGroup(name string, server string) (int, error) {
group, err := model.NewGroup(server)
2019-02-03 01:18:33 +00:00
if err == nil {
conversationID, err := cp.NewContactConversation(group.GroupID, model.DefaultP2PAccessControl(), true)
if err != nil {
return -1, err
2019-02-03 01:18:33 +00:00
}
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)), group.GroupID)
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupServer)), group.GroupServer)
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupKey)), base64.StdEncoding.EncodeToString(group.GroupKey[:]))
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.Name)), name)
cp.eventBus.Publish(event.NewEvent(event.GroupCreated, map[event.Field]string{
event.ConversationID: strconv.Itoa(conversationID),
event.GroupID: group.GroupID,
event.GroupServer: group.GroupServer,
}))
return conversationID, nil
2019-02-03 01:18:33 +00:00
}
log.Errorf("error creating group: %v", err)
return -1, err
}
2020-07-23 21:40:44 +00:00
// 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: Ready for 1.5
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
conversationInfo, _ := cp.FetchConversationInfo(onion)
if conversationInfo == nil {
_, err := cp.NewContactConversation(onion, model.DefaultP2PAccessControl(), true)
if err != nil {
return err
}
}
conversationInfo, err = cp.FetchConversationInfo(onion)
if conversationInfo != nil && err == nil {
ab := keyBundle.AttributeBundle()
for k, v := range ab {
val, exists := conversationInfo.Attributes[k]
if exists {
if val != v {
// this is inconsistent!
return model.InconsistentKeyBundleError
}
}
// we haven't seen this key associated with the server before
}
// // 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 save all the keys
for k, v := range ab {
cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(k)), v)
}
cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.BundleType))), serverSpecification)
} else {
return err
}
return nil
}
return nil
}
2020-12-17 01:40:03 +00:00
// GetServers returns an unordered list of servers
// Status: TODO
2020-12-17 01:40:03 +00:00
func (cp *cwtchPeer) GetServers() []string {
var servers []string
return servers
}
// GetOnion
// Status: Deprecated in 1.5
func (cp *cwtchPeer) GetOnion() string {
cp.mutex.Lock()
defer cp.mutex.Unlock()
onion, _ := cp.storage.LoadProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString())
return string(onion)
}
// GetPeerState
// Status: Ready for 1.5
func (cp *cwtchPeer) GetPeerState(handle string) connections.ConnectionState {
cp.mutex.Lock()
defer cp.mutex.Unlock()
if state, ok := cp.state[handle]; ok {
return state
}
return connections.DISCONNECTED
}
// PeerWithOnion initiates a request to the Protocol Engine to set up Cwtch Session with a given tor v3 onion
// address.
2019-07-17 19:10:52 +00:00
func (cp *cwtchPeer) PeerWithOnion(onion string) {
2019-01-21 20:08:03 +00:00
cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion}))
2018-03-09 20:44:13 +00:00
}
// InviteOnionToGroup kicks off the invite process
// Status: TODO
func (cp *cwtchPeer) SendInviteToConversation(conversationID int, inviteConversationID int) error {
var invite model.MessageWrapper
inviteConversationInfo, err := cp.GetConversationInfo(inviteConversationID)
if inviteConversationInfo == nil || err != nil {
return err
}
// groupServer, isGroup := conversationInfo.Attributes[event.GroupServer]; isGroup {
//bundle, _ := cp.Get(profile.GetGroup(target).GroupServer).GetAttribute(string(model.BundleType))
//inviteStr, err := profile.GetGroup(target).Invite()
//if err == nil {
// invite = model.MessageWrapper{Overlay: 101, Data: fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString([]byte(bundle)), inviteStr)}
//}
if tor.IsValidHostname(inviteConversationInfo.Handle) {
invite = model.MessageWrapper{Overlay: 100, Data: inviteConversationInfo.Handle}
} else {
// Reconstruct Group
groupID, ok := inviteConversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]
if !ok {
return errors.New("group structure is malformed - no id")
}
groupServer, ok := inviteConversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupServer)).ToString()]
if !ok {
return errors.New("group structure is malformed - no server")
}
groupKeyBase64, ok := inviteConversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupKey)).ToString()]
if !ok {
return errors.New("group structure is malformed - no key")
}
groupName, ok := inviteConversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.Name)).ToString()]
if !ok {
return errors.New("group structure is malformed - no name")
}
groupKey, err := base64.StdEncoding.DecodeString(groupKeyBase64)
if err != nil {
return errors.New("malformed group key")
}
var groupKeyFixed = [32]byte{}
copy(groupKeyFixed[:], groupKey[:])
group := model.Group{
GroupID: groupID,
GroupName: groupName,
GroupKey: groupKeyFixed,
GroupServer: groupServer,
}
groupInvite, err := group.Invite()
if err != nil {
return errors.New("group invite is malformed")
}
serverInfo, err := cp.FetchConversationInfo(groupServer)
if err != nil {
return errors.New("unknown server associated with group")
}
bundle, exists := serverInfo.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.BundleType))).ToString()]
if !exists {
return errors.New("server bundle not found")
}
invite = model.MessageWrapper{Overlay: 101, Data: fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString([]byte(bundle)), groupInvite)}
}
inviteBytes, err := json.Marshal(invite)
if err != nil {
log.Errorf("malformed invite: %v", err)
return err
2019-01-04 21:44:21 +00:00
}
return cp.SendMessage(conversationID, string(inviteBytes))
}
const serverPrefix = "server:"
const tofuBundlePrefix = "tofubundle:"
const groupPrefix = "torv3"
const importBundlePrefix = "importBundle"
func (cp *cwtchPeer) ImportBundle(importString string) error {
if strings.HasPrefix(importString, tofuBundlePrefix) {
bundle := strings.Split(importString, "||")
if len(bundle) == 2 {
err := cp.ImportBundle(bundle[0][len(tofuBundlePrefix):])
// if the server import failed then abort the whole process..
if err != nil && !strings.HasSuffix(err.Error(), "success") {
return ConstructResponse(importBundlePrefix, err.Error())
}
return cp.ImportBundle(bundle[1])
}
} else if strings.HasPrefix(importString, serverPrefix) {
// Server Key Bundles are prefixed with
bundle, err := base64.StdEncoding.DecodeString(importString[len(serverPrefix):])
if err == nil {
if err = cp.AddServer(string(bundle)); err != nil {
return ConstructResponse(importBundlePrefix, err.Error())
}
return ConstructResponse(importBundlePrefix, "success")
}
return ConstructResponse(importBundlePrefix, err.Error())
} else if strings.HasPrefix(importString, groupPrefix) {
//eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA==
if _, err := cp.ImportGroup(importString); err != nil {
return ConstructResponse(importBundlePrefix, err.Error())
}
return ConstructResponse(importBundlePrefix, "success")
}
return ConstructResponse(importBundlePrefix, "invalid_group_invite_prefix")
2018-03-30 21:16:51 +00:00
}
// JoinServer manages a new server connection with the given onion address
// Status: TODO
2020-09-21 21:26:28 +00:00
func (cp *cwtchPeer) JoinServer(onion string) error {
ci, err := cp.FetchConversationInfo(onion)
if ci == nil || err != nil {
return errors.New("no keys found for server connection")
}
//if cp.GetContact(onion) != nil {
tokenY, yExists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.KeyTypePrivacyPass))).ToString()]
tokenOnion, onionExists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.KeyTypeTokenOnion))).ToString()]
if yExists && onionExists {
signature, exists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(lastKnownSignature)).ToString()]
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
}
2020-09-21 21:26:28 +00:00
return errors.New("no keys found for server connection")
2018-03-09 20:44:13 +00:00
}
2021-06-01 18:34:35 +00:00
// ResyncServer completely tears down and resyncs a new server connection with the given onion address
// Status: TODO
2021-06-01 18:34:35 +00:00
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
// }
//}
2021-06-01 18:34:35 +00:00
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)
}
// 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()
2021-06-02 18:34:57 +00:00
if !cp.listenStatus {
log.Infof("cwtchPeer Listen sending ProtocolEngineStartListen\n")
cp.listenStatus = true
onion, _ := cp.storage.LoadProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString())
cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: string(onion)}))
}
2021-06-02 18:34:57 +00:00
// else protocol engine is already listening
2018-03-09 20:44:13 +00:00
}
// 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)
// }
//}
}
// 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)
// }
// }
//}
}
// 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
2019-01-04 21:44:21 +00:00
cp.queue.Shutdown()
if cp.storage != nil {
cp.storage.Close()
}
}
// 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 {
id, err := cp.NewContactConversation(handle, model.DefaultP2PAccessControl(), false)
if err != nil {
return err
}
ci, err = cp.GetConversationInfo(id)
if err != nil {
return err
}
}
cp.mutex.Lock()
defer cp.mutex.Unlock()
// Generate a random number and use it as the signature
signature := event.GetRandNumber().String()
return cp.storage.InsertMessage(ci.ID, 0, message, model.Attributes{constants.AttrAck: event.True, constants.AttrSentTimestamp: sent.Format(time.RFC3339Nano)}, signature, model.CalculateContentHash(handle, message))
}
// 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}))
}
2019-01-04 21:44:21 +00:00
// eventHandler process events from other subsystems
func (cp *cwtchPeer) eventHandler() {
for {
ev := cp.queue.Next()
switch ev.EventType {
/***** Default auto handled events *****/
case event.ProtocolEngineStopped:
cp.mutex.Lock()
cp.listenStatus = false
log.Infof("Protocol engine for %v has stopped listening", cp.GetOnion())
cp.mutex.Unlock()
2019-01-04 21:44:21 +00:00
case event.EncryptedGroupMessage:
2021-06-01 18:34:35 +00:00
// If successful, a side effect is the message is added to the group's timeline
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
log.Debugf("received encrypted group message: %x", ev.Data[event.Signature])
2021-06-01 18:34:35 +00:00
// SECURITY NOTE: A malicious server could insert posts such that everyone always has a different lastKnownSignature
// However the server can always replace **all** messages in an attempt to track users
// This is mitigated somewhat by resync events which do wipe things entire.
// The security of cwtch groups are also not dependent on the servers inability to uniquely tag connections (as long as
// it learns nothing else about each connection).
// store the base64 encoded signature for later use
//cp.SetConversationAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature])
2021-06-01 18:34:35 +00:00
conversations, err := cp.FetchConversations()
if err == nil {
for _, conversationInfo := range conversations {
if tor.IsValidHostname(conversationInfo.Handle) == false {
group, err := cp.constructGroupFromConversation(conversationInfo)
if err == nil {
success, dgm := group.AttemptDecryption(ciphertext, signature)
if success {
// Time to either acknowledge the message or insert a new message
cp.attemptInsertOrAcknowledgeLegacyGroupConversation(conversationInfo.ID, ev.Data[event.Signature], dgm)
break
}
}
}
}
}
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
2020-10-03 23:49:05 +00:00
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
cp.storeMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
case event.PeerAcknowledgement:
err := cp.attemptAcknowledgeP2PConversation(ev.Data[event.RemotePeer], ev.Data[event.EventID])
if err != nil {
// Note: This is not an Error because malicious peers can just send acks for random things
// There is no point in polluting error logs with that mess.
log.Debugf("failed to acknowledge acknowledgement: %v", err)
}
case event.SendMessageToGroupError:
err := cp.attemptErrorConversationMessage(ev.Data[event.GroupID], ev.Data[event.Signature], event.SendMessageToGroupError, ev.Data[event.Error])
if err != nil {
log.Errorf("failed to error p2p message: %v", err)
}
case event.SendMessageToPeerError:
err := cp.attemptErrorConversationMessage(ev.Data[event.RemotePeer], ev.Data[event.EventID], event.SendMessageToPeerError, ev.Data[event.Error])
if err != nil {
log.Errorf("failed to error p2p message: %v", err)
}
case event.RetryServerRequest:
// Automated Join Server Request triggered by a plugin.
log.Debugf("profile received an automated retry event for %v", ev.Data[event.GroupServer])
err := cp.JoinServer(ev.Data[event.GroupServer])
if err != nil {
log.Errorf("error joining server... %v", err)
}
case event.NewGetValMessageFromPeer:
onion := ev.Data[event.RemotePeer]
scope := ev.Data[event.Scope]
path := ev.Data[event.Path]
2021-10-07 22:40:25 +00:00
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
conversationInfo, err := cp.FetchConversationInfo(onion)
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
if conversationInfo != nil && conversationInfo.Accepted {
2021-10-07 22:40:25 +00:00
scope := attr.IntoScope(scope)
if scope.IsPublic() || scope.IsConversation() {
zone, zpath := attr.ParseZone(path)
val, exists := cp.GetScopedZonedAttribute(scope, zone, zpath)
// NOTE: Temporary Override because UI currently wipes names if it can't find them...
if !exists && zone == attr.UnknownZone && path == constants.Name {
val, exists = cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
}
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationInfo.ID), event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)})
resp.EventID = ev.EventID
if exists {
resp.Data[event.Data] = val
} else {
resp.Data[event.Data] = ""
}
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
cp.eventBus.Publish(resp)
}
}
/***** Non default but requestable handleable events *****/
case event.ManifestReceived:
log.Debugf("Manifest Received Event!: %v", ev)
handle := ev.Data[event.Handle]
fileKey := ev.Data[event.FileKey]
serializedManifest := ev.Data[event.SerializedManifest]
2021-10-07 22:40:25 +00:00
manifestFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey))
if exists {
2021-10-07 22:40:25 +00:00
downloadFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fileKey)
if exists {
log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath)
var manifest files.Manifest
err := json.Unmarshal([]byte(serializedManifest), &manifest)
if err == nil {
manifest.Title = manifest.FileName
manifest.FileName = downloadFilePath
log.Debugf("saving manifest")
err = manifest.Save(manifestFilePath)
if err != nil {
log.Errorf("could not save manifest: %v", err)
} else {
tempFile := ""
if runtime.GOOS == "android" {
tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")]
log.Debugf("derived android temp path: %v", tempFile)
}
cp.eventBus.Publish(event.NewEvent(event.ManifestSaved, map[event.Field]string{
event.FileKey: fileKey,
event.Handle: handle,
event.SerializedManifest: string(manifest.Serialize()),
event.TempFile: tempFile,
event.NameSuggestion: manifest.Title,
}))
}
} else {
log.Errorf("error saving manifest: %v", err)
}
} else {
log.Errorf("found manifest path but not download path for %v", fileKey)
}
} else {
log.Errorf("no download path found for manifest: %v", fileKey)
}
case event.NewRetValMessageFromPeer:
onion := ev.Data[event.RemotePeer]
scope := ev.Data[event.Scope]
path := ev.Data[event.Path]
val := ev.Data[event.Data]
exists, _ := strconv.ParseBool(ev.Data[event.Exists])
log.Debugf("NewRetValMessageFromPeer %v %v %v %v %v\n", onion, scope, path, exists, val)
if exists {
2021-10-07 22:40:25 +00:00
// Handle File Sharing Metadata
// TODO This probably should be broken out to it's own code..
zone, path := attr.ParseZone(path)
if attr.Scope(scope).IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(path, ".manifest.size") {
fileKey := strings.Replace(path, ".manifest.size", "", 1)
size, err := strconv.Atoi(val)
// if size is valid and below the maximum size for a manifest
// this is to prevent malicious sharers from using large amounts of memory when distributing
// a manifest as we reconstruct this in-memory
if err == nil && size < files.MaxManifestSize {
cp.eventBus.Publish(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: val, event.Handle: onion}))
} else {
2021-10-07 22:40:25 +00:00
cp.eventBus.Publish(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: onion}))
}
}
2021-10-07 22:40:25 +00:00
// Allow public profile parameters to be added as peer specific attributes...
if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone {
ci, err := cp.FetchConversationInfo(onion)
log.Debugf("fetch conversation info %v %v", ci, err)
if ci != nil && err == nil {
err := cp.SetConversationAttribute(ci.ID, attr.Scope(scope).ConstructScopedZonedPath(zone.ConstructZonedPath(path)), val)
if err != nil {
log.Errorf("error setting conversation attribute %v", err)
}
}
2021-10-07 22:40:25 +00:00
}
}
case event.PeerStateChange:
cp.mutex.Lock()
cp.state[ev.Data[event.RemotePeer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
case event.ServerStateChange:
cp.mutex.Lock()
cp.state[ev.Data[event.GroupServer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
2019-01-04 21:44:21 +00:00
default:
if ev.EventType != "" {
log.Errorf("peer event handler received an event it was not subscribed for: %v", ev.EventType)
2019-01-04 21:44:21 +00:00
}
2019-01-21 20:08:03 +00:00
return
2019-01-04 21:44:21 +00:00
}
}
2018-10-04 19:15:03 +00:00
}
// attemptInsertOrAcknowledgeLegacyGroupConversation is a convenience method that looks up the conversation
// by the given handle and attempts to mark the message as acknowledged. returns error on failure
// to either find the contact or the associated message
func (cp *cwtchPeer) attemptInsertOrAcknowledgeLegacyGroupConversation(conversationID int, signature string, dm *groups.DecryptedGroupMessage) error {
log.Infof("attempting to insert or ack group message %v %v", conversationID, signature)
messageID, err := cp.GetChannelMessageBySignature(conversationID, 0, signature)
// We have received our own message (probably), acknowledge and move on...
if err == nil {
_, attr, err := cp.GetChannelMessage(conversationID, 0, messageID)
if err == nil {
cp.mutex.Lock()
attr[constants.AttrAck] = constants.True
cp.storage.UpdateMessageAttributes(conversationID, 0, messageID, attr)
cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationID), event.Index: strconv.Itoa(messageID)}))
return nil
}
} else {
cp.mutex.Lock()
cp.storage.InsertMessage(conversationID, 0, dm.Text, model.Attributes{constants.AttrAck: constants.True, "PreviousSignature": base64.StdEncoding.EncodeToString(dm.PreviousMessageSig), "Author": dm.Onion, constants.AttrSentTimestamp: strconv.Itoa(int(dm.Timestamp))}, signature, model.CalculateContentHash(dm.Onion, dm.Text))
cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationID), event.Index: strconv.Itoa(messageID)}))
return nil
}
return err
}
// attemptAcknowledgeP2PConversation is a convenience method that looks up the conversation
// by the given handle and attempts to mark the message as acknowledged. returns error on failure
// to either find the contact or the associated message
func (cp *cwtchPeer) attemptAcknowledgeP2PConversation(handle string, signature string) error {
ci, err := cp.FetchConversationInfo(handle)
// We should *never* received a peer acknowledgement for a conversation that doesn't exist...
if ci != nil && err == nil {
// for p2p messages the randomly generated event ID is the "signature"
id, err := cp.GetChannelMessageBySignature(ci.ID, 0, signature)
if err == nil {
_, attr, err := cp.GetChannelMessage(ci.ID, 0, id)
if err == nil {
cp.mutex.Lock()
attr[constants.AttrAck] = constants.True
cp.storage.UpdateMessageAttributes(ci.ID, 0, id, attr)
cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: handle, event.Index: strconv.Itoa(id)}))
return nil
}
return err
}
return err
}
return err
}
// attemptErrorConversationMessage is a convenience method that looks up the conversation
// by the given handle and attempts to mark the message as errored. returns error on failure
// to either find the contact or the associated message
func (cp *cwtchPeer) attemptErrorConversationMessage(handle string, signature string, eventType event.Type, error string) error {
ci, err := cp.FetchConversationInfo(handle)
// We should *never* received a peer acknowledgement for a conversation that doesn't exist...
if ci != nil && err == nil {
// for p2p messages the randomly generated event ID is the "signature"
id, err := cp.GetChannelMessageBySignature(ci.ID, 0, signature)
if err == nil {
_, attr, err := cp.GetChannelMessage(ci.ID, 0, id)
if err == nil {
cp.mutex.Lock()
attr[constants.AttrErr] = constants.True
cp.storage.UpdateMessageAttributes(ci.ID, 0, id, attr)
cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(eventType, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: handle, event.Error: error, event.Index: strconv.Itoa(id)}))
return nil
}
return err
}
return err
}
return err
}
func (cp *cwtchPeer) GetChannelMessageBySignature(conversationID int, channelID int, signature string) (int, error) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.GetChannelMessageBySignature(conversationID, channelID, signature)
}
func (cp *cwtchPeer) constructGroupFromConversation(conversationInfo *model.Conversation) (*model.Group, error) {
key := conversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupKey)).ToString()]
groupKey, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, errors.New("group key is malformed")
}
var groupKeyFixed [32]byte
copy(groupKeyFixed[:], groupKey[:])
group := model.Group{
GroupID: conversationInfo.Handle,
GroupServer: conversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupServer)).ToString()],
GroupKey: groupKeyFixed,
}
return &group, nil
}