forked from cwtch.im/cwtch
492 lines
15 KiB
Go
492 lines
15 KiB
Go
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
|
|
}
|
|
|
|
// 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
|
|
|
|
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
|
|
}
|
|
|
|
// ExportGroup serializes a group invite so it can be given offline
|
|
func (cp *cwtchPeer) SetPeerDataHandler(dataHandler func(string, []byte) []byte) {
|
|
cp.dataHandler = dataHandler
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
})
|
|
}
|
|
|
|
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()
|
|
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)
|
|
}
|