Skip to content
Snippets Groups Projects
Select Git revision
  • 2c74045e820c68fe8a1122360f2b61f169977a76
  • main default protected
  • dev protected
  • hotfixes-oct-2022
  • refactor/avatar-cell
  • 1.1.5
  • 1.1.4
  • 1.1.3
  • 1.1
  • 1.0.8
  • 1.0.7
  • 1.0.6
12 results

CellConfigurator.swift

Blame
  • notifications.go 13.14 KiB
    ////////////////////////////////////////////////////////////////////////////////
    // Copyright © 2020 Privategrity Corporation                                   /
    //                                                                             /
    // All rights reserved.                                                        /
    ////////////////////////////////////////////////////////////////////////////////
    
    // This file contains the main logic for notifications, including the main implementation and core loop
    
    package notifications
    
    import (
    	"encoding/base64"
    	"gitlab.com/elixxir/notifications-bot/notifications/apns"
    	"sync"
    
    	// "github.com/jonahh-yeti/apns"
    	"github.com/pkg/errors"
    	"github.com/sideshow/apns2"
    	"github.com/sideshow/apns2/payload"
    	apnstoken "github.com/sideshow/apns2/token"
    	jww "github.com/spf13/jwalterweatherman"
    	pb "gitlab.com/elixxir/comms/mixmessages"
    	"gitlab.com/elixxir/comms/network"
    	"gitlab.com/elixxir/comms/notificationBot"
    	"gitlab.com/elixxir/crypto/hash"
    	"gitlab.com/elixxir/crypto/registration"
    	"gitlab.com/elixxir/notifications-bot/notifications/firebase"
    	"gitlab.com/elixxir/notifications-bot/storage"
    	"gitlab.com/xx_network/comms/connect"
    	"gitlab.com/xx_network/crypto/signature/rsa"
    	"gitlab.com/xx_network/primitives/id"
    	"gitlab.com/xx_network/primitives/id/ephemeral"
    	"gitlab.com/xx_network/primitives/ndf"
    	"gitlab.com/xx_network/primitives/netTime"
    	"gitlab.com/xx_network/primitives/utils"
    	"gorm.io/gorm"
    	"strings"
    	"time"
    )
    
    // Function type definitions for the main operations (poll and notify)
    type NotifyFunc func(*pb.NotificationData, *apns.ApnsComm, *firebase.FirebaseComm, *storage.Storage) error
    
    // Params struct holds info passed in for configuration
    type Params struct {
    	Address  string
    	CertPath string
    	KeyPath  string
    	FBCreds  string
    	APNS     APNSParams
    }
    type APNSParams struct {
    	KeyPath  string
    	KeyID    string
    	Issuer   string
    	BundleID string
    	Dev      bool
    }
    
    // Local impl for notifications; holds comms, storage object, creds and main functions
    type Impl struct {
    	Comms       *notificationBot.Comms
    	Storage     *storage.Storage
    	inst        *network.Instance
    	notifyFunc  NotifyFunc
    	fcm         *firebase.FirebaseComm
    	apnsClient  *apns.ApnsComm
    	receivedNdf *uint32
    	roundStore  sync.Map
    
    	ndfStopper Stopper
    }
    
    // StartNotifications creates an Impl from the information passed in
    func StartNotifications(params Params, noTLS, noFirebase bool) (*Impl, error) {
    	var cert, key []byte
    	var err error
    
    	// Read in private key
    	if params.KeyPath != "" {
    		key, err = utils.ReadFile(params.KeyPath)
    		if err != nil {
    			return nil, errors.Wrapf(err, "failed to read key at %+v", params.KeyPath)
    		}
    	} else {
    		jww.WARN.Println("Running without key...")
    	}
    
    	if !noTLS {
    		// Read in TLS keys from files
    		cert, err = utils.ReadFile(params.CertPath)
    		if err != nil {
    			return nil, errors.Wrapf(err, "failed to read certificate at %+v", params.CertPath)
    		}
    	}
    
    	// Set up firebase messaging client
    	var fbComm *firebase.FirebaseComm
    	if !noFirebase {
    		app, err := firebase.SetupMessagingApp(params.FBCreds)
    		if err != nil {
    			return nil, errors.Wrap(err, "Failed to setup firebase messaging app")
    		}
    		fbComm = firebase.NewFirebaseComm(app)
    	}
    	receivedNdf := uint32(0)
    
    	impl := &Impl{
    		notifyFunc:  notifyUser,
    		fcm:         fbComm,
    		receivedNdf: &receivedNdf,
    	}
    
    	if params.APNS.KeyPath == "" {
    		jww.WARN.Println("WARNING: RUNNING WITHOUT APNS")
    	} else {
    		if params.APNS.KeyID == "" || params.APNS.Issuer == "" || params.APNS.BundleID == "" {
    			return nil, errors.WithMessagef(err, "APNS not properly configured: %+v", params.APNS)
    		}
    
    		authKey, err := apnstoken.AuthKeyFromFile(params.APNS.KeyPath)
    		if err != nil {
    			return nil, errors.WithMessage(err, "Failed to load auth key from file")
    		}
    		token := &apnstoken.Token{
    			AuthKey: authKey,
    			// KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
    			KeyID: params.APNS.KeyID,
    			// TeamID from developer account (View Account -> Membership)
    			TeamID: params.APNS.Issuer,
    		}
    		apnsClient := apns2.NewTokenClient(token)
    		if params.APNS.Dev {
    			jww.INFO.Printf("Running with dev apns gateway")
    			apnsClient.Development()
    		} else {
    			apnsClient.Production()
    		}
    
    		impl.apnsClient = apns.NewApnsComm(apnsClient, params.APNS.BundleID)
    	}
    
    	// Start notification comms server
    	handler := NewImplementation(impl)
    	comms := notificationBot.StartNotificationBot(&id.NotificationBot, params.Address, handler, cert, key)
    	impl.Comms = comms
    	i, err := network.NewInstance(impl.Comms.ProtoComms, &ndf.NetworkDefinition{AddressSpace: []ndf.AddressSpace{{Size: 16, Timestamp: netTime.Now()}}}, nil, nil, network.None, false)
    	if err != nil {
    		return nil, errors.WithMessage(err, "Failed to start instance")
    	}
    	i.SetGatewayAuthentication()
    	impl.inst = i
    
    	go impl.Cleaner()
    
    	return impl, nil
    }
    
    // NewImplementation initializes impl object
    func NewImplementation(instance *Impl) *notificationBot.Implementation {
    	impl := notificationBot.NewImplementation()
    
    	impl.Functions.RegisterForNotifications = func(request *pb.NotificationRegisterRequest) error {
    		return instance.RegisterForNotifications(request)
    	}
    
    	impl.Functions.UnregisterForNotifications = func(request *pb.NotificationUnregisterRequest) error {
    		return instance.UnregisterForNotifications(request)
    	}
    
    	impl.Functions.ReceiveNotificationBatch = func(data *pb.NotificationBatch, auth *connect.Auth) error {
    		return instance.ReceiveNotificationBatch(data, auth)
    	}
    
    	return impl
    }
    
    // NotifyUser accepts a UID and service key file path.
    // It handles the logic involved in retrieving a user's token and sending the notification
    func notifyUser(data *pb.NotificationData, apnsClient *apns.ApnsComm, fc *firebase.FirebaseComm, db *storage.Storage) error {
    	elist, err := db.GetEphemeral(data.EphemeralID)
    	if err != nil {
    		if errors.Is(err, gorm.ErrRecordNotFound) {
    			jww.DEBUG.Printf("No registration found for ephemeral ID %+v", data.EphemeralID)
    			// This path is not an error.  if no results are returned, the user hasn't registered for notifications
    			return nil
    		}
    		return errors.WithMessagef(err, "Could not retrieve registration for ephemeral ID %+v", data.EphemeralID)
    	}
    	for _, e := range elist {
    		u, err := db.GetUserByHash(e.TransmissionRSAHash)
    		if err != nil {
    			return errors.WithMessagef(err, "Failed to lookup user with tRSA hash %+v", e.TransmissionRSAHash)
    		}
    
    		isAPNS := !strings.Contains(u.Token, ":")
    		// mutableContent := 1
    		if isAPNS {
    			jww.INFO.Printf("Notifying ephemeral ID %+v via APNS to token %+v", data.EphemeralID, u.Token)
    			notifPayload := payload.NewPayload().AlertTitle("Privacy: protected!").AlertBody(
    				"Some notifications are not for you to ensure privacy; we hope to remove this notification soon").MutableContent().Custom(
    				"messagehash", base64.StdEncoding.EncodeToString(data.MessageHash)).Custom(
    				"identityfingerprint", base64.StdEncoding.EncodeToString(data.IdentityFP))
    			notif := &apns2.Notification{
    				CollapseID:  base64.StdEncoding.EncodeToString(u.TransmissionRSAHash),
    				DeviceToken: u.Token,
    				Expiration:  time.Now().Add(time.Hour * 24 * 7),
    				Priority:    apns2.PriorityHigh,
    				Payload:     notifPayload,
    				PushType:    apns2.PushTypeAlert,
    				Topic:       apnsClient.GetTopic(),
    			}
    			resp, err := apnsClient.Push(notif)
    			if err != nil {
    				jww.ERROR.Printf("Failed to send notification via APNS: %+v: %+v", resp, err)
    				// TODO : Should be re-enabled for specific error cases? deep dive on apns docs may be helpful
    				//err := db.DeleteUserByHash(u.TransmissionRSAHash)
    				//if err != nil {
    				//	return errors.WithMessagef(err, "Failed to remove user registration tRSA hash: %+v", u.TransmissionRSAHash)
    				//}
    			} else {
    				jww.INFO.Printf("Notified ephemeral ID %+v [%+v] and received response %+v", data.EphemeralID, u.Token, resp)
    			}
    		} else {
    			resp, err := fc.SendNotification(fc.Client, u.Token, data)
    			if err != nil {
    				// Catch two firebase errors that we don't want to crash on
    				// 400 and 404 indicate that the token stored is incorrect
    				// this means rather than crashing we should log and unregister the user
    				// Error documentation: https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode
    				// Stale token documentation: https://firebase.google.com/docs/cloud-messaging/manage-tokens
    				if strings.Contains(err.Error(), "400") || strings.Contains(err.Error(), "404") {
    					jww.ERROR.Printf("User with Transmission RSA hash %+v has invalid token, unregistering...", u.TransmissionRSAHash)
    					err := db.DeleteUserByHash(u.TransmissionRSAHash)
    					if err != nil {
    						return errors.WithMessagef(err, "Failed to remove user registration tRSA hash: %+v", u.TransmissionRSAHash)
    					}
    				} else {
    					jww.ERROR.Printf("Error sending notification: %+v", err)
    					return errors.WithMessagef(err, "Failed to send notification to user with tRSA hash %+v", u.TransmissionRSAHash)
    				}
    			}
    			jww.INFO.Printf("Notified ephemeral ID %+v [%+v] and received response %+v", data.EphemeralID, u.Token, resp)
    		}
    	}
    	return nil
    }
    
    // RegisterForNotifications is called by the client, and adds a user registration to our database
    func (nb *Impl) RegisterForNotifications(request *pb.NotificationRegisterRequest) error {
    	var err error
    	// Check auth & inputs
    	if string(request.Token) == "" {
    		return errors.New("Cannot register for notifications with empty client token")
    	}
    
    	// Verify permissioning RSA signature
    	permHost, ok := nb.Comms.GetHost(&id.Permissioning)
    	if !ok {
    		return errors.New("Could not find permissioning host to verify client signature")
    	}
    	err = registration.VerifyWithTimestamp(permHost.GetPubKey(), request.RegistrationTimestamp,
    		string(request.TransmissionRsa), request.TransmissionRsaSig)
    	if err != nil {
    		return errors.WithMessage(err, "Failed to verify perm sig with timestamp")
    	}
    
    	// Verify IID transmission RSA signature
    	h, err := hash.NewCMixHash()
    	if err != nil {
    		return errors.WithMessage(err, "Failed to create cmix hash")
    	}
    	_, err = h.Write(request.IntermediaryId)
    	if err != nil {
    		return errors.Wrap(err, "Failed to write intermediary id to hash")
    	}
    	pub, err := rsa.LoadPublicKeyFromPem(request.TransmissionRsa)
    	if err != nil {
    		return errors.WithMessage(err, "Failed to load public key from bytes")
    	}
    	err = rsa.Verify(pub, hash.CMixHash, h.Sum(nil), request.IIDTransmissionRsaSig, nil)
    	if err != nil {
    		return errors.Wrap(err, "Failed to verify IID signature from client")
    	}
    
    	// Add the user to storage
    	u, err := nb.Storage.AddUser(request.IntermediaryId, request.TransmissionRsa, request.IIDTransmissionRsaSig, request.Token)
    	if err != nil {
    		return errors.Wrap(err, "Failed to register user with notifications")
    	}
    	_, epoch := ephemeral.HandleQuantization(time.Now())
    	def := nb.inst.GetPartialNdf()
    	// FIXME: Does the address space need more logic here?
    	e, err := nb.Storage.AddLatestEphemeral(u, epoch, uint(def.Get().AddressSpace[0].Size))
    	if err != nil {
    		return errors.WithMessage(err, "Failed to add ephemeral ID for user")
    	}
    	jww.INFO.Printf("Added ephemeral ID %+v for user %+v", e.EphemeralId, u.IntermediaryId)
    
    	return nil
    }
    
    // UnregisterForNotifications is called by the client, and removes a user registration from our database
    func (nb *Impl) UnregisterForNotifications(request *pb.NotificationUnregisterRequest) error {
    	h, err := hash.NewCMixHash()
    	if err != nil {
    		return errors.WithMessage(err, "Failed to create cmix hash")
    	}
    	_, err = h.Write(request.IntermediaryId)
    	if err != nil {
    		return errors.WithMessage(err, "Failed to write intermediary id to hash")
    	}
    
    	u, err := nb.Storage.GetUser(request.IntermediaryId)
    	if err != nil {
    		return errors.WithMessagef(err, "Failed to find user with intermediary ID %+v", request.IntermediaryId)
    	}
    
    	pub, err := rsa.LoadPublicKeyFromPem(u.TransmissionRSA)
    	if err != nil {
    		return errors.WithMessage(err, "Failed to load public key from database")
    	}
    	err = rsa.Verify(pub, hash.CMixHash, h.Sum(nil), request.IIDTransmissionRsaSig, nil)
    	if err != nil {
    		return errors.Wrap(err, "Failed to verify IID signature from client")
    	}
    	err = nb.Storage.DeleteUserByHash(u.TransmissionRSAHash)
    	if err != nil {
    		return errors.Wrap(err, "Failed to unregister user with notifications")
    	}
    	return nil
    }
    
    // ReceiveNotificationBatch receives the batch of notification data from gateway.
    func (nb *Impl) ReceiveNotificationBatch(notifBatch *pb.NotificationBatch, auth *connect.Auth) error {
    	//if !auth.IsAuthenticated {
    	//	return errors.New("Cannot receive notification data: client is not authenticated")
    	//}
    
    	rid := notifBatch.RoundID
    
    	_, loaded := nb.roundStore.LoadOrStore(rid, time.Now())
    	if loaded {
    		jww.DEBUG.Printf("Dropping duplicate notification batch for round %+v", notifBatch.RoundID)
    		return nil
    	}
    
    	jww.INFO.Printf("Received notification batch for round %+v", notifBatch.RoundID)
    
    	for _, notifData := range notifBatch.GetNotifications() {
    		err := nb.notifyFunc(notifData, nb.apnsClient, nb.fcm, nb.Storage)
    		if err != nil {
    			return err
    		}
    	}
    
    	return nil
    }
    
    func (nb *Impl) ReceivedNdf() *uint32 {
    	return nb.receivedNdf
    }
    
    func (nb *Impl) Cleaner() {
    	for {
    		f := func(key, val interface{}) bool {
    			t := val.(time.Time)
    			if time.Since(t) > (5 * time.Minute) {
    				nb.roundStore.Delete(key)
    			}
    			return true
    		}
    		nb.roundStore.Range(f)
    		time.Sleep(time.Minute * 10)
    	}
    }