2020-07-22 07:41:40 +00:00
package pushnotificationclient
2020-06-30 07:50:59 +00:00
import (
2020-07-14 14:07:19 +00:00
"bytes"
2020-07-07 13:55:24 +00:00
"context"
2020-06-30 07:50:59 +00:00
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/rand"
2020-07-09 16:52:26 +00:00
"encoding/hex"
2020-07-14 14:07:19 +00:00
"encoding/json"
2020-07-06 08:54:22 +00:00
"errors"
2020-06-30 07:50:59 +00:00
"io"
2020-07-15 10:29:16 +00:00
"math"
2020-08-25 09:46:01 +00:00
mrand "math/rand"
2020-07-07 13:55:24 +00:00
"time"
2020-06-30 07:50:59 +00:00
2020-07-07 13:55:24 +00:00
"github.com/golang/protobuf/proto"
2020-06-30 07:50:59 +00:00
"github.com/google/uuid"
2020-07-22 07:41:40 +00:00
"go.uber.org/zap"
2020-06-30 07:50:59 +00:00
2024-09-26 22:37:32 +00:00
gocommon "github.com/status-im/status-go/common"
2020-07-10 13:26:06 +00:00
"github.com/status-im/status-go/eth-node/crypto"
2020-06-30 07:50:59 +00:00
"github.com/status-im/status-go/eth-node/crypto/ecies"
2020-07-14 14:07:19 +00:00
"github.com/status-im/status-go/eth-node/types"
2020-07-06 08:54:22 +00:00
"github.com/status-im/status-go/protocol/common"
2020-06-30 07:50:59 +00:00
"github.com/status-im/status-go/protocol/protobuf"
)
2020-07-22 07:41:40 +00:00
// How does sending notifications work?
// 1) Every time a message is scheduled for sending, it will be received on a channel.
// we keep track on whether we should send a push notification for this message.
// 2) Every time a message is dispatched, we check whether we should send a notification.
// If so, we query the user info if necessary, check which installations we should be targeting
// and notify the server if we have information about the user (i.e a token).
// The logic is complicated by the fact that sometimes messages are batched together (datasync)
// and the fact that sometimes we send messages to all devices (dh messages).
// 3) The server will notify us if the wrong token is used, in which case a loop will be started that
// will re-query and re-send the notification, up to a maximum.
// How does registering works?
// We register with the server asynchronously, through a loop, that will try to make sure that
// we have registered with all the servers added, until eventually it gives up.
// A lot of the logic is complicated by the fact that waku/whisper is not req/response, so we just fire a message
// hoping to get a reply at some later stages.
2020-07-07 13:55:24 +00:00
const encryptedPayloadKeyLength = 16
2020-06-30 07:50:59 +00:00
const accessTokenKeyLength = 16
2020-07-10 13:26:06 +00:00
const staleQueryTimeInSeconds = 86400
2020-09-02 14:11:16 +00:00
const mentionInstallationID = "mention"
2020-09-07 08:25:57 +00:00
const oneToOneChatIDLength = 132
2020-06-30 07:50:59 +00:00
2020-07-20 12:46:15 +00:00
// maxRegistrationRetries is the maximum number of attempts we do before giving up registering with a server
const maxRegistrationRetries int64 = 12
// maxPushNotificationRetries is the maximum number of attempts before we give up sending a push notification
const maxPushNotificationRetries int64 = 4
// pushNotificationBackoffTime is the step of the exponential backoff
const pushNotificationBackoffTime int64 = 2
2020-07-15 10:29:16 +00:00
// RegistrationBackoffTime is the step of the exponential backoff
const RegistrationBackoffTime int64 = 15
2020-08-25 09:46:01 +00:00
// defaultPushNotificationsServerCount is how many push notification servers we should register with if none is selected
const defaultPushNotificationsServersCount = 3
2020-08-20 09:05:39 +00:00
type ServerType int
const (
ServerTypeDefault = iota + 1
ServerTypeCustom
)
2020-06-30 07:50:59 +00:00
type PushNotificationServer struct {
2020-07-15 10:29:16 +00:00
PublicKey * ecdsa . PublicKey ` json:"-" `
Registered bool ` json:"registered,omitempty" `
RegisteredAt int64 ` json:"registeredAt,omitempty" `
LastRetriedAt int64 ` json:"lastRetriedAt,omitempty" `
RetryCount int64 ` json:"retryCount,omitempty" `
AccessToken string ` json:"accessToken,omitempty" `
2020-08-20 09:05:39 +00:00
Type ServerType ` json:"type,omitempty" `
2020-07-14 14:07:19 +00:00
}
func ( s * PushNotificationServer ) MarshalJSON ( ) ( [ ] byte , error ) {
type ServerAlias PushNotificationServer
item := struct {
* ServerAlias
PublicKeyString string ` json:"publicKey" `
} {
ServerAlias : ( * ServerAlias ) ( s ) ,
PublicKeyString : types . EncodeHex ( crypto . FromECDSAPub ( s . PublicKey ) ) ,
}
return json . Marshal ( item )
2020-07-07 09:00:04 +00:00
}
type PushNotificationInfo struct {
2020-07-10 13:26:06 +00:00
AccessToken string
InstallationID string
PublicKey * ecdsa . PublicKey
ServerPublicKey * ecdsa . PublicKey
RetrievedAt int64
2020-07-17 11:41:49 +00:00
Version uint64
2020-06-30 07:50:59 +00:00
}
2020-07-20 10:01:42 +00:00
type SentNotification struct {
2020-09-02 14:11:16 +00:00
PublicKey * ecdsa . PublicKey
InstallationID string
LastTriedAt int64
RetryCount int64
MessageID [ ] byte
ChatID string
NotificationType protobuf . PushNotification_PushNotificationType
Success bool
Error protobuf . PushNotificationReport_ErrorType
}
type RegistrationOptions struct {
2023-06-22 05:06:32 +00:00
PublicChatIDs [ ] string
MutedChatIDs [ ] string
BlockedChatIDs [ ] string
ContactIDs [ ] * ecdsa . PublicKey
2020-07-20 10:01:42 +00:00
}
func ( s * SentNotification ) HashedPublicKey ( ) [ ] byte {
return common . HashPublicKey ( s . PublicKey )
}
2020-06-30 08:30:58 +00:00
type Config struct {
2020-06-30 07:50:59 +00:00
// Identity is our identity key
Identity * ecdsa . PrivateKey
// SendEnabled indicates whether we should be sending push notifications
SendEnabled bool
// RemoteNotificationsEnabled is whether we should register with a remote server for push notifications
RemoteNotificationsEnabled bool
2020-07-22 07:41:40 +00:00
// AllowyFromContactsOnly indicates whether we should be receiving push notifications
2020-06-30 07:50:59 +00:00
// only from contacts
2020-07-22 07:41:40 +00:00
AllowFromContactsOnly bool
2020-07-15 10:29:16 +00:00
2020-09-02 14:11:16 +00:00
// BlockMentions indicates whether we should not receive notification for mentions
BlockMentions bool
2020-06-30 07:50:59 +00:00
// InstallationID is the installation-id for this device
InstallationID string
2020-07-07 13:55:24 +00:00
Logger * zap . Logger
2020-08-20 09:05:39 +00:00
// DefaultServers holds the push notification servers used by
// default if none is selected
DefaultServers [ ] * ecdsa . PublicKey
2020-06-30 07:50:59 +00:00
}
2020-09-02 14:11:16 +00:00
type MessagePersistence interface {
MessageByID ( string ) ( * common . Message , error )
}
2020-06-30 08:30:58 +00:00
type Client struct {
2020-09-02 14:11:16 +00:00
persistence * Persistence
messagePersistence MessagePersistence
2020-07-22 07:41:40 +00:00
config * Config
2020-06-30 07:50:59 +00:00
2020-07-15 07:23:31 +00:00
// lastPushNotificationRegistration is the latest known push notification version
lastPushNotificationRegistration * protobuf . PushNotificationRegistration
2020-06-30 07:50:59 +00:00
2020-07-15 08:22:43 +00:00
// lastContactIDs is the latest contact ids array
lastContactIDs [ ] * ecdsa . PublicKey
2020-06-30 07:50:59 +00:00
// AccessToken is the access token that is currently being used
AccessToken string
2020-07-17 12:29:51 +00:00
// deviceToken is the device token for this device
deviceToken string
2020-07-30 08:24:30 +00:00
// TokenType is the type of token
tokenType protobuf . PushNotificationRegistration_TokenType
// APNTopic is the topic of the apn topic for push notification
apnTopic string
2020-06-30 07:50:59 +00:00
// randomReader only used for testing so we have deterministic encryption
reader io . Reader
2020-07-06 08:54:22 +00:00
2021-06-23 14:13:48 +00:00
//messageSender used to send and being notified of messages
messageSender * common . MessageSender
2020-07-15 10:29:16 +00:00
// registrationLoopQuitChan is a channel to indicate to the registration loop that should be terminating
registrationLoopQuitChan chan struct { }
2020-07-20 12:46:15 +00:00
2020-07-22 07:41:40 +00:00
// resendingLoopQuitChan is a channel to indicate to the send loop that should be terminating
2020-07-20 12:46:15 +00:00
resendingLoopQuitChan chan struct { }
2020-07-22 07:41:40 +00:00
quit chan struct { }
2020-08-18 15:07:48 +00:00
// registrationSubscriptions is a list of chan of client subscribed to the registration event
registrationSubscriptions [ ] chan struct { }
2021-09-28 10:25:13 +00:00
// pendingRegistrations is a map of pending registrations.
// in theory we should store them in the database, but for now we can keep them in memory at
// the cost of having to register multiple times in case the program stops
pendingRegistrations map [ string ] bool
2020-07-06 08:54:22 +00:00
}
2021-06-23 14:13:48 +00:00
func New ( persistence * Persistence , config * Config , sender * common . MessageSender , messagePersistence MessagePersistence ) * Client {
2020-07-06 08:54:22 +00:00
return & Client {
2021-09-28 10:25:13 +00:00
quit : make ( chan struct { } ) ,
config : config ,
messageSender : sender ,
messagePersistence : messagePersistence ,
persistence : persistence ,
pendingRegistrations : make ( map [ string ] bool ) ,
reader : rand . Reader ,
2020-07-15 10:29:16 +00:00
}
2020-07-06 08:54:22 +00:00
}
2020-07-15 07:23:31 +00:00
func ( c * Client ) Start ( ) error {
2021-06-23 14:13:48 +00:00
if c . messageSender == nil {
return errors . New ( "can't start, missing message sender" )
2020-07-15 07:23:31 +00:00
}
err := c . loadLastPushNotificationRegistration ( )
if err != nil {
return err
}
2020-07-22 07:41:40 +00:00
2020-09-02 14:11:16 +00:00
c . subscribeForMessageEvents ( )
2020-07-30 13:01:49 +00:00
// We start even if push notifications are disabled, as we might
// actually be sending an unregister message
2020-07-15 10:29:16 +00:00
c . startRegistrationLoop ( )
2020-07-30 13:01:49 +00:00
2020-07-20 12:46:15 +00:00
c . startResendingLoop ( )
2020-07-15 07:23:31 +00:00
2020-07-06 08:54:22 +00:00
return nil
2020-06-30 07:50:59 +00:00
}
2020-08-27 12:38:59 +00:00
func ( c * Client ) Offline ( ) {
c . stopRegistrationLoop ( )
c . stopResendingLoop ( )
}
func ( c * Client ) Online ( ) {
c . startRegistrationLoop ( )
c . startResendingLoop ( )
}
2020-08-18 15:07:48 +00:00
func ( c * Client ) publishOnRegistrationSubscriptions ( ) {
// Publish on channels, drop if buffer is full
for _ , s := range c . registrationSubscriptions {
select {
case s <- struct { } { } :
default :
c . config . Logger . Warn ( "subscription channel full, dropping message" )
}
}
}
func ( c * Client ) quitRegistrationSubscriptions ( ) {
for _ , s := range c . registrationSubscriptions {
close ( s )
}
}
2020-07-06 08:54:22 +00:00
func ( c * Client ) Stop ( ) error {
close ( c . quit )
2020-07-20 12:46:15 +00:00
c . stopRegistrationLoop ( )
c . stopResendingLoop ( )
2020-08-18 15:07:48 +00:00
c . quitRegistrationSubscriptions ( )
2020-07-06 08:54:22 +00:00
return nil
2020-06-30 07:50:59 +00:00
}
2020-07-22 07:41:40 +00:00
// Unregister unregisters from all the servers
func ( c * Client ) Unregister ( ) error {
// stop registration loop
c . stopRegistrationLoop ( )
2020-07-30 13:01:49 +00:00
c . config . RemoteNotificationsEnabled = false
2020-07-22 07:41:40 +00:00
registration := c . buildPushNotificationUnregisterMessage ( )
err := c . saveLastPushNotificationRegistration ( registration , nil )
2020-07-20 10:01:42 +00:00
if err != nil {
return err
}
2020-07-22 07:41:40 +00:00
// reset servers
err = c . resetServers ( )
if err != nil {
return err
2020-07-20 10:01:42 +00:00
}
2020-07-22 07:41:40 +00:00
// and asynchronously register
c . startRegistrationLoop ( )
2020-07-20 10:01:42 +00:00
return nil
}
2020-07-22 07:41:40 +00:00
// Registered returns true if we registered with all the servers
func ( c * Client ) Registered ( ) ( bool , error ) {
servers , err := c . persistence . GetServers ( )
if err != nil {
return false , err
2020-07-10 13:26:06 +00:00
}
2020-07-14 14:07:19 +00:00
2020-07-22 07:41:40 +00:00
for _ , s := range servers {
if ! s . Registered {
return false , nil
2020-07-14 14:07:19 +00:00
}
}
2020-07-22 07:41:40 +00:00
return true , nil
}
2020-07-10 13:26:06 +00:00
2020-08-18 15:07:48 +00:00
func ( c * Client ) SubscribeToRegistrations ( ) chan struct { } {
s := make ( chan struct { } , 100 )
c . registrationSubscriptions = append ( c . registrationSubscriptions , s )
return s
}
2020-07-22 07:41:40 +00:00
func ( c * Client ) GetSentNotification ( hashedPublicKey [ ] byte , installationID string , messageID [ ] byte ) ( * SentNotification , error ) {
return c . persistence . GetSentNotification ( hashedPublicKey , installationID , messageID )
}
2020-07-10 13:26:06 +00:00
2020-07-22 07:41:40 +00:00
func ( c * Client ) GetServers ( ) ( [ ] * PushNotificationServer , error ) {
return c . persistence . GetServers ( )
}
2020-07-10 13:26:06 +00:00
2020-09-02 14:11:16 +00:00
func ( c * Client ) Reregister ( options * RegistrationOptions ) error {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "re-registering" )
if len ( c . deviceToken ) == 0 {
c . config . Logger . Info ( "no device token, not registering" )
2020-07-10 13:26:06 +00:00
return nil
}
2020-07-30 13:01:49 +00:00
if ! c . config . RemoteNotificationsEnabled {
c . config . Logger . Info ( "remote notifications not enabled, not registering" )
return nil
}
2020-09-02 14:11:16 +00:00
return c . Register ( c . deviceToken , c . apnTopic , c . tokenType , options )
2020-07-22 07:41:40 +00:00
}
2020-07-14 14:07:19 +00:00
2020-08-25 09:46:01 +00:00
// pickDefaultServesr picks n servers at random
func ( c * Client ) pickDefaultServers ( servers [ ] * ecdsa . PublicKey ) [ ] * ecdsa . PublicKey {
// shuffle and pick n at random
shuffledServers := make ( [ ] * ecdsa . PublicKey , len ( servers ) )
copy ( shuffledServers , c . config . DefaultServers )
2020-08-27 13:30:48 +00:00
mrand . Seed ( time . Now ( ) . Unix ( ) )
2020-08-25 09:46:01 +00:00
mrand . Shuffle ( len ( shuffledServers ) , func ( i , j int ) {
shuffledServers [ i ] , shuffledServers [ j ] = shuffledServers [ j ] , shuffledServers [ i ]
} )
// Take the min not to get an out of bounds slice
min := len ( c . config . DefaultServers )
if min > defaultPushNotificationsServersCount {
min = defaultPushNotificationsServersCount
}
return shuffledServers [ : min ]
}
2020-07-22 07:41:40 +00:00
// Register registers with all the servers
2020-09-02 14:11:16 +00:00
func ( c * Client ) Register ( deviceToken , apnTopic string , tokenType protobuf . PushNotificationRegistration_TokenType , options * RegistrationOptions ) error {
2020-07-22 07:41:40 +00:00
// stop registration loop
c . stopRegistrationLoop ( )
2020-07-30 13:01:49 +00:00
c . config . RemoteNotificationsEnabled = true
2020-08-20 09:05:39 +00:00
// check if we need to fallback on default servers
currentServers , err := c . persistence . GetServers ( )
if err != nil {
return err
}
2020-08-25 09:46:01 +00:00
if len ( currentServers ) == 0 && len ( c . config . DefaultServers ) != 0 {
2020-08-20 09:05:39 +00:00
c . config . Logger . Debug ( "servers empty, checking default servers" )
2020-08-25 09:46:01 +00:00
for _ , s := range c . pickDefaultServers ( c . config . DefaultServers ) {
2020-08-20 09:05:39 +00:00
err = c . AddPushNotificationsServer ( s , ServerTypeDefault )
if err != nil {
return err
}
}
}
2020-07-22 07:41:40 +00:00
// reset servers
2020-08-20 09:05:39 +00:00
err = c . resetServers ( )
2020-07-10 13:26:06 +00:00
if err != nil {
return err
}
2020-07-14 14:07:19 +00:00
2020-07-22 07:41:40 +00:00
c . deviceToken = deviceToken
2020-07-30 08:24:30 +00:00
c . apnTopic = apnTopic
c . tokenType = tokenType
2020-07-14 14:07:19 +00:00
2020-09-02 14:11:16 +00:00
registration , err := c . buildPushNotificationRegistrationMessage ( options )
2020-07-22 07:41:40 +00:00
if err != nil {
return err
}
2020-09-02 14:11:16 +00:00
err = c . saveLastPushNotificationRegistration ( registration , options . ContactIDs )
2020-07-22 07:41:40 +00:00
if err != nil {
return err
2020-07-10 13:26:06 +00:00
}
2020-07-22 07:41:40 +00:00
c . startRegistrationLoop ( )
2020-06-30 07:50:59 +00:00
return nil
}
2020-07-22 07:41:40 +00:00
// HandlePushNotificationRegistrationResponse should check whether the response was successful or not, retry if necessary otherwise store the result in the database
2023-08-18 11:39:59 +00:00
func ( c * Client ) HandlePushNotificationRegistrationResponse ( publicKey * ecdsa . PublicKey , response * protobuf . PushNotificationRegistrationResponse ) error {
if response == nil {
return nil
}
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "received push notification registration response" , zap . Any ( "response" , response ) )
2020-07-10 13:26:06 +00:00
2021-09-28 10:25:13 +00:00
if len ( response . RequestId ) == 0 {
return errors . New ( "empty requestId" )
}
if ! c . pendingRegistrations [ hex . EncodeToString ( response . RequestId ) ] {
return errors . New ( "not for one of our requests" )
}
2020-07-22 07:41:40 +00:00
// Not successful ignore for now
if ! response . Success {
return errors . New ( "response was not successful" )
2020-07-14 14:07:19 +00:00
}
2020-06-30 07:50:59 +00:00
2020-07-22 07:41:40 +00:00
servers , err := c . persistence . GetServersByPublicKey ( [ ] * ecdsa . PublicKey { publicKey } )
if err != nil {
return err
}
2020-06-30 07:50:59 +00:00
2020-07-22 07:41:40 +00:00
// we haven't registered with this server
if len ( servers ) != 1 {
return errors . New ( "not registered with this server, ignoring" )
2020-06-30 07:50:59 +00:00
}
2020-07-22 07:41:40 +00:00
server := servers [ 0 ]
server . Registered = true
server . RegisteredAt = time . Now ( ) . Unix ( )
2020-08-18 15:07:48 +00:00
err = c . persistence . UpsertServer ( server )
if err != nil {
return err
}
c . publishOnRegistrationSubscriptions ( )
return nil
2020-06-30 07:50:59 +00:00
}
2020-07-31 13:46:27 +00:00
// processQueryInfo takes info about push notifications and validates them
func ( c * Client ) processQueryInfo ( clientPublicKey * ecdsa . PublicKey , serverPublicKey * ecdsa . PublicKey , info * protobuf . PushNotificationQueryInfo ) error {
// make sure the public key matches
if ! bytes . Equal ( info . PublicKey , common . HashPublicKey ( clientPublicKey ) ) {
c . config . Logger . Warn ( "reply for different key, ignoring" )
2020-08-17 11:35:43 +00:00
return errors . New ( "reply for a different key, ignoring" )
2020-07-31 13:46:27 +00:00
}
accessToken := info . AccessToken
// the user wants notification from contacts only, try to decrypt the access token to see if we are in their contacts
if len ( accessToken ) == 0 && len ( info . AllowedKeyList ) != 0 {
accessToken = c . handleAllowedKeyList ( clientPublicKey , info . AllowedKeyList )
}
// no luck
if len ( accessToken ) == 0 {
c . config . Logger . Debug ( "not in the allowed key list" )
return nil
}
// We check the user has allowed this server to store this particular
// access token, otherwise anyone could reply with a fake token
// and receive notifications for a user
if err := c . handleGrant ( clientPublicKey , serverPublicKey , info . Grant , accessToken ) ; err != nil {
c . config . Logger . Warn ( "grant verification failed, ignoring" , zap . Error ( err ) )
return err
}
pushNotificationInfo := & PushNotificationInfo {
PublicKey : clientPublicKey ,
ServerPublicKey : serverPublicKey ,
AccessToken : accessToken ,
InstallationID : info . InstallationId ,
Version : info . Version ,
RetrievedAt : time . Now ( ) . Unix ( ) ,
}
err := c . persistence . SavePushNotificationInfo ( [ ] * PushNotificationInfo { pushNotificationInfo } )
if err != nil {
c . config . Logger . Error ( "failed to save push notifications" , zap . Error ( err ) )
return err
}
return nil
}
2020-07-22 07:41:40 +00:00
// HandlePushNotificationQueryResponse should update the data in the database for a given user
2023-08-18 11:39:59 +00:00
func ( c * Client ) HandlePushNotificationQueryResponse ( serverPublicKey * ecdsa . PublicKey , response * protobuf . PushNotificationQueryResponse ) error {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "received push notification query response" , zap . Any ( "response" , response ) )
2023-08-18 11:39:59 +00:00
if response == nil || len ( response . Info ) == 0 {
2020-07-22 07:41:40 +00:00
return errors . New ( "empty response from the server" )
2020-06-30 07:50:59 +00:00
}
2020-07-22 07:41:40 +00:00
// get the public key associated with this query
2020-07-31 13:46:27 +00:00
clientPublicKey , err := c . persistence . GetQueryPublicKey ( response . MessageId )
2020-07-17 12:29:51 +00:00
if err != nil {
2021-03-31 16:23:45 +00:00
c . config . Logger . Error ( "failed to query client publicKey" , zap . Error ( err ) )
2020-07-22 07:41:40 +00:00
return err
2020-07-17 12:29:51 +00:00
}
2020-07-31 13:46:27 +00:00
if clientPublicKey == nil {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "query not found" )
return nil
2020-07-17 12:29:51 +00:00
}
2020-07-31 13:46:27 +00:00
// process query, make sure to validate grant as coming from the server
2020-07-22 07:41:40 +00:00
for _ , info := range response . Info {
2020-07-31 13:46:27 +00:00
err := c . processQueryInfo ( clientPublicKey , serverPublicKey , info )
if err != nil {
c . config . Logger . Warn ( "failed to process info" , zap . Any ( "info" , info ) , zap . Error ( err ) )
2020-07-22 07:41:40 +00:00
continue
2020-06-30 07:50:59 +00:00
}
2020-07-31 13:46:27 +00:00
}
return nil
2020-06-30 07:50:59 +00:00
2020-07-31 13:46:27 +00:00
}
2020-07-22 07:41:40 +00:00
2020-07-31 13:46:27 +00:00
// HandleContactCodeAdvertisement checks if there are any info and process them
2023-08-18 11:39:59 +00:00
func ( c * Client ) HandleContactCodeAdvertisement ( clientPublicKey * ecdsa . PublicKey , message * protobuf . ContactCodeAdvertisement ) error {
if message == nil {
return nil
}
2020-08-18 15:07:48 +00:00
// nothing to do for our own pubkey
if common . IsPubKeyEqual ( clientPublicKey , & c . config . Identity . PublicKey ) {
return nil
}
2020-07-31 13:46:27 +00:00
c . config . Logger . Debug ( "received contact code advertisement" , zap . Any ( "advertisement" , message ) )
for _ , info := range message . PushNotificationInfo {
c . config . Logger . Debug ( "handling push notification query info" )
2020-08-18 15:07:48 +00:00
serverPublicKey , err := crypto . DecompressPubkey ( info . ServerPublicKey )
2020-07-31 13:46:27 +00:00
if err != nil {
2020-08-18 15:07:48 +00:00
c . config . Logger . Error ( "could not unmarshal server pubkey" , zap . Binary ( "server-key" , info . ServerPublicKey ) )
2020-07-31 13:46:27 +00:00
return err
2020-07-22 07:41:40 +00:00
}
2020-07-31 13:46:27 +00:00
err = c . processQueryInfo ( clientPublicKey , serverPublicKey , info )
if err != nil {
return err
2020-07-22 07:41:40 +00:00
}
}
2020-08-18 15:07:48 +00:00
// Save query so that we won't query again to early
// NOTE: this is not very accurate as we might fetch an historical message,
// prolonging the time that we fetch new info.
// Most of the times it should work fine, as if the info are stale they'd be
// fetched again because of an error response from the push notification server
return c . persistence . SavePushNotificationQuery ( clientPublicKey , [ ] byte ( uuid . New ( ) . String ( ) ) )
2020-07-22 07:41:40 +00:00
}
// HandlePushNotificationResponse should set the request as processed
2023-08-18 11:39:59 +00:00
func ( c * Client ) HandlePushNotificationResponse ( serverKey * ecdsa . PublicKey , response * protobuf . PushNotificationResponse ) error {
if response == nil {
return nil
}
2020-07-22 07:41:40 +00:00
messageID := response . MessageId
2021-10-29 14:29:28 +00:00
c . config . Logger . Debug ( "received response for" , zap . String ( "messageID" , types . EncodeHex ( messageID ) ) )
2020-07-22 07:41:40 +00:00
for _ , report := range response . Reports {
c . config . Logger . Debug ( "received response" , zap . Any ( "report" , report ) )
err := c . persistence . UpdateNotificationResponse ( messageID , report )
if err != nil {
return err
}
}
// Restart resending loop, in case we need to resend some notifications
c . stopResendingLoop ( )
c . startResendingLoop ( )
return nil
}
func ( c * Client ) RemovePushNotificationServer ( publicKey * ecdsa . PublicKey ) error {
c . config . Logger . Debug ( "removing push notification server" , zap . Any ( "public-key" , publicKey ) )
//TODO: this needs implementing. It requires unregistering from the server and
// likely invalidate the device token of the user
return errors . New ( "not implemented" )
}
2020-08-20 09:05:39 +00:00
func ( c * Client ) AddPushNotificationsServer ( publicKey * ecdsa . PublicKey , serverType ServerType ) error {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "adding push notifications server" , zap . Any ( "public-key" , publicKey ) )
currentServers , err := c . persistence . GetServers ( )
if err != nil {
return err
}
for _ , server := range currentServers {
if common . IsPubKeyEqual ( server . PublicKey , publicKey ) {
return errors . New ( "push notification server already added" )
}
}
err = c . persistence . UpsertServer ( & PushNotificationServer {
PublicKey : publicKey ,
2020-08-20 09:05:39 +00:00
Type : serverType ,
2020-07-22 07:41:40 +00:00
} )
if err != nil {
return err
}
2020-07-15 07:23:31 +00:00
2020-07-22 07:41:40 +00:00
if c . config . RemoteNotificationsEnabled {
c . startRegistrationLoop ( )
}
return nil
2020-07-15 07:23:31 +00:00
}
2020-07-22 07:41:40 +00:00
func ( c * Client ) GetPushNotificationInfo ( publicKey * ecdsa . PublicKey , installationIDs [ ] string ) ( [ ] * PushNotificationInfo , error ) {
if len ( installationIDs ) == 0 {
return c . persistence . GetPushNotificationInfoByPublicKey ( publicKey )
}
return c . persistence . GetPushNotificationInfo ( publicKey , installationIDs )
}
2020-07-31 13:46:27 +00:00
func ( c * Client ) Enabled ( ) bool {
return c . config . RemoteNotificationsEnabled
}
2020-07-22 07:41:40 +00:00
func ( c * Client ) EnableSending ( ) {
c . config . SendEnabled = true
}
func ( c * Client ) DisableSending ( ) {
c . config . SendEnabled = false
}
2020-09-02 14:11:16 +00:00
func ( c * Client ) EnablePushNotificationsFromContactsOnly ( options * RegistrationOptions ) error {
2020-07-30 07:19:34 +00:00
c . config . Logger . Debug ( "enabling push notification from contacts only" )
2020-07-22 07:41:40 +00:00
c . config . AllowFromContactsOnly = true
2020-07-30 13:01:49 +00:00
if c . lastPushNotificationRegistration != nil && c . config . RemoteNotificationsEnabled {
2020-07-30 07:19:34 +00:00
c . config . Logger . Debug ( "re-registering after enabling push notifications from contacts only" )
2020-09-02 14:11:16 +00:00
return c . Register ( c . deviceToken , c . apnTopic , c . tokenType , options )
2020-07-22 07:41:40 +00:00
}
return nil
}
2020-09-02 14:11:16 +00:00
func ( c * Client ) DisablePushNotificationsFromContactsOnly ( options * RegistrationOptions ) error {
2020-07-30 07:19:34 +00:00
c . config . Logger . Debug ( "disabling push notification from contacts only" )
2020-07-22 07:41:40 +00:00
c . config . AllowFromContactsOnly = false
2020-07-30 13:01:49 +00:00
if c . lastPushNotificationRegistration != nil && c . config . RemoteNotificationsEnabled {
2020-07-30 07:19:34 +00:00
c . config . Logger . Debug ( "re-registering after disabling push notifications from contacts only" )
2020-09-02 14:11:16 +00:00
return c . Register ( c . deviceToken , c . apnTopic , c . tokenType , options )
2020-07-22 07:41:40 +00:00
}
return nil
}
2020-09-03 07:30:03 +00:00
func ( c * Client ) EnablePushNotificationsBlockMentions ( options * RegistrationOptions ) error {
c . config . Logger . Debug ( "disabling push notifications for mentions" )
c . config . BlockMentions = true
if c . lastPushNotificationRegistration != nil && c . config . RemoteNotificationsEnabled {
c . config . Logger . Debug ( "re-registering after disabling push notifications for mentions" )
return c . Register ( c . deviceToken , c . apnTopic , c . tokenType , options )
}
return nil
}
func ( c * Client ) DisablePushNotificationsBlockMentions ( options * RegistrationOptions ) error {
c . config . Logger . Debug ( "enabling push notifications for mentions" )
c . config . BlockMentions = false
if c . lastPushNotificationRegistration != nil && c . config . RemoteNotificationsEnabled {
c . config . Logger . Debug ( "re-registering after enabling push notifications for mentions" )
return c . Register ( c . deviceToken , c . apnTopic , c . tokenType , options )
}
return nil
}
2020-07-22 07:41:40 +00:00
func encryptAccessToken ( plaintext [ ] byte , key [ ] byte , reader io . Reader ) ( [ ] byte , error ) {
c , err := aes . NewCipher ( key )
if err != nil {
return nil , err
}
gcm , err := cipher . NewGCM ( c )
if err != nil {
return nil , err
}
nonce := make ( [ ] byte , gcm . NonceSize ( ) )
if _ , err = io . ReadFull ( reader , nonce ) ; err != nil {
return nil , err
}
return gcm . Seal ( nonce , nonce , plaintext , nil ) , nil
}
func ( c * Client ) encryptRegistration ( publicKey * ecdsa . PublicKey , payload [ ] byte ) ( [ ] byte , error ) {
sharedKey , err := c . generateSharedKey ( publicKey )
if err != nil {
return nil , err
}
return common . Encrypt ( payload , sharedKey , c . reader )
}
func ( c * Client ) generateSharedKey ( publicKey * ecdsa . PublicKey ) ( [ ] byte , error ) {
return ecies . ImportECDSA ( c . config . Identity ) . GenerateShared (
ecies . ImportECDSAPublic ( publicKey ) ,
encryptedPayloadKeyLength ,
encryptedPayloadKeyLength ,
)
}
2020-09-02 14:11:16 +00:00
// subscribeForMessageEvents subscribes for newly sent/scheduled messages so we can check if we need to send a push notification
func ( c * Client ) subscribeForMessageEvents ( ) {
2020-07-22 07:41:40 +00:00
go func ( ) {
2024-09-26 22:37:32 +00:00
defer gocommon . LogOnPanic ( )
2020-09-02 14:11:16 +00:00
c . config . Logger . Debug ( "subscribing for message events" )
2021-10-07 11:10:23 +00:00
messageEventsSubscription := c . messageSender . SubscribeToMessageEvents ( )
2020-07-22 07:41:40 +00:00
for {
select {
2021-10-07 11:10:23 +00:00
case m , more := <- messageEventsSubscription :
2020-07-22 07:41:40 +00:00
if ! more {
2021-10-07 11:10:23 +00:00
c . config . Logger . Debug ( "no more message events, quitting" )
2020-07-22 07:41:40 +00:00
return
}
2021-10-07 11:10:23 +00:00
switch m . Type {
case common . MessageScheduled :
c . config . Logger . Debug ( "handling message scheduled" )
2022-07-15 13:19:37 +00:00
if err := c . handleMessageScheduled ( m ) ; err != nil {
2021-10-07 11:10:23 +00:00
c . config . Logger . Error ( "failed to handle message" , zap . Error ( err ) )
}
case common . MessageSent :
c . config . Logger . Debug ( "handling message sent" )
2022-07-15 13:19:37 +00:00
if err := c . handleMessageSent ( m ) ; err != nil {
2021-10-07 11:10:23 +00:00
c . config . Logger . Error ( "failed to handle message" , zap . Error ( err ) )
}
default :
c . config . Logger . Warn ( "message event type not supported" )
2020-07-22 07:41:40 +00:00
}
case <- c . quit :
return
}
2021-10-07 11:10:23 +00:00
2020-07-22 07:41:40 +00:00
}
} ( )
}
// loadLastPushNotificationRegistration loads from the database the last registration
func ( c * Client ) loadLastPushNotificationRegistration ( ) error {
lastRegistration , lastContactIDs , err := c . persistence . GetLastPushNotificationRegistration ( )
if err != nil {
return err
}
if lastRegistration == nil {
lastRegistration = & protobuf . PushNotificationRegistration { }
}
c . lastContactIDs = lastContactIDs
c . lastPushNotificationRegistration = lastRegistration
c . deviceToken = lastRegistration . DeviceToken
2020-07-30 08:24:30 +00:00
c . apnTopic = lastRegistration . ApnTopic
c . tokenType = lastRegistration . TokenType
2020-07-22 07:41:40 +00:00
return nil
}
func ( c * Client ) stopRegistrationLoop ( ) {
// stop old registration loop
if c . registrationLoopQuitChan != nil {
close ( c . registrationLoopQuitChan )
c . registrationLoopQuitChan = nil
}
}
func ( c * Client ) stopResendingLoop ( ) {
// stop old registration loop
if c . resendingLoopQuitChan != nil {
close ( c . resendingLoopQuitChan )
c . resendingLoopQuitChan = nil
}
}
func ( c * Client ) startRegistrationLoop ( ) {
c . stopRegistrationLoop ( )
c . registrationLoopQuitChan = make ( chan struct { } )
go func ( ) {
2024-09-26 22:37:32 +00:00
defer gocommon . LogOnPanic ( )
2020-07-22 07:41:40 +00:00
err := c . registrationLoop ( )
if err != nil {
c . config . Logger . Error ( "registration loop exited with an error" , zap . Error ( err ) )
}
} ( )
}
func ( c * Client ) startResendingLoop ( ) {
c . stopResendingLoop ( )
c . resendingLoopQuitChan = make ( chan struct { } )
go func ( ) {
2024-09-26 22:37:32 +00:00
defer gocommon . LogOnPanic ( )
2020-07-22 07:41:40 +00:00
err := c . resendingLoop ( )
if err != nil {
c . config . Logger . Error ( "resending loop exited with an error" , zap . Error ( err ) )
}
} ( )
}
// queryNotificationInfo will block and query for the client token, if force is set it
// will ignore the cool off period
func ( c * Client ) queryNotificationInfo ( publicKey * ecdsa . PublicKey , force bool ) error {
c . config . Logger . Debug ( "retrieving queried at" )
// Check if we queried recently
queriedAt , err := c . persistence . GetQueriedAt ( publicKey )
if err != nil {
c . config . Logger . Error ( "failed to retrieve queried at" , zap . Error ( err ) )
return err
}
c . config . Logger . Debug ( "checking if querying necessary" )
// Naively query again if too much time has passed.
// Here it might not be necessary
if force || time . Now ( ) . Unix ( ) - queriedAt > staleQueryTimeInSeconds {
c . config . Logger . Debug ( "querying info" )
err := c . queryPushNotificationInfo ( publicKey )
if err != nil {
c . config . Logger . Error ( "could not query pn info" , zap . Error ( err ) )
return err
}
// This is just horrible, but for now will do,
// the issue is that we don't really know how long it will
// take to reply, as there might be multiple servers
// replying to us.
// The only time we are 100% certain that we can proceed is
// when we have non-stale info for each device, but
// most devices are not going to be registered, so we'd still
// have to wait the maximum amount of time allowed.
// A better way to handle this is to set a maximum timer of say
// 3 seconds, but act at a tick every 200ms.
// That way we still are able to batch multiple push notifications
// but we don't have to wait every time 3 seconds, which is wasteful
// This probably will have to be addressed before released
time . Sleep ( 3 * time . Second )
}
return nil
}
2020-09-02 14:11:16 +00:00
// handleMessageSent is called every time a message is sent
2022-07-15 13:19:37 +00:00
func ( c * Client ) handleMessageSent ( e * common . MessageEvent ) error {
2020-09-02 14:11:16 +00:00
2022-07-15 13:19:37 +00:00
sentMessage := e . SentMessage
2020-07-22 07:41:40 +00:00
// Ignore if we are not sending notifications
if ! c . config . SendEnabled {
return nil
}
2022-07-15 13:19:37 +00:00
// check if it's for one of our devices, do nothing in that case
if e . Recipient != nil && common . IsPubKeyEqual ( e . Recipient , & c . config . Identity . PublicKey ) {
return nil
}
2020-09-02 14:11:16 +00:00
if sentMessage . PublicKey == nil {
return c . handlePublicMessageSent ( sentMessage )
}
return c . handleDirectMessageSent ( sentMessage )
}
// saving to the database might happen after we fetch the message, so we retry
// for a reasonable amount of time before giving up
func ( c * Client ) getMessage ( messageID string ) ( * common . Message , error ) {
retries := 0
for retries < 10 {
message , err := c . messagePersistence . MessageByID ( messageID )
2020-10-08 10:46:03 +00:00
if err == common . ErrRecordNotFound {
2020-09-02 14:11:16 +00:00
retries ++
time . Sleep ( 300 * time . Millisecond )
continue
} else if err != nil {
return nil , err
}
return message , nil
}
2020-10-08 10:46:03 +00:00
return nil , common . ErrRecordNotFound
2020-09-02 14:11:16 +00:00
}
// handlePublicMessageSent handles public messages, we notify only on mentions
func ( c * Client ) handlePublicMessageSent ( sentMessage * common . SentMessage ) error {
// We always expect a single message, as we never batch them
if len ( sentMessage . MessageIDs ) != 1 {
return errors . New ( "batched public messages not handled" )
}
messageID := sentMessage . MessageIDs [ 0 ]
c . config . Logger . Debug ( "handling public messages" , zap . Binary ( "messageID" , messageID ) )
tracked , err := c . persistence . TrackedMessage ( messageID )
if err != nil {
return err
}
if ! tracked {
c . config . Logger . Debug ( "messageID not tracked, nothing to do" , zap . Binary ( "messageID" , messageID ) )
}
c . config . Logger . Debug ( "messageID tracked" , zap . Binary ( "messageID" , messageID ) )
message , err := c . getMessage ( types . EncodeHex ( messageID ) )
if err != nil {
c . config . Logger . Error ( "could not retrieve message" , zap . Error ( err ) )
}
// This might happen if the user deleted their messages for example
if message == nil {
c . config . Logger . Warn ( "message not retrieved" )
return nil
}
c . config . Logger . Debug ( "message found" , zap . Binary ( "messageID" , messageID ) )
2024-07-26 21:34:57 +00:00
if message . ResponseTo != "" {
reply , err := c . getMessage ( message . ResponseTo )
if err != nil {
c . config . Logger . Error ( "could not retrieve message" , zap . Error ( err ) )
}
if reply != nil {
pkString := reply . From
c . config . Logger . Debug ( "handling mention" , zap . String ( "publickey" , pkString ) )
pubkeyBytes , err := types . DecodeHex ( pkString )
if err != nil {
return err
}
publicKey , err := crypto . UnmarshalPubkey ( pubkeyBytes )
if err != nil {
return err
}
// we use a synthetic installationID for mentions, as all devices need to be notified
shouldNotify , err := c . shouldNotifyOn ( publicKey , mentionInstallationID , messageID )
if err != nil {
return err
}
c . config . Logger . Debug ( "should no mention" , zap . Any ( "publickey" , shouldNotify ) )
// we send the notifications and return the info of the devices notified
infos , err := c . SendNotification ( publicKey , nil , messageID , message . LocalChatID , protobuf . PushNotification_MENTION )
if err != nil {
return err
}
// mark message as sent so we don't notify again
for _ , i := range infos {
c . config . Logger . Debug ( "marking as sent " , zap . Binary ( "mid" , messageID ) , zap . String ( "id" , i . InstallationID ) )
if err := c . notifiedOn ( publicKey , i . InstallationID , messageID , message . LocalChatID , protobuf . PushNotification_MESSAGE ) ; err != nil {
return err
}
}
}
}
2020-09-02 14:11:16 +00:00
for _ , pkString := range message . Mentions {
c . config . Logger . Debug ( "handling mention" , zap . String ( "publickey" , pkString ) )
pubkeyBytes , err := types . DecodeHex ( pkString )
if err != nil {
return err
}
publicKey , err := crypto . UnmarshalPubkey ( pubkeyBytes )
if err != nil {
return err
}
// we use a synthetic installationID for mentions, as all devices need to be notified
shouldNotify , err := c . shouldNotifyOn ( publicKey , mentionInstallationID , messageID )
if err != nil {
return err
}
c . config . Logger . Debug ( "should no mention" , zap . Any ( "publickey" , shouldNotify ) )
// we send the notifications and return the info of the devices notified
2021-03-31 16:23:45 +00:00
infos , err := c . SendNotification ( publicKey , nil , messageID , message . LocalChatID , protobuf . PushNotification_MENTION )
2020-09-02 14:11:16 +00:00
if err != nil {
return err
}
// mark message as sent so we don't notify again
for _ , i := range infos {
c . config . Logger . Debug ( "marking as sent " , zap . Binary ( "mid" , messageID ) , zap . String ( "id" , i . InstallationID ) )
if err := c . notifiedOn ( publicKey , i . InstallationID , messageID , message . LocalChatID , protobuf . PushNotification_MESSAGE ) ; err != nil {
return err
}
}
}
return nil
}
// handleDirectMessageSent handles one to ones and private group chat messages
// It will check if we need to notify on the message, and if so it will try to
// dispatch a push notification messages might be batched, if coming
// from datasync for example.
func ( c * Client ) handleDirectMessageSent ( sentMessage * common . SentMessage ) error {
c . config . Logger . Debug ( "handling direct messages" , zap . Any ( "messageIDs" , sentMessage . MessageIDs ) )
2020-07-22 07:41:40 +00:00
publicKey := sentMessage . PublicKey
// Collect the messageIDs we want to notify on
var trackedMessageIDs [ ] [ ] byte
for _ , messageID := range sentMessage . MessageIDs {
tracked , err := c . persistence . TrackedMessage ( messageID )
if err != nil {
return err
}
if tracked {
trackedMessageIDs = append ( trackedMessageIDs , messageID )
}
}
// Nothing to do
if len ( trackedMessageIDs ) == 0 {
c . config . Logger . Debug ( "nothing to do for" , zap . Any ( "messageIDs" , sentMessage . MessageIDs ) )
return nil
}
// sendToAllDevices indicates whether the message has been sent using public key encryption only
// i.e not through the double ratchet. In that case, any device will have received it.
sendToAllDevices := len ( sentMessage . Spec . Installations ) == 0
var installationIDs [ ] string
anyActionableMessage := sendToAllDevices
// Check if we should be notifiying those installations
for _ , messageID := range trackedMessageIDs {
for _ , installation := range sentMessage . Spec . Installations {
installationID := installation . ID
shouldNotify , err := c . shouldNotifyOn ( publicKey , installationID , messageID )
if err != nil {
return err
}
if shouldNotify {
anyActionableMessage = true
installationIDs = append ( installationIDs , installation . ID )
}
}
}
// Is there anything we should be notifying on?
if ! anyActionableMessage {
c . config . Logger . Debug ( "no actionable installation IDs" )
return nil
}
2021-10-29 14:29:28 +00:00
c . config . Logger . Debug ( "actionable messages" , zap . Any ( "messageIDs" , trackedMessageIDs ) , zap . Any ( "installation-ids" , installationIDs ) )
2020-07-22 07:41:40 +00:00
2020-09-02 14:11:16 +00:00
// Get message to check chatID. Again we use the first message for simplicity, but we should send one for each chatID. Messages though are very rarely batched.
message , err := c . getMessage ( types . EncodeHex ( trackedMessageIDs [ 0 ] ) )
if err != nil {
return err
}
// This is not the prettiest.
// because chatIDs are asymettric, we need to check if it's a one-to-one message or a group chat message.
// to do that we fingerprint the chatID.
// If it's a public key, we use our own public key as chatID, which correspond to the chatID used by the other peer
// otherwise we use the group chat ID
var chatID string
2020-09-07 08:25:57 +00:00
if len ( message . ChatId ) == oneToOneChatIDLength {
2020-09-02 14:11:16 +00:00
chatID = types . EncodeHex ( crypto . FromECDSAPub ( & c . config . Identity . PublicKey ) )
} else {
// this is a group chat
chatID = message . ChatId
}
2020-07-22 07:41:40 +00:00
// we send the notifications and return the info of the devices notified
2021-03-31 16:23:45 +00:00
infos , err := c . SendNotification ( publicKey , installationIDs , trackedMessageIDs [ 0 ] , chatID , protobuf . PushNotification_MESSAGE )
2020-07-22 07:41:40 +00:00
if err != nil {
return err
}
// mark message as sent so we don't notify again
for _ , i := range infos {
for _ , messageID := range trackedMessageIDs {
c . config . Logger . Debug ( "marking as sent " , zap . Binary ( "mid" , messageID ) , zap . String ( "id" , i . InstallationID ) )
2020-09-02 14:11:16 +00:00
if err := c . notifiedOn ( publicKey , i . InstallationID , messageID , chatID , protobuf . PushNotification_MESSAGE ) ; err != nil {
2020-07-22 07:41:40 +00:00
return err
}
}
}
return nil
}
// handleMessageScheduled keeps track of the message to make sure we notify on it
2022-07-15 13:19:37 +00:00
func ( c * Client ) handleMessageScheduled ( e * common . MessageEvent ) error {
message := e . RawMessage
2020-07-22 07:41:40 +00:00
if ! message . SendPushNotification {
return nil
}
2022-07-15 13:19:37 +00:00
// check if it's for one of our devices, do nothing in that case
if e . Recipient != nil && common . IsPubKeyEqual ( e . Recipient , & c . config . Identity . PublicKey ) {
return nil
}
2020-07-22 07:41:40 +00:00
messageID , err := types . DecodeHex ( message . ID )
if err != nil {
return err
}
return c . persistence . TrackPushNotification ( message . LocalChatID , messageID )
}
// shouldNotifyOn check whether we should notify a particular public-key/installation-id/message-id combination
func ( c * Client ) shouldNotifyOn ( publicKey * ecdsa . PublicKey , installationID string , messageID [ ] byte ) ( bool , error ) {
2022-07-15 13:19:37 +00:00
if publicKey != nil && common . IsPubKeyEqual ( publicKey , & c . config . Identity . PublicKey ) {
return false , nil
}
2020-07-22 07:41:40 +00:00
if len ( installationID ) == 0 {
return c . persistence . ShouldSendNotificationToAllInstallationIDs ( publicKey , messageID )
}
return c . persistence . ShouldSendNotificationFor ( publicKey , installationID , messageID )
}
2020-09-02 14:11:16 +00:00
// notifiedOn marks a combination of publickey/installationid/messageID/chatID/type as notified
func ( c * Client ) notifiedOn ( publicKey * ecdsa . PublicKey , installationID string , messageID [ ] byte , chatID string , notificationType protobuf . PushNotification_PushNotificationType ) error {
2020-07-22 07:41:40 +00:00
return c . persistence . UpsertSentNotification ( & SentNotification {
2020-09-02 14:11:16 +00:00
PublicKey : publicKey ,
LastTriedAt : time . Now ( ) . Unix ( ) ,
InstallationID : installationID ,
MessageID : messageID ,
ChatID : chatID ,
NotificationType : notificationType ,
2020-07-22 07:41:40 +00:00
} )
}
2020-09-02 14:11:16 +00:00
func ( c * Client ) chatIDsHashes ( chatIDs [ ] string ) [ ] [ ] byte {
2020-07-22 07:41:40 +00:00
var mutedChatListHashes [ ] [ ] byte
for _ , chatID := range chatIDs {
mutedChatListHashes = append ( mutedChatListHashes , common . Shake256 ( [ ] byte ( chatID ) ) )
}
return mutedChatListHashes
}
func ( c * Client ) encryptToken ( publicKey * ecdsa . PublicKey , token [ ] byte ) ( [ ] byte , error ) {
sharedKey , err := ecies . ImportECDSA ( c . config . Identity ) . GenerateShared (
ecies . ImportECDSAPublic ( publicKey ) ,
accessTokenKeyLength ,
accessTokenKeyLength ,
)
if err != nil {
return nil , err
}
encryptedToken , err := encryptAccessToken ( token , sharedKey , c . reader )
if err != nil {
return nil , err
}
return encryptedToken , nil
}
func ( c * Client ) decryptToken ( publicKey * ecdsa . PublicKey , token [ ] byte ) ( [ ] byte , error ) {
sharedKey , err := ecies . ImportECDSA ( c . config . Identity ) . GenerateShared (
ecies . ImportECDSAPublic ( publicKey ) ,
accessTokenKeyLength ,
accessTokenKeyLength ,
)
if err != nil {
return nil , err
}
decryptedToken , err := common . Decrypt ( token , sharedKey )
if err != nil {
return nil , err
}
return decryptedToken , nil
}
// allowedKeyList builds up a list of encrypted tokens, used for registering with the server
func ( c * Client ) allowedKeyList ( token [ ] byte , contactIDs [ ] * ecdsa . PublicKey ) ( [ ] [ ] byte , error ) {
// If we allow everyone, don't set the list
if ! c . config . AllowFromContactsOnly {
return nil , nil
}
var encryptedTokens [ ] [ ] byte
for _ , publicKey := range contactIDs {
encryptedToken , err := c . encryptToken ( publicKey , token )
if err != nil {
return nil , err
}
encryptedTokens = append ( encryptedTokens , encryptedToken )
}
return encryptedTokens , nil
}
// getToken checks if we need to refresh the token
// and return a new one in that case. A token is refreshed only if it's not set
// or if a contact has been removed
func ( c * Client ) getToken ( contactIDs [ ] * ecdsa . PublicKey ) string {
2020-07-30 07:19:34 +00:00
if c . lastPushNotificationRegistration == nil || len ( c . lastPushNotificationRegistration . AccessToken ) == 0 || c . shouldRefreshToken ( c . lastContactIDs , contactIDs , c . lastPushNotificationRegistration . AllowFromContactsOnly , c . config . AllowFromContactsOnly ) {
2020-07-22 07:41:40 +00:00
c . config . Logger . Info ( "refreshing access token" )
return uuid . New ( ) . String ( )
}
return c . lastPushNotificationRegistration . AccessToken
}
2020-07-15 07:23:31 +00:00
func ( c * Client ) getVersion ( ) uint64 {
if c . lastPushNotificationRegistration == nil {
return 1
}
return c . lastPushNotificationRegistration . Version + 1
}
2020-09-02 14:11:16 +00:00
func ( c * Client ) buildPushNotificationRegistrationMessage ( options * RegistrationOptions ) ( * protobuf . PushNotificationRegistration , error ) {
token := c . getToken ( options . ContactIDs )
allowedKeyList , err := c . allowedKeyList ( [ ] byte ( token ) , options . ContactIDs )
2020-06-30 07:50:59 +00:00
if err != nil {
return nil , err
}
2020-09-02 14:11:16 +00:00
return & protobuf . PushNotificationRegistration {
AccessToken : token ,
TokenType : c . tokenType ,
ApnTopic : c . apnTopic ,
Version : c . getVersion ( ) ,
InstallationId : c . config . InstallationID ,
DeviceToken : c . deviceToken ,
AllowFromContactsOnly : c . config . AllowFromContactsOnly ,
Enabled : c . config . RemoteNotificationsEnabled ,
2023-06-22 05:06:32 +00:00
BlockedChatList : c . chatIDsHashes ( options . BlockedChatIDs ) ,
2020-09-02 14:11:16 +00:00
BlockMentions : c . config . BlockMentions ,
AllowedMentionsChatList : c . chatIDsHashes ( options . PublicChatIDs ) ,
AllowedKeyList : allowedKeyList ,
2023-06-22 05:06:32 +00:00
MutedChatList : c . chatIDsHashes ( options . MutedChatIDs ) ,
2020-09-02 14:11:16 +00:00
} , nil
2020-06-30 07:50:59 +00:00
}
2020-07-16 07:45:42 +00:00
func ( c * Client ) buildPushNotificationUnregisterMessage ( ) * protobuf . PushNotificationRegistration {
options := & protobuf . PushNotificationRegistration {
Version : c . getVersion ( ) ,
InstallationId : c . config . InstallationID ,
Unregister : true ,
}
return options
}
2020-07-30 07:19:34 +00:00
// shouldRefreshToken tells us whether we should create a new token,
// that's only necessary when a contact is removed
// or allowFromContactsOnly is enabled.
// In both cases we want to invalidate any existing token
func ( c * Client ) shouldRefreshToken ( oldContactIDs , newContactIDs [ ] * ecdsa . PublicKey , oldAllowFromContactsOnly , newAllowFromContactsOnly bool ) bool {
// Check if allowFromContactsOnly has just been enabled
if ! oldAllowFromContactsOnly && newAllowFromContactsOnly {
return true
}
2020-07-15 07:23:31 +00:00
newContactIDsMap := make ( map [ string ] bool )
for _ , pk := range newContactIDs {
newContactIDsMap [ types . EncodeHex ( crypto . FromECDSAPub ( pk ) ) ] = true
}
for _ , pk := range oldContactIDs {
if ok := newContactIDsMap [ types . EncodeHex ( crypto . FromECDSAPub ( pk ) ) ] ; ! ok {
return true
}
}
return false
}
2020-07-15 10:29:16 +00:00
func nextServerRetry ( server * PushNotificationServer ) int64 {
return server . LastRetriedAt + RegistrationBackoffTime * server . RetryCount * int64 ( math . Exp2 ( float64 ( server . RetryCount ) ) )
}
2020-07-20 12:46:15 +00:00
func nextPushNotificationRetry ( pn * SentNotification ) int64 {
return pn . LastTriedAt + pushNotificationBackoffTime * pn . RetryCount * int64 ( math . Exp2 ( float64 ( pn . RetryCount ) ) )
}
2020-07-15 10:29:16 +00:00
// We calculate if it's too early to retry, by exponentially backing off
func shouldRetryRegisteringWithServer ( server * PushNotificationServer ) bool {
2020-07-20 13:58:54 +00:00
return time . Now ( ) . Unix ( ) >= nextServerRetry ( server )
2020-07-15 10:29:16 +00:00
}
2020-07-20 12:46:15 +00:00
// We calculate if it's too early to retry, by exponentially backing off
func shouldRetryPushNotification ( pn * SentNotification ) bool {
if pn . RetryCount > maxPushNotificationRetries {
return false
}
2020-07-20 14:52:55 +00:00
return time . Now ( ) . Unix ( ) >= nextPushNotificationRetry ( pn )
2020-07-20 12:46:15 +00:00
}
2020-07-16 07:45:42 +00:00
func ( c * Client ) resetServers ( ) error {
servers , err := c . persistence . GetServers ( )
if err != nil {
return err
}
for _ , server := range servers {
// Reset server registration data
server . Registered = false
server . RegisteredAt = 0
2020-07-20 13:58:54 +00:00
server . RetryCount = 0
2020-07-16 07:45:42 +00:00
server . LastRetriedAt = time . Now ( ) . Unix ( )
server . AccessToken = ""
if err := c . persistence . UpsertServer ( server ) ; err != nil {
return err
}
}
return nil
}
2020-07-22 07:41:40 +00:00
// registerWithServer will register with a push notification server. This will use
// the user identity key for dispatching, as the content is in any case signed, so identity needs to be revealed.
2020-07-15 07:23:31 +00:00
func ( c * Client ) registerWithServer ( registration * protobuf . PushNotificationRegistration , server * PushNotificationServer ) error {
2020-07-22 07:41:40 +00:00
// reset server registration data
2020-07-15 07:23:31 +00:00
server . Registered = false
server . RegisteredAt = 0
2020-07-22 07:41:40 +00:00
server . RetryCount ++
2020-07-15 10:29:16 +00:00
server . LastRetriedAt = time . Now ( ) . Unix ( )
2020-07-15 07:23:31 +00:00
server . AccessToken = registration . AccessToken
2020-07-22 07:41:40 +00:00
// save
2020-07-15 07:23:31 +00:00
if err := c . persistence . UpsertServer ( server ) ; err != nil {
return err
}
2020-07-22 07:41:40 +00:00
// build grant for this specific server
2020-07-15 07:23:31 +00:00
grant , err := c . buildGrantSignature ( server . PublicKey , registration . AccessToken )
if err != nil {
c . config . Logger . Error ( "failed to build grant" , zap . Error ( err ) )
return err
}
registration . Grant = grant
2020-07-22 07:41:40 +00:00
// marshal message
2020-07-15 07:23:31 +00:00
marshaledRegistration , err := proto . Marshal ( registration )
if err != nil {
return err
}
2020-07-22 07:41:40 +00:00
// encrypt and dispatch message
2020-07-15 07:23:31 +00:00
encryptedRegistration , err := c . encryptRegistration ( server . PublicKey , marshaledRegistration )
if err != nil {
return err
}
2020-07-28 13:22:22 +00:00
rawMessage := common . RawMessage {
2020-07-15 07:23:31 +00:00
Payload : encryptedRegistration ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION ,
2021-01-18 09:12:03 +00:00
// We send on personal topic to avoid a lot of traffic on the partitioned topic
SendOnPersonalTopic : true ,
2023-11-08 18:05:33 +00:00
SkipEncryptionLayer : true ,
2020-07-15 07:23:31 +00:00
}
2021-06-23 14:13:48 +00:00
_ , err = c . messageSender . SendPrivate ( context . Background ( ) , server . PublicKey , & rawMessage )
2020-07-15 07:23:31 +00:00
if err != nil {
return err
}
2021-09-28 10:25:13 +00:00
c . pendingRegistrations [ hex . EncodeToString ( common . Shake256 ( encryptedRegistration ) ) ] = true
2020-07-15 07:23:31 +00:00
return nil
2020-07-15 10:29:16 +00:00
}
2021-03-31 16:23:45 +00:00
// SendNotification sends an actual notification to the push notification server.
2020-07-22 07:41:40 +00:00
// the notification is sent using an ephemeral key to shield the real identity of the sender
2021-03-31 16:23:45 +00:00
func ( c * Client ) SendNotification ( publicKey * ecdsa . PublicKey , installationIDs [ ] string , messageID [ ] byte , chatID string , notificationType protobuf . PushNotification_PushNotificationType ) ( [ ] * PushNotificationInfo , error ) {
2020-09-02 14:11:16 +00:00
2022-07-15 13:19:37 +00:00
if common . IsPubKeyEqual ( publicKey , & c . config . Identity . PublicKey ) {
return nil , nil
}
2020-07-22 07:41:40 +00:00
// get latest push notification infos
2020-07-20 12:46:15 +00:00
err := c . queryNotificationInfo ( publicKey , false )
if err != nil {
return nil , err
}
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "queried info" )
// retrieve info from the database
2020-07-20 12:46:15 +00:00
info , err := c . GetPushNotificationInfo ( publicKey , installationIDs )
if err != nil {
c . config . Logger . Error ( "could not get pn info" , zap . Error ( err ) )
return nil , err
}
2020-07-22 07:41:40 +00:00
// naively dispatch to the first server for now
// push notifications are only retried for now if a WRONG_TOKEN response is returned.
// we should also retry if no response at all is received after a timeout.
// also we send a single notification for multiple message ids, need to check with UI what's the desired behavior
2020-07-20 12:46:15 +00:00
2020-08-25 09:46:01 +00:00
// shuffle so we don't hit the same servers all the times
// NOTE: here's is a tradeoff, ideally we want to randomly pick a server,
// but hit the same servers for batched notifications, for now naively
// hit a random server
2020-08-27 13:30:48 +00:00
mrand . Seed ( time . Now ( ) . Unix ( ) )
2020-08-25 09:46:01 +00:00
mrand . Shuffle ( len ( info ) , func ( i , j int ) {
info [ i ] , info [ j ] = info [ j ] , info [ i ]
2020-07-20 12:46:15 +00:00
} )
installationIDsMap := make ( map [ string ] bool )
2020-07-22 07:41:40 +00:00
// one info per installation id, grouped by server
2020-07-20 12:46:15 +00:00
actionableInfos := make ( map [ string ] [ ] * PushNotificationInfo )
2021-03-31 16:23:45 +00:00
2020-07-20 12:46:15 +00:00
for _ , i := range info {
if ! installationIDsMap [ i . InstallationID ] {
serverKey := hex . EncodeToString ( crypto . CompressPubkey ( i . ServerPublicKey ) )
actionableInfos [ serverKey ] = append ( actionableInfos [ serverKey ] , i )
installationIDsMap [ i . InstallationID ] = true
}
}
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "actionable info" , zap . Int ( "count" , len ( actionableInfos ) ) )
2024-06-17 11:45:53 +00:00
ephemeralKey , err := c . messageSender . GetEphemeralKey ( )
2020-07-21 15:41:10 +00:00
if err != nil {
return nil , err
}
2020-07-20 12:46:15 +00:00
var actionedInfo [ ] * PushNotificationInfo
for _ , infos := range actionableInfos {
var pushNotifications [ ] * protobuf . PushNotification
for _ , i := range infos {
pushNotifications = append ( pushNotifications , & protobuf . PushNotification {
2020-09-02 14:11:16 +00:00
Type : notificationType ,
2020-08-25 09:46:01 +00:00
// For now we set the ChatID to our own identity key, this will work fine for blocked users
// and muted 1-to-1 chats, but not for group chats.
2020-09-02 14:11:16 +00:00
ChatId : common . Shake256 ( [ ] byte ( chatID ) ) ,
2020-09-07 08:25:57 +00:00
Author : common . Shake256 ( [ ] byte ( types . EncodeHex ( crypto . FromECDSAPub ( & c . config . Identity . PublicKey ) ) ) ) ,
2020-07-20 12:46:15 +00:00
AccessToken : i . AccessToken ,
PublicKey : common . HashPublicKey ( publicKey ) ,
InstallationId : i . InstallationID ,
} )
}
request := & protobuf . PushNotificationRequest {
MessageId : messageID ,
Requests : pushNotifications ,
}
serverPublicKey := infos [ 0 ] . ServerPublicKey
payload , err := proto . Marshal ( request )
if err != nil {
return nil , err
}
2020-07-28 13:22:22 +00:00
rawMessage := common . RawMessage {
2020-07-22 07:41:40 +00:00
Payload : payload ,
Sender : ephemeralKey ,
// we skip encryption as we don't want to save any key material
// for an ephemeral key, no need to use pfs as these are throw away keys
2023-11-08 18:05:33 +00:00
SkipEncryptionLayer : true ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_REQUEST ,
2020-07-20 12:46:15 +00:00
}
2021-06-23 14:13:48 +00:00
_ , err = c . messageSender . SendPrivate ( context . Background ( ) , serverPublicKey , & rawMessage )
2020-07-20 12:46:15 +00:00
if err != nil {
return nil , err
}
actionedInfo = append ( actionedInfo , infos ... )
}
return actionedInfo , nil
}
func ( c * Client ) resendNotification ( pn * SentNotification ) error {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "resending notification" )
pn . RetryCount ++
2020-07-20 12:46:15 +00:00
pn . LastTriedAt = time . Now ( ) . Unix ( )
err := c . persistence . UpsertSentNotification ( pn )
if err != nil {
2020-07-20 14:52:55 +00:00
c . config . Logger . Error ( "failed to upsert notification" , zap . Error ( err ) )
2020-07-20 12:46:15 +00:00
return err
}
2020-07-22 07:41:40 +00:00
// re-fetch push notification info
2020-07-20 12:46:15 +00:00
err = c . queryNotificationInfo ( pn . PublicKey , true )
if err != nil {
2020-07-20 14:52:55 +00:00
c . config . Logger . Error ( "failed to query notification info" , zap . Error ( err ) )
2020-07-20 12:46:15 +00:00
return err
}
if err != nil {
c . config . Logger . Error ( "could not get pn info" , zap . Error ( err ) )
return err
}
2021-03-31 16:23:45 +00:00
_ , err = c . SendNotification ( pn . PublicKey , [ ] string { pn . InstallationID } , pn . MessageID , pn . ChatID , pn . NotificationType )
2020-07-20 12:46:15 +00:00
return err
}
2020-07-22 07:41:40 +00:00
// resendingLoop is a loop that is running when push notifications need to be resent, it only runs when needed, it will quit if no work is necessary.
2020-07-20 12:46:15 +00:00
func ( c * Client ) resendingLoop ( ) error {
for {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "running resending loop" )
2020-07-20 12:46:15 +00:00
var lowestNextRetry int64
2020-07-22 07:41:40 +00:00
// fetch retriable notifications
2020-07-20 12:46:15 +00:00
retriableNotifications , err := c . persistence . GetRetriablePushNotifications ( )
if err != nil {
c . config . Logger . Error ( "failed retrieving notifications, quitting resending loop" , zap . Error ( err ) )
return err
}
if len ( retriableNotifications ) == 0 {
c . config . Logger . Debug ( "no retriable notifications, quitting" )
return nil
}
2021-01-08 19:55:55 +00:00
c . config . Logger . Debug ( "have some retriable notifications" , zap . Int ( "retryable-notifications" , len ( retriableNotifications ) ) )
2020-07-20 12:46:15 +00:00
for _ , pn := range retriableNotifications {
2020-07-22 07:41:40 +00:00
// check if we should retry the notification
2020-07-20 12:46:15 +00:00
if shouldRetryPushNotification ( pn ) {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "retrying pn" )
2020-07-20 12:46:15 +00:00
err := c . resendNotification ( pn )
if err != nil {
return err
}
}
2020-07-22 07:41:40 +00:00
// set the lowest next retry if necessary
2020-07-20 13:58:54 +00:00
nextRetry := nextPushNotificationRetry ( pn )
2020-07-20 12:46:15 +00:00
if lowestNextRetry == 0 || nextRetry < lowestNextRetry {
lowestNextRetry = nextRetry
}
}
nextRetry := lowestNextRetry - time . Now ( ) . Unix ( )
2021-01-08 19:55:55 +00:00
// Give some room, sleep at least a second
if nextRetry < 1 {
nextRetry = 1
}
2020-07-22 07:41:40 +00:00
// how long should we sleep for?
2020-07-20 12:46:15 +00:00
waitFor := time . Duration ( nextRetry )
2021-01-08 19:55:55 +00:00
2020-07-20 12:46:15 +00:00
select {
case <- time . After ( waitFor * time . Second ) :
case <- c . resendingLoopQuitChan :
return nil
}
}
}
2020-07-22 07:41:40 +00:00
// registrationLoop is a loop that is running when we need to register with a push notification server, it only runs when needed, it will quit if no work is necessary.
2020-07-15 10:29:16 +00:00
func ( c * Client ) registrationLoop ( ) error {
2020-09-02 14:11:16 +00:00
if c . lastPushNotificationRegistration == nil {
return nil
}
2020-07-15 10:29:16 +00:00
for {
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "running registration loop" )
2020-07-15 10:29:16 +00:00
servers , err := c . persistence . GetServers ( )
if err != nil {
c . config . Logger . Error ( "failed retrieving servers, quitting registration loop" , zap . Error ( err ) )
return err
}
if len ( servers ) == 0 {
c . config . Logger . Debug ( "nothing to do, quitting registration loop" )
return nil
}
2020-07-22 07:41:40 +00:00
var nonRegisteredServers [ ] * PushNotificationServer
for _ , server := range servers {
if ! server . Registered && server . RetryCount < maxRegistrationRetries {
nonRegisteredServers = append ( nonRegisteredServers , server )
}
}
2020-07-07 13:55:24 +00:00
2020-07-22 07:41:40 +00:00
if len ( nonRegisteredServers ) == 0 {
c . config . Logger . Debug ( "registered with all servers, quitting registration loop" )
return nil
2020-07-16 08:36:17 +00:00
}
2020-07-07 13:55:24 +00:00
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "Trying to register with" , zap . Int ( "servers" , len ( nonRegisteredServers ) ) )
2020-07-16 08:36:17 +00:00
2020-07-22 07:41:40 +00:00
var lowestNextRetry int64
2020-07-20 14:14:42 +00:00
2020-07-22 07:41:40 +00:00
for _ , server := range nonRegisteredServers {
if shouldRetryRegisteringWithServer ( server ) {
c . config . Logger . Debug ( "registering with server" , zap . Any ( "server" , server ) )
err := c . registerWithServer ( c . lastPushNotificationRegistration , server )
if err != nil {
return err
}
}
nextRetry := nextServerRetry ( server )
if lowestNextRetry == 0 || nextRetry < lowestNextRetry {
lowestNextRetry = nextRetry
}
}
2020-07-16 08:36:17 +00:00
2020-07-22 07:41:40 +00:00
nextRetry := lowestNextRetry - time . Now ( ) . Unix ( )
waitFor := time . Duration ( nextRetry )
c . config . Logger . Debug ( "Waiting for" , zap . Any ( "wait for" , waitFor ) )
select {
2020-07-20 13:58:54 +00:00
2020-07-22 07:41:40 +00:00
case <- time . After ( waitFor * time . Second ) :
case <- c . registrationLoopQuitChan :
return nil
}
}
2020-07-20 13:58:54 +00:00
}
2020-07-22 07:41:40 +00:00
func ( c * Client ) saveLastPushNotificationRegistration ( registration * protobuf . PushNotificationRegistration , contactIDs [ ] * ecdsa . PublicKey ) error {
2020-07-16 08:36:17 +00:00
// stop registration loop
c . stopRegistrationLoop ( )
2020-07-22 07:41:40 +00:00
err := c . persistence . SaveLastPushNotificationRegistration ( registration , contactIDs )
2020-07-16 07:45:42 +00:00
if err != nil {
2020-07-16 08:36:17 +00:00
return err
2020-07-07 13:55:24 +00:00
}
2020-07-22 07:41:40 +00:00
c . lastPushNotificationRegistration = registration
c . lastContactIDs = contactIDs
2020-07-10 07:45:40 +00:00
2020-06-30 07:50:59 +00:00
return nil
}
2020-07-13 08:53:13 +00:00
// buildGrantSignatureMaterial builds a grant for a specific server.
// We use 3 components:
// 1) The client public key. Not sure this applies to our signature scheme, but best to be conservative. https://crypto.stackexchange.com/questions/15538/given-a-message-and-signature-find-a-public-key-that-makes-the-signature-valid
// 2) The server public key
// 3) The access token
// By verifying this signature, a client can trust the server was instructed to store this access token.
func ( c * Client ) buildGrantSignatureMaterial ( clientPublicKey * ecdsa . PublicKey , serverPublicKey * ecdsa . PublicKey , accessToken string ) [ ] byte {
var signatureMaterial [ ] byte
signatureMaterial = append ( signatureMaterial , crypto . CompressPubkey ( clientPublicKey ) ... )
signatureMaterial = append ( signatureMaterial , crypto . CompressPubkey ( serverPublicKey ) ... )
signatureMaterial = append ( signatureMaterial , [ ] byte ( accessToken ) ... )
return crypto . Keccak256 ( signatureMaterial )
}
func ( c * Client ) buildGrantSignature ( serverPublicKey * ecdsa . PublicKey , accessToken string ) ( [ ] byte , error ) {
signatureMaterial := c . buildGrantSignatureMaterial ( & c . config . Identity . PublicKey , serverPublicKey , accessToken )
return crypto . Sign ( signatureMaterial , c . config . Identity )
}
func ( c * Client ) handleGrant ( clientPublicKey * ecdsa . PublicKey , serverPublicKey * ecdsa . PublicKey , grant [ ] byte , accessToken string ) error {
signatureMaterial := c . buildGrantSignatureMaterial ( clientPublicKey , serverPublicKey , accessToken )
extractedPublicKey , err := crypto . SigToPub ( signatureMaterial , grant )
if err != nil {
return err
}
if ! common . IsPubKeyEqual ( clientPublicKey , extractedPublicKey ) {
return errors . New ( "invalid grant" )
}
return nil
}
2020-07-22 07:41:40 +00:00
// handleAllowedKeyList will try to decrypt a token from the list, to see if we are allowed to send push notification to a given user
func ( c * Client ) handleAllowedKeyList ( publicKey * ecdsa . PublicKey , allowedKeyList [ ] [ ] byte ) string {
c . config . Logger . Debug ( "handling allowed key list" )
for _ , encryptedToken := range allowedKeyList {
2020-07-17 12:29:51 +00:00
token , err := c . decryptToken ( publicKey , encryptedToken )
if err != nil {
c . config . Logger . Warn ( "could not decrypt token" , zap . Error ( err ) )
continue
}
2020-07-22 07:41:40 +00:00
c . config . Logger . Debug ( "decrypted token" )
2020-07-17 12:29:51 +00:00
return string ( token )
}
return ""
}
2020-07-31 13:46:27 +00:00
func ( c * Client ) MyPushNotificationQueryInfo ( ) ( [ ] * protobuf . PushNotificationQueryInfo , error ) {
// Nothing to do
if c . lastPushNotificationRegistration == nil || c . lastPushNotificationRegistration . Unregister {
return nil , nil
}
var response [ ] * protobuf . PushNotificationQueryInfo
servers , err := c . persistence . GetServers ( )
if err != nil {
return nil , err
}
for _ , server := range servers {
// ignore non-registered servers
if ! server . Registered {
continue
}
// build grant for this specific server
grant , err := c . buildGrantSignature ( server . PublicKey , c . lastPushNotificationRegistration . AccessToken )
if err != nil {
c . config . Logger . Error ( "failed to build grant" , zap . Error ( err ) )
return nil , err
}
queryInfo := & protobuf . PushNotificationQueryInfo {
InstallationId : c . config . InstallationID ,
// is this the right key?
PublicKey : common . HashPublicKey ( & c . config . Identity . PublicKey ) ,
Version : c . lastPushNotificationRegistration . Version ,
Grant : grant ,
ServerPublicKey : crypto . CompressPubkey ( server . PublicKey ) ,
}
if c . lastPushNotificationRegistration . AllowFromContactsOnly {
queryInfo . AllowedKeyList = c . lastPushNotificationRegistration . AllowedKeyList
} else {
queryInfo . AccessToken = c . lastPushNotificationRegistration . AccessToken
}
response = append ( response , queryInfo )
}
return response , nil
}
2020-07-22 07:41:40 +00:00
// queryPushNotificationInfo sends a message to any server who has the given user registered.
// it uses an ephemeral key so the identity of the client querying is not disclosed
func ( c * Client ) queryPushNotificationInfo ( publicKey * ecdsa . PublicKey ) error {
2020-07-09 16:52:26 +00:00
hashedPublicKey := common . HashPublicKey ( publicKey )
query := & protobuf . PushNotificationQuery {
PublicKeys : [ ] [ ] byte { hashedPublicKey } ,
}
encodedMessage , err := proto . Marshal ( query )
if err != nil {
2020-07-10 13:26:06 +00:00
return err
2020-07-09 16:52:26 +00:00
}
2024-06-17 11:45:53 +00:00
ephemeralKey , err := c . messageSender . GetEphemeralKey ( )
2020-07-21 15:41:10 +00:00
if err != nil {
return err
}
2020-07-28 13:22:22 +00:00
rawMessage := common . RawMessage {
2020-07-22 07:41:40 +00:00
Payload : encodedMessage ,
Sender : ephemeralKey ,
// we don't want to wrap in an encryption layer message
2023-11-08 18:05:33 +00:00
SkipEncryptionLayer : true ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY ,
2024-08-01 18:36:25 +00:00
Priority : & common . LowPriority ,
2020-07-21 15:41:10 +00:00
}
2020-07-22 07:41:40 +00:00
// this is the topic of message
2020-07-09 16:52:26 +00:00
encodedPublicKey := hex . EncodeToString ( hashedPublicKey )
2021-06-23 14:13:48 +00:00
messageID , err := c . messageSender . SendPublic ( context . Background ( ) , encodedPublicKey , rawMessage )
2020-07-09 16:52:26 +00:00
2020-07-10 13:26:06 +00:00
if err != nil {
return err
}
2020-07-09 16:52:26 +00:00
2020-07-10 13:26:06 +00:00
return c . persistence . SavePushNotificationQuery ( publicKey , messageID )
}