package peer import ( "crypto/rsa" "cwtch.im/cwtch/model" "cwtch.im/cwtch/peer/connections" "cwtch.im/cwtch/peer/peer" "cwtch.im/cwtch/protocol" "encoding/base64" "encoding/json" "errors" "github.com/golang/protobuf/proto" "github.com/s-rah/go-ricochet/application" "github.com/s-rah/go-ricochet/channels" "github.com/s-rah/go-ricochet/connection" "golang.org/x/crypto/ed25519" "io/ioutil" "log" "strings" "sync" ) // CwtchPeer manages incoming and outgoing connections and all processing for a Cwtch Peer type CwtchPeer struct { connection.AutoConnectionHandler Profile *model.Profile app *application.RicochetApplication mutex sync.Mutex Log chan string `json:"-"` connectionsManager *connections.Manager profilefile string } func (cp *CwtchPeer) setup() { cp.Log = make(chan string) cp.connectionsManager = connections.NewConnectionsManager() cp.Init() go cp.connectionsManager.AttemptReconnections() for onion, profile := range cp.Profile.Contacts { if profile.Trusted && !profile.Blocked { cp.PeerWithOnion(onion) } } for _, group := range cp.Profile.Groups { if group.Accepted || group.Owner == "self" { cp.JoinServer(group.GroupServer) } } } // 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.setup() return cp } // Save saves the CwtchPeer profile state to a file. func (cp *CwtchPeer) Save(profilefile string) error { cp.mutex.Lock() bytes, _ := json.Marshal(cp) err := ioutil.WriteFile(profilefile, bytes, 0600) cp.profilefile = profilefile cp.mutex.Unlock() return err } // LoadCwtchPeer loads an existing CwtchPeer from a file. func LoadCwtchPeer(profilefile string) (*CwtchPeer, error) { bytes, _ := ioutil.ReadFile(profilefile) cp := new(CwtchPeer) err := json.Unmarshal(bytes, &cp) cp.setup() cp.profilefile = profilefile return cp, err } // 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, "torv2") { data, err := base64.StdEncoding.DecodeString(exportedInvite[21+44:]) if err == nil { cpp := &protocol.CwtchPeerPacket{} err := proto.Unmarshal(data, cpp) if err == nil { pk, err := base64.StdEncoding.DecodeString(exportedInvite[21 : 21+44]) if err == nil { edpk := ed25519.PublicKey(pk) onion := exportedInvite[5:21] 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() if err == nil { exportedInvite := "torv2" + cp.Profile.Onion + base64.StdEncoding.EncodeToString(cp.Profile.Ed25519PublicKey) + base64.StdEncoding.EncodeToString(invite) return exportedInvite, err } } return "", errors.New("group id could not be found") } // PeerWithOnion is the entry point for CwtchPeer relationships func (cp *CwtchPeer) PeerWithOnion(onion string) { cp.connectionsManager.ManagePeerConnection(onion, cp.Profile) } // InviteOnionToGroup kicks off the invite process func (cp *CwtchPeer) InviteOnionToGroup(onion string, groupid string) error { group := cp.Profile.GetGroupByGroupID(groupid) if group != nil { log.Printf("Constructing invite for group: %v\n", group) invite, err := group.Invite() if err != nil { return err } ppc := cp.connectionsManager.GetPeerPeerConnectionForOnion(onion) if ppc == nil { return errors.New("peer connection not setup for onion. peers must be trusted before sending") } if ppc.GetState() == connections.AUTHENTICATED { log.Printf("Got connection for group: %v - Sending Invite\n", ppc) ppc.SendGroupInvite(invite) } else { return errors.New("cannot send invite to onion: peer connection is not ready") } return nil } return errors.New("group id could not be found") } // ReceiveGroupMessage is a callback function that processes GroupMessages from a given server func (cp *CwtchPeer) ReceiveGroupMessage(server string, gm *protocol.GroupMessage) { cp.Profile.AttemptDecryption(gm.Ciphertext) } // JoinServer manages a new server connection with the given onion address func (cp *CwtchPeer) JoinServer(onion string) { cp.connectionsManager.ManageServerConnection(onion, cp.ReceiveGroupMessage) } // 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("group does not exist") } psc := cp.connectionsManager.GetPeerServerConnectionForOnion(group.GroupServer) if psc == nil { return errors.New("could not find server connection to send message to") } ct, err := cp.Profile.EncryptMessageToGroup(message, groupid) if err != nil { return err } gm := &protocol.GroupMessage{ Ciphertext: ct, } err = psc.SendGroupMessage(gm) return err } // GetPeers returns a list of peer connections. func (cp *CwtchPeer) GetPeers() map[string]connections.ConnectionState { return cp.connectionsManager.GetPeers() } // GetServers returns a list of server connections func (cp *CwtchPeer) GetServers() map[string]connections.ConnectionState { return cp.connectionsManager.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.connectionsManager.ClosePeerConnection(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) } // LookupContact returns that a contact is known and allowed to communicate for all cases. func (cp *CwtchPeer) LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool) { blocked := cp.Profile.IsBlocked(hostname) return !blocked, true } // ContactRequest needed to implement ContactRequestHandler Interface func (cp *CwtchPeer) ContactRequest(name string, message string) string { return "Accepted" } // Listen sets up an onion listener to process incoming cwtch messages func (cp *CwtchPeer) Listen() error { cwtchpeer := new(application.RicochetApplication) l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", cp.Profile.OnionPrivateKey, 9878) if err != nil { return err } af := application.ApplicationInstanceFactory{} af.Init() af.AddHandler("im.cwtch.peer", func(rai *application.ApplicationInstance) func() channels.Handler { cpi := new(CwtchPeerInstance) cpi.Init(rai, cwtchpeer) return func() channels.Handler { cpc := new(peer.CwtchPeerChannel) cpc.Handler = &CwtchPeerHandler{Onion: rai.RemoteHostname, Peer: cp} return cpc } }) cwtchpeer.Init(cp.Profile.Name, cp.Profile.OnionPrivateKey, af, cp) log.Printf("Running cwtch peer on %v", l.Addr().String()) cp.app = cwtchpeer cwtchpeer.Run(l) return nil } func (cp *CwtchPeer) Shutdown() { cp.connectionsManager.Shutdown() cp.app.Shutdown() } // CwtchPeerInstance encapsulates incoming peer connections type CwtchPeerInstance struct { rai *application.ApplicationInstance ra *application.RicochetApplication } // Init sets up a CwtchPeerInstance func (cpi *CwtchPeerInstance) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) { cpi.rai = rai cpi.ra = ra } // CwtchPeerHandler encapsulates handling of incoming CwtchPackets type CwtchPeerHandler struct { Onion string Peer *CwtchPeer } // ClientIdentity handles incoming ClientIdentity packets func (cph *CwtchPeerHandler) ClientIdentity(ci *protocol.CwtchIdentity) { log.Printf("Received Client Identity from %v %v\n", cph.Onion, ci.String()) cph.Peer.Profile.AddCwtchIdentity(cph.Onion, ci) cph.Peer.Save(cph.Peer.profilefile) } // HandleGroupInvite handles incoming GroupInvites func (cph *CwtchPeerHandler) HandleGroupInvite(gci *protocol.GroupChatInvite) { log.Printf("Received GroupID from %v %v\n", cph.Onion, gci.String()) cph.Peer.Profile.ProcessInvite(gci, cph.Onion) } // GetClientIdentityPacket returns our ClientIdentity packet so it can be sent to the connected peer. func (cph *CwtchPeerHandler) GetClientIdentityPacket() []byte { return cph.Peer.Profile.GetCwtchIdentityPacket() }