package peer import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "cwtch.im/cwtch/protocol" "cwtch.im/cwtch/protocol/connections" "encoding/base64" "errors" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "github.com/ethereum/go-ethereum/log" "github.com/golang/protobuf/proto" "golang.org/x/crypto/ed25519" "strings" "sync" ) // cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch cwtchPeer type cwtchPeer struct { Profile *model.Profile mutex sync.Mutex shutdown bool started bool engine *connections.Engine queue *event.Queue eventBus *event.Manager } // CwtchPeer provides us with a way of testing systems built on top of cwtch without having to // directly implement a cwtchPeer. type CwtchPeer interface { Init(connectivity.ACN, *event.Manager) PeerWithOnion(string) *connections.PeerPeerConnection InviteOnionToGroup(string, string) error SendMessageToPeer(string, string) TrustPeer(string) error BlockPeer(string) error AcceptInvite(string) error RejectInvite(string) JoinServer(string) SendMessageToGroup(string, string) error GetProfile() *model.Profile GetPeers() map[string]connections.ConnectionState GetServers() map[string]connections.ConnectionState StartGroup(string) (string, []byte, error) ImportGroup(string) (string, error) ExportGroup(string) (string, error) GetGroup(string) *model.Group GetGroups() []string GetContacts() []string GetContact(string) *model.PublicProfile IsStarted() bool Listen() Shutdown() } // NewCwtchPeer creates and returns a new cwtchPeer with the given name. func NewCwtchPeer(name string) CwtchPeer { cp := new(cwtchPeer) cp.Profile = model.GenerateNewProfile(name) cp.shutdown = false return cp } // FromProfile generates a new peer from a profile. func FromProfile(profile *model.Profile) CwtchPeer { cp := new(cwtchPeer) cp.Profile = profile return cp } // Init instantiates a cwtchPeer func (cp *cwtchPeer) Init(acn connectivity.ACN, eventBus *event.Manager) { cp.queue = event.NewEventQueue(100) go cp.eventHandler() cp.eventBus = eventBus cp.eventBus.Subscribe(event.EncryptedGroupMessage, cp.queue.EventChannel) cp.eventBus.Subscribe(event.NewGroupInvite, cp.queue.EventChannel) // TODO: Would be nice if ProtocolEngine did not need to explictly be given the Private Key. cp.engine = connections.NewProtocolEngine(cp.Profile.Ed25519PrivateKey, acn, eventBus) cp.engine.Identity = identity.InitializeV3(cp.Profile.Name, &cp.Profile.Ed25519PrivateKey, &cp.Profile.Ed25519PublicKey) } // ImportGroup intializes a group from an imported source rather than a peer invite func (cp *cwtchPeer) ImportGroup(exportedInvite string) (groupID string, err error) { if strings.HasPrefix(exportedInvite, "torv3") { data, err := base64.StdEncoding.DecodeString(exportedInvite[5+44:]) if err == nil { cpp := &protocol.CwtchPeerPacket{} err := proto.Unmarshal(data, cpp) if err == nil { pk, err := base64.StdEncoding.DecodeString(exportedInvite[5 : 5+44]) if err == nil { edpk := ed25519.PublicKey(pk) onion := utils.GetTorV3Hostname(edpk) cp.Profile.AddContact(onion, &model.PublicProfile{Name: "", Ed25519PublicKey: edpk, Trusted: true, Blocked: false, Onion: onion}) cp.Profile.ProcessInvite(cpp.GetGroupChatInvite(), onion) return cpp.GroupChatInvite.GetGroupName(), nil } } } } else { err = errors.New("unsupported exported group type") } return } // ExportGroup serializes a group invite so it can be given offline func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) { group := cp.Profile.GetGroupByGroupID(groupID) if group != nil { invite, err := group.Invite(group.GetInitialMessage()) if err == nil { exportedInvite := "torv3" + base64.StdEncoding.EncodeToString(cp.Profile.Ed25519PublicKey) + base64.StdEncoding.EncodeToString(invite) return exportedInvite, err } } return "", errors.New("group id could not be found") } // StartGroup create a new group linked to the given server and returns the group ID, an invite or an error. func (cp *cwtchPeer) StartGroup(server string) (string, []byte, error) { return cp.Profile.StartGroup(server) } // StartGroupWithMessage create a new group linked to the given server and returns the group ID, an invite or an error. func (cp *cwtchPeer) StartGroupWithMessage(server string, initialMessage []byte) (string, []byte, error) { return cp.Profile.StartGroupWithMessage(server, initialMessage) } // GetGroups returns an unordered list of all group IDs. func (cp *cwtchPeer) GetGroups() []string { return cp.Profile.GetGroups() } // GetGroup returns a pointer to a specific group, nil if no group exists. func (cp *cwtchPeer) GetGroup(groupID string) *model.Group { return cp.Profile.GetGroupByGroupID(groupID) } // GetContacts returns an unordered list of onions func (cp *cwtchPeer) GetContacts() []string { return cp.Profile.GetContacts() } // GetContact returns a given contact, nil is no such contact exists func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile { contact, _ := cp.Profile.GetContact(onion) return contact } // GetProfile returns the profile associated with this cwtchPeer. func (cp *cwtchPeer) GetProfile() *model.Profile { return cp.Profile } // PeerWithOnion is the entry point for cwtchPeer relationships func (cp *cwtchPeer) PeerWithOnion(onion string) *connections.PeerPeerConnection { cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[string]string{"Onion": onion})) return nil } // InviteOnionToGroup kicks off the invite process func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error { group := cp.Profile.GetGroupByGroupID(groupid) if group == nil { return errors.New("invalid group id") } invite, err := group.Invite(group.InitialMessage) if err == nil { cp.eventBus.Publish(event.NewEvent(event.InvitePeerToGroup, map[string]string{"Onion": onion, "Invite": string(invite)})) } return err } // JoinServer manages a new server connection with the given onion address func (cp *cwtchPeer) JoinServer(onion string) { cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[string]string{"Onion": onion})) } // SendMessageToGroup attemps to sent the given message to the given group id. func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error { group := cp.Profile.GetGroupByGroupID(groupid) if group == nil { return errors.New("invalid group id") } ct, sig, err := cp.Profile.EncryptMessageToGroup(message, groupid) if err == nil { cp.eventBus.Publish(event.NewEvent(event.SendMessageToGroup, map[string]string{"Server": group.GroupServer, "Ciphertext": string(ct), "Signature": string(sig)})) } return err } func (cp *cwtchPeer) SendMessageToPeer(onion string, message string) { cp.eventBus.Publish(event.NewEvent(event.SendMessageToPeer, map[string]string{"Peer": onion, "Message": message})) } // GetPeers returns a list of peer connections. func (cp *cwtchPeer) GetPeers() map[string]connections.ConnectionState { return cp.engine.GetPeers() } // GetServers returns a list of server connections func (cp *cwtchPeer) GetServers() map[string]connections.ConnectionState { return cp.engine.GetServers() } // TrustPeer sets an existing peer relationship to trusted func (cp *cwtchPeer) TrustPeer(peer string) error { err := cp.Profile.TrustPeer(peer) if err == nil { cp.PeerWithOnion(peer) } return err } // BlockPeer blocks an existing peer relationship. func (cp *cwtchPeer) BlockPeer(peer string) error { err := cp.Profile.BlockPeer(peer) cp.eventBus.Publish(event.NewEvent(event.BlockPeer, map[string]string{"Onion": peer})) return err } // AcceptInvite accepts a given existing group invite func (cp *cwtchPeer) AcceptInvite(groupID string) error { return cp.Profile.AcceptInvite(groupID) } // RejectInvite rejects a given group invite. func (cp *cwtchPeer) RejectInvite(groupID string) { cp.Profile.RejectInvite(groupID) } func (cp *cwtchPeer) Listen() { cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[string]string{})) } // Shutdown kills all connections and cleans up all goroutines for the peer func (cp *cwtchPeer) Shutdown() { cp.shutdown = true cp.engine.Shutdown() cp.queue.Shutdown() } // IsStarted returns true if Listen() has successfully been run before on this connection (ever). TODO: we will need to properly unset this flag on error if we want to support resumption in the future func (cp *cwtchPeer) IsStarted() bool { return cp.started } // eventHandler process events from other subsystems func (cp *cwtchPeer) eventHandler() { for { ev := cp.queue.Next() switch ev.EventType { case event.EncryptedGroupMessage: ok, groupID, _ := cp.Profile.AttemptDecryption([]byte(ev.Data["Ciphertext"]), []byte(ev.Data["Signature"])) if ok { cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[string]string{"GroupID": groupID})) } case event.NewGroupInvite: var groupInvite protocol.GroupChatInvite proto.Unmarshal([]byte(ev.Data["GroupInvite"]), &groupInvite) cp.Profile.ProcessInvite(&groupInvite, ev.Data["Onion"]) default: if ev.EventType != "" { log.Error("peer event handler received an event it was not subscribed for: %v") } return } } }