Skip to content
Snippets Groups Projects
e2e.go 13.01 KiB
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation                                             //
//                                                                            //
// Use of this source code is governed by a license that can be found in the  //
// LICENSE file.                                                              //
////////////////////////////////////////////////////////////////////////////////

package xxdk

import (
	"encoding/json"
	"time"

	"github.com/pkg/errors"
	jww "github.com/spf13/jwalterweatherman"
	"gitlab.com/elixxir/client/auth"
	"gitlab.com/elixxir/client/cmix/identity/receptionID"
	"gitlab.com/elixxir/client/cmix/rounds"
	"gitlab.com/elixxir/client/e2e"
	"gitlab.com/elixxir/client/e2e/rekey"
	"gitlab.com/elixxir/client/storage/user"
	"gitlab.com/elixxir/client/storage/versioned"
	"gitlab.com/elixxir/crypto/contact"
	"gitlab.com/elixxir/ekv"
	"gitlab.com/xx_network/crypto/xx"
	"gitlab.com/xx_network/primitives/id"
)

// E2e object bundles a ReceptionIdentity with a Cmix object and can be used for
// high-level operations, such as connections.
type E2e struct {
	*Cmix
	auth        auth.State
	e2e         e2e.Handler
	backup      *Container
	e2eIdentity ReceptionIdentity
}

// AuthCallbacks is an adapter for the auth.Callbacks interface that allows for
// initializing an E2e object without an E2e-dependant auth.Callbacks.
type AuthCallbacks interface {
	Request(partner contact.Contact, receptionID receptionID.EphemeralIdentity,
		round rounds.Round, user *E2e)
	Confirm(partner contact.Contact, receptionID receptionID.EphemeralIdentity,
		round rounds.Round, user *E2e)
	Reset(partner contact.Contact, receptionID receptionID.EphemeralIdentity,
		round rounds.Round, user *E2e)
}

// Login creates a new E2e backed by the xxdk.Cmix persistent versioned.KV. It
// bundles a Cmix object with a ReceptionIdentity object and initializes the
// auth.State and e2e.Handler objects.
func Login(net *Cmix, callbacks AuthCallbacks,
	identity ReceptionIdentity, params E2EParams) (m *E2e, err error) {

	// If the given identity matches the stored ReceptionID,
	// then we are using a legacy ReceptionIdentity
	defaultReceptionId := net.GetStorage().PortableUserInfo().ReceptionID
	if identity.ID.Cmp(defaultReceptionId) {
		return loginLegacy(net, callbacks, identity, params)
	}

	// Otherwise, we are using a modern ReceptionIdentity
	return login(net, callbacks, identity, net.GetStorage().GetKV(), params)
}

// LoginEphemeral creates a new E2e backed by a totally ephemeral versioned.KV.
func LoginEphemeral(net *Cmix, callbacks AuthCallbacks,
	identity ReceptionIdentity, params E2EParams) (m *E2e, err error) {
	return login(net, callbacks, identity,
		versioned.NewKV(ekv.MakeMemstore()), params)
}

// loginLegacy creates a new E2e backed by the xxdk.Cmix persistent
// versioned.KV. It uses the pre-generated transmission ID used by xxdk.Cmix.
// This function is designed to maintain backwards compatibility with previous
// xx messenger designs and should not be used for other purposes.
func loginLegacy(net *Cmix, callbacks AuthCallbacks,
	identity ReceptionIdentity, params E2EParams) (
	m *E2e, err error) {
	m = &E2e{
		Cmix:   net,
		backup: &Container{},
	}

	m.e2e, err = loadOrInitE2eLegacy(identity, net)
	if err != nil {
		return nil, err
	}
	net.GetCmix().AddIdentity(identity.ID, time.Time{}, true)

	err = net.AddService(m.e2e.StartProcesses)
	if err != nil {
		return nil, errors.WithMessage(err, "Failed to add the e2e processes")
	}

	m.auth, err = auth.NewStateLegacy(net.GetStorage().GetKV(),
		net.GetCmix(), m.e2e, net.GetRng(), net.GetEventReporter(),
		params.Auth, params.Session,
		MakeAuthCallbacksAdapter(callbacks, m),
		m.backup.TriggerBackup)
	if err != nil {
		return nil, err
	}

	rsaKey, err := identity.GetRSAPrivateKey()
	if err != nil {
		return nil, err
	}
	m.e2eIdentity, err = buildReceptionIdentity(identity.ID, identity.Salt,
		rsaKey, m.e2e.GetGroup(), m.e2e.GetHistoricalDHPrivkey())
	return m, err
}

// login creates a new xxdk.E2e backed by the given versioned.KV.
func login(net *Cmix, callbacks AuthCallbacks, identity ReceptionIdentity,
	kv *versioned.KV, params E2EParams) (m *E2e, err error) {

	// Verify the passed-in ReceptionIdentity matches its properties
	privatePem, err := identity.GetRSAPrivateKey()
	if err != nil {
		return nil, err
	}
	generatedId, err := xx.NewID(privatePem.GetPublic(), identity.Salt, id.User)
	if err != nil {
		return nil, err
	}
	if !generatedId.Cmp(identity.ID) {
		return nil, errors.Errorf(
			"Given identity %s is invalid, generated ID does not match",
			identity.ID.String())
	}

	m = &E2e{
		Cmix:        net,
		backup:      &Container{},
		e2eIdentity: identity,
	}
	dhPrivKey, err := identity.GetDHKeyPrivate()
	if err != nil {
		return nil, err
	}

	// Load or init the new e2e storage
	e2eGrp := net.GetStorage().GetE2EGroup()
	m.e2e, err = e2e.Load(kv, net.GetCmix(), identity.ID, e2eGrp, net.GetRng(),
		net.GetEventReporter())
	if err != nil {
		// Initialize the e2e storage
		err = e2e.Init(kv, identity.ID, dhPrivKey, e2eGrp, params.Rekey)
		if err != nil {
			return nil, err
		}

		// Load the new e2e storage
		m.e2e, err = e2e.Load(kv, net.GetCmix(), identity.ID, e2eGrp,
			net.GetRng(), net.GetEventReporter())
		if err != nil {
			return nil, errors.WithMessage(
				err, "Failed to load a newly created e2e store")
		}
	}

	err = net.AddService(m.e2e.StartProcesses)
	if err != nil {
		return nil, errors.WithMessage(err, "Failed to add the e2e processes")
	}

	m.auth, err = auth.NewState(kv, net.GetCmix(), m.e2e, net.GetRng(),
		net.GetEventReporter(), params.Auth, params.Session,
		MakeAuthCallbacksAdapter(callbacks, m), m.backup.TriggerBackup)
	if err != nil {
		return nil, err
	}

	net.network.AddIdentity(identity.ID, time.Time{}, true)
	jww.INFO.Printf("Client logged in: \n\tReceptionID: %s", identity.ID)
	return m, err
}

// loadOrInitE2eLegacy loads the e2e.Handler or makes a new one, generating a
// new E2E private key. It attempts to load via a legacy construction, then
// tries to load the modern one, creating a new modern ID if neither can be
// found.
func loadOrInitE2eLegacy(identity ReceptionIdentity, net *Cmix) (e2e.Handler, error) {
	e2eGrp := net.GetStorage().GetE2EGroup()
	kv := net.GetStorage().GetKV()

	// Try to load a legacy e2e handler
	e2eHandler, err := e2e.LoadLegacy(kv,
		net.GetCmix(), identity.ID, e2eGrp, net.GetRng(),
		net.GetEventReporter(), rekey.GetDefaultParams())
	if err != nil {
		jww.DEBUG.Printf("e2e.LoadLegacy error: %v", err)
		// If no legacy e2e handler exists, try to load a new one
		e2eHandler, err = e2e.Load(kv,
			net.GetCmix(), identity.ID, e2eGrp, net.GetRng(),
			net.GetEventReporter())
		if err != nil {
			jww.WARN.Printf("Failed to load e2e instance for %s, "+
				"creating a new one: %v", identity.ID, err)

			// Initialize the e2e storage
			privKey, err := identity.GetDHKeyPrivate()
			if err != nil {
				return nil, err
			}
			err = e2e.Init(kv, identity.ID, privKey, e2eGrp,
				rekey.GetDefaultParams())
			if err != nil {
				return nil, err
			}

			// Load the new e2e storage
			e2eHandler, err = e2e.Load(kv,
				net.GetCmix(), identity.ID, e2eGrp, net.GetRng(),
				net.GetEventReporter())
			if err != nil {
				return nil, errors.WithMessage(err,
					"Failed to load a newly created e2e store")
			}
		} else {
			jww.INFO.Printf("Loaded a modern e2e instance for %s", identity.ID)
		}
	} else {
		jww.INFO.Printf("Loaded a legacy e2e instance for %s", identity.ID)
	}

	return e2eHandler, nil
}

// GetReceptionIdentity returns a safe copy of the E2e ReceptionIdentity.
func (m *E2e) GetReceptionIdentity() ReceptionIdentity {
	return m.e2eIdentity.DeepCopy()
}

// ConstructProtoUserFile is a helper function that is used for proto client
// testing. This is used for development testing.
func (m *E2e) ConstructProtoUserFile() ([]byte, error) {

	// load the registration code
	regCode, err := m.GetStorage().GetRegCode()
	if err != nil {
		return nil, errors.WithMessage(err,
			"failed to register with permissioning")
	}

	transIdentity := m.Cmix.GetTransmissionIdentity()
	receptionIdentity := m.GetReceptionIdentity()
	privatePem, err := receptionIdentity.GetRSAPrivateKey()
	if err != nil {
		return nil, err
	}

	Usr := user.Proto{
		TransmissionID:        transIdentity.ID,
		TransmissionSalt:      transIdentity.Salt,
		TransmissionRSA:       transIdentity.RSAPrivatePem,
		ReceptionID:           receptionIdentity.ID,
		ReceptionSalt:         receptionIdentity.Salt,
		ReceptionRSA:          privatePem,
		Precanned:             m.GetStorage().IsPrecanned(),
		RegistrationTimestamp: transIdentity.RegistrationTimestamp,
		RegCode:               regCode,
		TransmissionRegValidationSig: m.GetStorage().
			GetTransmissionRegistrationValidationSignature(),
		ReceptionRegValidationSig: m.GetStorage().
			GetReceptionRegistrationValidationSignature(),
		E2eDhPrivateKey: m.e2e.GetHistoricalDHPrivkey(),
		E2eDhPublicKey:  m.e2e.GetHistoricalDHPubkey(),
	}

	jsonBytes, err := json.Marshal(Usr)
	if err != nil {
		return nil, errors.WithMessage(err,
			"failed to register with permissioning")
	}

	return jsonBytes, nil
}

// GetAuth returns the auth.State.
func (m *E2e) GetAuth() auth.State {
	return m.auth
}

// GetE2E returns the e2e.Handler.
func (m *E2e) GetE2E() e2e.Handler {
	return m.e2e
}

// GetBackupContainer returns the backup Container.
func (m *E2e) GetBackupContainer() *Container {
	return m.backup
}

// DeleteContact removes a partner from E2e's storage.
func (m *E2e) DeleteContact(partnerId *id.ID) error {
	jww.DEBUG.Printf("Deleting contact with ID %s", partnerId)

	_, err := m.e2e.GetPartner(partnerId)
	if err != nil {
		return errors.WithMessagef(err, "Could not delete %s because "+
			"they could not be found", partnerId)
	}

	if err = m.e2e.DeletePartner(partnerId); err != nil {
		return err
	}

	m.backup.TriggerBackup("contact deleted")

	// FIXME: Do we need this?
	// c.e2e.Conversations().Delete(partnerId)

	// call delete requests to make sure nothing is lingering.
	// this is for safety to ensure the contact can be re-added
	// in the future
	_ = m.auth.DeleteRequest(partnerId)

	return nil
}

// DeleteContactNotify removes a partner from E2e's storage and sends an E2E
// message to the contact notifying them.
func (m *E2e) DeleteContactNotify(partnerId *id.ID, params e2e.Params) error {
	jww.DEBUG.Printf("Deleting contact with ID %s", partnerId)

	_, err := m.e2e.GetPartner(partnerId)
	if err != nil {
		return errors.WithMessagef(err, "Could not delete %s because "+
			"they could not be found", partnerId)
	}

	if err = m.e2e.DeletePartnerNotify(partnerId, params); err != nil {
		return err
	}

	m.backup.TriggerBackup("contact deleted")

	// FIXME: Do we need this?
	// c.e2e.Conversations().Delete(partnerId)

	// call delete requests to make sure nothing is lingering.
	// this is for safety to ensure the contact can be re-added
	// in the future
	_ = m.auth.DeleteRequest(partnerId)

	return nil
}

// MakeAuthCallbacksAdapter creates an authCallbacksAdapter.
func MakeAuthCallbacksAdapter(ac AuthCallbacks, e2e *E2e) *authCallbacksAdapter {
	return &authCallbacksAdapter{
		ac:  ac,
		e2e: e2e,
	}
}

// authCallbacksAdapter is an adapter type to make the AuthCallbacks type
// compatible with the auth.Callbacks type.
type authCallbacksAdapter struct {
	ac  AuthCallbacks
	e2e *E2e
}

// MakeAuthCB generates a new auth.Callbacks with the given AuthCallbacks.
func MakeAuthCB(e2e *E2e, cbs AuthCallbacks) auth.Callbacks {
	return &authCallbacksAdapter{
		ac:  cbs,
		e2e: e2e,
	}
}

// Request will be called when an auth Request message is processed.
func (aca *authCallbacksAdapter) Request(partner contact.Contact,
	receptionID receptionID.EphemeralIdentity, round rounds.Round) {
	aca.ac.Request(partner, receptionID, round, aca.e2e)
}

// Confirm will be called when an auth Confirm message is processed.
func (aca *authCallbacksAdapter) Confirm(partner contact.Contact,
	receptionID receptionID.EphemeralIdentity, round rounds.Round) {
	aca.ac.Confirm(partner, receptionID, round, aca.e2e)
}

// Reset will be called when an auth Reset operation occurs.
func (aca *authCallbacksAdapter) Reset(partner contact.Contact,
	receptionID receptionID.EphemeralIdentity, round rounds.Round) {
	aca.ac.Reset(partner, receptionID, round, aca.e2e)
}

// DefaultAuthCallbacks is a simple structure for providing a default
// AuthCallbacks implementation. It should generally not be used.
type DefaultAuthCallbacks struct{}

// Request will be called when an auth Request message is processed.
func (a DefaultAuthCallbacks) Request(contact.Contact,
	receptionID.EphemeralIdentity, rounds.Round, *E2e) {
	jww.ERROR.Printf("No valid auth callback assigned!")
}

// Confirm will be called when an auth Confirm message is processed.
func (a DefaultAuthCallbacks) Confirm(contact.Contact,
	receptionID.EphemeralIdentity, rounds.Round, *E2e) {
	jww.ERROR.Printf("No valid auth callback assigned!")
}

// Reset will be called when an auth Reset operation occurs.
func (a DefaultAuthCallbacks) Reset(contact.Contact,
	receptionID.EphemeralIdentity, rounds.Round, *E2e) {
	jww.ERROR.Printf("No valid auth callback assigned!")
}