package peer import ( "crypto/rand" "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" "fmt" "git.openprivacy.ca/openprivacy/libricochet-go/application" "git.openprivacy.ca/openprivacy/libricochet-go/channels" "git.openprivacy.ca/openprivacy/libricochet-go/connection" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "github.com/golang/protobuf/proto" "github.com/ulule/deepcopier" "golang.org/x/crypto/ed25519" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/sha3" "io" "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 key [32]byte salt [128]byte dataHandler func(string, []byte) []byte //handlers map[string]func(*application.ApplicationInstance) func() channels.Handler aif application.ApplicationInstanceFactory } // CwtchPeer provides us with a way of testing systems built on top of cwtch without having to // directly implement a cwtchPeer. type CwtchPeer interface { Save() error ChangePassword(string) error PeerWithOnion(string) *connections.PeerPeerConnection InviteOnionToGroup(string, string) error 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 SetApplicationInstanceFactory(factory application.ApplicationInstanceFactory) SetPeerDataHandler(func(string, []byte) []byte) Listen() error Shutdown() } // createKey derives a key and salt for use in encrypting cwtchPeers func createKey(password string) ([32]byte, [128]byte, error) { var salt [128]byte if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil { log.Printf("Error: 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 } //encryptProfile encrypts the cwtchPeer via the specified key. func encryptProfile(p *cwtchPeer, key [32]byte) ([]byte, error) { var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { log.Printf("Error: Cannot read from random: %v\n", err) return nil, err } //copy the struct, then remove the key and salt before saving the copy cpc := &cwtchPeer{} deepcopier.Copy(p).To(cpc) var blankkey [32]byte var blanksalt [128]byte cpc.key = blankkey cpc.salt = blanksalt bytes, _ := json.Marshal(cpc) encrypted := secretbox.Seal(nonce[:], []byte(bytes), &nonce, &key) return encrypted, nil } //decryptProfile decrypts the passed ciphertext into a cwtchPeer via the specified key. func decryptProfile(ciphertext []byte, key [32]byte) (*cwtchPeer, error) { var decryptNonce [24]byte copy(decryptNonce[:], ciphertext[:24]) decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key) if ok { cp := &cwtchPeer{} err := json.Unmarshal(decrypted, &cp) if err == nil { return cp, nil } return nil, err } return nil, fmt.Errorf("Failed to decrypt") } func (cp *cwtchPeer) setup() { cp.Log = make(chan string) cp.connectionsManager = connections.NewConnectionsManager() cp.Init() go cp.connectionsManager.AttemptReconnections() } // NewCwtchPeer creates and returns a new cwtchPeer with the given name. func NewCwtchPeer(name string, password string, profilefile string) (CwtchPeer, error) { cp := new(cwtchPeer) cp.profilefile = profilefile cp.Profile = model.GenerateNewProfile(name) cp.setup() key, salt, err := createKey(password) if err != nil { return nil, err } cp.key = key cp.salt = salt return cp, nil } func (cp *cwtchPeer) ChangePassword(password string) error { key, salt, err := createKey(password) if err != nil { return err } cp.key = key cp.salt = salt return nil } // Save saves the cwtchPeer profile state to a file. func (cp *cwtchPeer) Save() error { cp.mutex.Lock() defer cp.mutex.Unlock() encryptedbytes, err := encryptProfile(cp, cp.key) if err != nil { return err } // the salt for the derived key is appended to the front of the file encryptedbytes = append(cp.salt[:], encryptedbytes...) err = ioutil.WriteFile(cp.profilefile, encryptedbytes, 0600) return err } // LoadCwtchPeer loads an existing cwtchPeer from a file. func LoadCwtchPeer(profilefile string, password string) (CwtchPeer, error) { encryptedbytes, err := ioutil.ReadFile(profilefile) if err == nil { var dkr [32]byte var salty [128]byte //Separate the salt from the encrypted bytes, then generate the derived key salt, encryptedbytes := encryptedbytes[0:128], encryptedbytes[128:] dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512) //cast to arrays copy(dkr[:], dk) copy(salty[:], salt) var cp *cwtchPeer cp, err = decryptProfile(encryptedbytes, dkr) if err == nil { cp.setup() cp.profilefile = profilefile cp.key = dkr cp.salt = salty return cp, nil } } return nil, 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, "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 } // a handler for the optional data handler // note that the "correct" way to do this would be to AddChannelHandler("im.cwtch.peerdata", ...") but peerdata is such // a handy channel that it's nice to have this convenience shortcut func (cp *cwtchPeer) SetPeerDataHandler(dataHandler func(string, []byte) []byte) { cp.dataHandler = dataHandler } // add extra channel handlers (note that the peer will merge these with the ones necessary to make cwtch work, so you // are not clobbering the underlying functionality) func (cp *cwtchPeer) SetApplicationInstanceFactory(aif application.ApplicationInstanceFactory) { cp.aif = aif } // 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 Peer. // TODO While it is probably "safe", it is not really "safe", to call functions on this profile. This only exists to return things like Name and Onion,we should gate these. 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 { return cp.connectionsManager.ManagePeerConnection(onion, cp.Profile, cp.dataHandler, cp.aif) } // 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(group.GetInitialMessage()) 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, gm.Signature) } // 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, sig, err := cp.Profile.EncryptMessageToGroup(message, groupid) if err != nil { return err } gm := &protocol.GroupMessage{ Ciphertext: ct, Signature: sig, } 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 } // LookupContact returns that a contact is known and allowed to communicate for all cases. func (cp *cwtchPeer) LookupContactV3(hostname string, publicKey ed25519.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.SetupOnionV3("127.0.0.1:9051", "tcp4", "", cp.Profile.Ed25519PrivateKey, cp.GetProfile().Onion, 9878) if err != nil && fmt.Sprintf("%v", err) != "550 Unspecified Tor error: Onion address collision" { 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 } }) if cp.dataHandler != nil { af.AddHandler("im.cwtch.peer.data", func(rai *application.ApplicationInstance) func() channels.Handler { cpi := new(CwtchPeerInstance) cpi.Init(rai, cwtchpeer) return func() channels.Handler { cpc := new(peer.CwtchPeerDataChannel) cpc.Handler = &CwtchPeerHandler{Onion: rai.RemoteHostname, Peer: cp, DataHandler: cp.dataHandler} return cpc } }) } handlers := cp.aif.GetHandlers() for i := range handlers { af.AddHandler(handlers[i], cp.aif.GetHandler(handlers[i])) } cwtchpeer.InitV3(cp.Profile.Name, identity.InitializeV3(cp.Profile.Name, &cp.Profile.Ed25519PrivateKey, &cp.Profile.Ed25519PublicKey), af, cp) log.Printf("Running cwtch peer on %v", l.Addr().String()) cp.app = cwtchpeer cwtchpeer.Run(l) return nil } // Shutdown kills all connections and cleans up all goroutines for the peer func (cp *cwtchPeer) Shutdown() { cp.connectionsManager.Shutdown() if cp.app != nil { cp.app.Shutdown() } cp.Save() } // 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 DataHandler func(string, []byte) []byte } // 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() } // 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() } // HandlePacket handles the Cwtch Peer Data Channel func (cph *CwtchPeerHandler) HandlePacket(data []byte) []byte { return cph.DataHandler(cph.Onion, data) }