2020-06-30 08:30:58 +00:00
package push_notification_client
2020-06-30 07:50:59 +00:00
import (
2020-07-07 13:55:24 +00:00
"context"
2020-06-30 07:50:59 +00:00
"crypto/aes"
"crypto/cipher"
2020-07-09 16:52:26 +00:00
"bytes"
2020-06-30 07:50:59 +00:00
"crypto/ecdsa"
"crypto/rand"
2020-07-09 16:52:26 +00:00
"encoding/hex"
2020-07-06 08:54:22 +00:00
"errors"
2020-06-30 07:50:59 +00:00
"io"
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"
"github.com/status-im/status-go/eth-node/crypto/ecies"
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-07 13:55:24 +00:00
"go.uber.org/zap"
2020-06-30 07:50:59 +00:00
)
2020-07-07 13:55:24 +00:00
const encryptedPayloadKeyLength = 16
2020-06-30 07:50:59 +00:00
const accessTokenKeyLength = 16
type PushNotificationServer struct {
2020-07-07 09:00:04 +00:00
publicKey * ecdsa . PublicKey
registered bool
registeredAt int64
}
type PushNotificationInfo struct {
AccessToken string
InstallationID string
PublicKey * ecdsa . PublicKey
2020-06-30 07:50:59 +00:00
}
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
// AllowOnlyFromContacts indicates whether we should be receiving push notifications
// only from contacts
AllowOnlyFromContacts bool
// PushNotificationServers is an array of push notification servers we want to register with
PushNotificationServers [ ] * PushNotificationServer
// InstallationID is the installation-id for this device
InstallationID string
2020-07-07 13:55:24 +00:00
Logger * zap . Logger
// TokenType is the type of token
TokenType protobuf . PushNotificationRegistration_TokenType
2020-06-30 07:50:59 +00:00
}
2020-06-30 08:30:58 +00:00
type Client struct {
persistence * Persistence
2020-07-06 08:54:22 +00:00
quit chan struct { }
2020-06-30 08:30:58 +00:00
config * Config
2020-06-30 07:50:59 +00:00
2020-07-01 08:37:54 +00:00
// lastPushNotificationVersion is the latest known push notification version
lastPushNotificationVersion uint64
2020-06-30 07:50:59 +00:00
// AccessToken is the access token that is currently being used
AccessToken string
// DeviceToken is the device token for this device
DeviceToken string
// randomReader only used for testing so we have deterministic encryption
reader io . Reader
2020-07-06 08:54:22 +00:00
//messageProcessor is a message processor used to send and being notified of messages
messageProcessor * common . MessageProcessor
2020-07-09 16:52:26 +00:00
//pushNotificationRegistrationResponses is a channel that listens to pushNotificationResponse
pushNotificationRegistrationResponses chan * protobuf . PushNotificationRegistrationResponse
2020-07-07 13:55:24 +00:00
//pushNotificationQueryResponses is a channel that listens to pushNotificationResponse
pushNotificationQueryResponses chan * protobuf . PushNotificationQueryResponse
2020-07-06 08:54:22 +00:00
}
2020-07-07 13:55:24 +00:00
func New ( persistence * Persistence , config * Config , processor * common . MessageProcessor ) * Client {
2020-07-06 08:54:22 +00:00
return & Client {
2020-07-09 16:52:26 +00:00
quit : make ( chan struct { } ) ,
config : config ,
pushNotificationRegistrationResponses : make ( chan * protobuf . PushNotificationRegistrationResponse ) ,
pushNotificationQueryResponses : make ( chan * protobuf . PushNotificationQueryResponse ) ,
messageProcessor : processor ,
persistence : persistence ,
reader : rand . Reader }
2020-07-06 08:54:22 +00:00
}
func ( c * Client ) Start ( ) error {
if c . messageProcessor == nil {
return errors . New ( "can't start, missing message processor" )
}
go func ( ) {
subscription := c . messageProcessor . Subscribe ( )
for {
select {
case m := <- subscription :
if err := c . HandleMessageSent ( m ) ; err != nil {
// TODO: log
}
case <- c . quit :
return
}
}
} ( )
return nil
2020-06-30 07:50:59 +00:00
}
2020-07-06 08:54:22 +00:00
func ( c * Client ) Stop ( ) error {
close ( c . quit )
return nil
2020-06-30 07:50:59 +00:00
}
// Sends an actual push notification, where do we get the chatID?
func sendPushNotificationTo ( publicKey * ecdsa . PublicKey , chatID string ) error {
return nil
}
// This should schedule:
// 1) Check we have reasonably fresh push notifications info
// 2) Otherwise it should fetch them
// 3) Send a push notification to the devices in question
2020-07-06 08:54:22 +00:00
func ( p * Client ) HandleMessageSent ( sentMessage * common . SentMessage ) error {
2020-06-30 07:50:59 +00:00
return nil
}
2020-06-30 08:30:58 +00:00
func ( p * Client ) NotifyOnMessageID ( messageID [ ] byte ) error {
2020-06-30 07:50:59 +00:00
return nil
}
2020-07-07 09:00:04 +00:00
func ( p * Client ) mutedChatIDsHashes ( chatIDs [ ] string ) [ ] [ ] byte {
2020-06-30 07:50:59 +00:00
var mutedChatListHashes [ ] [ ] byte
2020-07-07 09:00:04 +00:00
for _ , chatID := range chatIDs {
2020-07-07 13:55:24 +00:00
mutedChatListHashes = append ( mutedChatListHashes , common . Shake256 ( [ ] byte ( chatID ) ) )
2020-06-30 07:50:59 +00:00
}
return mutedChatListHashes
}
2020-07-01 08:37:54 +00:00
func ( p * Client ) encryptToken ( publicKey * ecdsa . PublicKey , token [ ] byte ) ( [ ] byte , error ) {
2020-06-30 07:50:59 +00:00
sharedKey , err := ecies . ImportECDSA ( p . config . Identity ) . GenerateShared (
ecies . ImportECDSAPublic ( publicKey ) ,
accessTokenKeyLength ,
accessTokenKeyLength ,
)
if err != nil {
return nil , err
}
encryptedToken , err := encryptAccessToken ( token , sharedKey , p . reader )
if err != nil {
return nil , err
}
2020-07-01 08:37:54 +00:00
return encryptedToken , nil
2020-06-30 07:50:59 +00:00
}
2020-07-07 09:00:04 +00:00
func ( p * Client ) allowedUserList ( token [ ] byte , contactIDs [ ] * ecdsa . PublicKey ) ( [ ] [ ] byte , error ) {
2020-07-01 08:37:54 +00:00
var encryptedTokens [ ] [ ] byte
2020-07-07 09:00:04 +00:00
for _ , publicKey := range contactIDs {
2020-07-01 08:37:54 +00:00
encryptedToken , err := p . encryptToken ( publicKey , token )
2020-06-30 07:50:59 +00:00
if err != nil {
return nil , err
}
2020-07-01 08:37:54 +00:00
encryptedTokens = append ( encryptedTokens , encryptedToken )
2020-06-30 07:50:59 +00:00
}
2020-07-01 08:37:54 +00:00
return encryptedTokens , nil
2020-06-30 07:50:59 +00:00
}
2020-07-07 09:00:04 +00:00
func ( p * Client ) buildPushNotificationRegistrationMessage ( contactIDs [ ] * ecdsa . PublicKey , mutedChatIDs [ ] string ) ( * protobuf . PushNotificationRegistration , error ) {
2020-07-01 08:37:54 +00:00
token := uuid . New ( ) . String ( )
2020-07-07 09:00:04 +00:00
allowedUserList , err := p . allowedUserList ( [ ] byte ( token ) , contactIDs )
2020-06-30 07:50:59 +00:00
if err != nil {
return nil , err
}
2020-07-02 08:08:19 +00:00
options := & protobuf . PushNotificationRegistration {
2020-07-01 08:37:54 +00:00
AccessToken : token ,
2020-07-07 13:55:24 +00:00
TokenType : p . config . TokenType ,
2020-07-01 08:37:54 +00:00
Version : p . lastPushNotificationVersion + 1 ,
2020-06-30 07:50:59 +00:00
InstallationId : p . config . InstallationID ,
Token : p . DeviceToken ,
Enabled : p . config . RemoteNotificationsEnabled ,
2020-07-07 09:00:04 +00:00
BlockedChatList : p . mutedChatIDsHashes ( mutedChatIDs ) ,
2020-06-30 07:50:59 +00:00
AllowedUserList : allowedUserList ,
}
return options , nil
}
2020-07-09 16:52:26 +00:00
func ( c * Client ) Register ( deviceToken string , contactIDs [ ] * ecdsa . PublicKey , mutedChatIDs [ ] string ) ( [ ] string , error ) {
2020-07-07 13:55:24 +00:00
c . DeviceToken = deviceToken
servers , err := c . persistence . GetServers ( )
2020-07-07 09:00:04 +00:00
if err != nil {
2020-07-09 16:52:26 +00:00
return nil , err
2020-07-07 09:00:04 +00:00
}
2020-07-07 13:55:24 +00:00
2020-07-07 09:00:04 +00:00
if len ( servers ) == 0 {
2020-07-09 16:52:26 +00:00
return nil , errors . New ( "no servers to register with" )
2020-07-07 09:00:04 +00:00
}
2020-07-07 13:55:24 +00:00
registration , err := c . buildPushNotificationRegistrationMessage ( contactIDs , mutedChatIDs )
if err != nil {
2020-07-09 16:52:26 +00:00
return nil , err
2020-07-07 13:55:24 +00:00
}
marshaledRegistration , err := proto . Marshal ( registration )
if err != nil {
2020-07-09 16:52:26 +00:00
return nil , err
2020-07-07 13:55:24 +00:00
}
for _ , server := range servers {
encryptedRegistration , err := c . encryptRegistration ( server . publicKey , marshaledRegistration )
if err != nil {
2020-07-09 16:52:26 +00:00
return nil , err
2020-07-07 13:55:24 +00:00
}
rawMessage := & common . RawMessage {
Payload : encryptedRegistration ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION ,
}
_ , err = c . messageProcessor . SendPrivate ( context . Background ( ) , server . publicKey , rawMessage )
// Send message and wait for reply
}
2020-07-09 16:52:26 +00:00
// TODO: this needs to wait for all the registrations, probably best to poll the database
2020-07-07 13:55:24 +00:00
for {
select {
case <- c . quit :
2020-07-09 16:52:26 +00:00
return nil , nil
2020-07-07 13:55:24 +00:00
case <- time . After ( 5 * time . Second ) :
2020-07-09 16:52:26 +00:00
return nil , errors . New ( "no registration response received" )
case <- c . pushNotificationRegistrationResponses :
return nil , nil
2020-07-07 13:55:24 +00:00
}
}
2020-06-30 07:50:59 +00:00
}
// HandlePushNotificationRegistrationResponse should check whether the response was successful or not, retry if necessary otherwise store the result in the database
2020-07-09 16:52:26 +00:00
func ( c * Client ) HandlePushNotificationRegistrationResponse ( response protobuf . PushNotificationRegistrationResponse ) error {
c . config . Logger . Debug ( "received push notification registration response" , zap . Any ( "response" , response ) )
select {
case c . pushNotificationRegistrationResponses <- & response :
default :
return errors . New ( "could not process push notification registration response" )
}
2020-06-30 07:50:59 +00:00
return nil
}
// HandlePushNotificationAdvertisement should store any info related to push notifications
2020-06-30 08:30:58 +00:00
func ( p * Client ) HandlePushNotificationAdvertisement ( info * protobuf . PushNotificationAdvertisementInfo ) error {
2020-06-30 07:50:59 +00:00
return nil
}
// HandlePushNotificationQueryResponse should update the data in the database for a given user
2020-07-09 16:52:26 +00:00
func ( c * Client ) HandlePushNotificationQueryResponse ( response protobuf . PushNotificationQueryResponse ) error {
2020-07-07 13:55:24 +00:00
c . config . Logger . Debug ( "received push notification query response" , zap . Any ( "response" , response ) )
select {
2020-07-09 16:52:26 +00:00
case c . pushNotificationQueryResponses <- & response :
2020-07-07 13:55:24 +00:00
default :
return errors . New ( "could not process push notification query response" )
}
2020-06-30 07:50:59 +00:00
return nil
}
2020-07-03 08:26:35 +00:00
// HandlePushNotificationResponse should set the request as processed
func ( p * Client ) HandlePushNotificationResponse ( ack * protobuf . PushNotificationResponse ) error {
2020-06-30 07:50:59 +00:00
return nil
}
2020-07-07 09:00:04 +00:00
func ( c * Client ) AddPushNotificationServer ( publicKey * ecdsa . PublicKey ) error {
2020-07-07 13:55:24 +00:00
c . config . Logger . Debug ( "adding push notification server" , zap . Any ( "public-key" , publicKey ) )
2020-07-07 09:00:04 +00:00
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" )
}
}
return c . persistence . UpsertServer ( & PushNotificationServer {
publicKey : publicKey ,
} )
2020-06-30 07:50:59 +00:00
}
2020-07-07 09:00:04 +00:00
func ( c * Client ) RetrievePushNotificationInfo ( publicKey * ecdsa . PublicKey ) ( [ ] * PushNotificationInfo , 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 {
return nil , err
}
rawMessage := & common . RawMessage {
Payload : encodedMessage ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY ,
}
encodedPublicKey := hex . EncodeToString ( hashedPublicKey )
c . config . Logger . Debug ( "sending query" )
messageID , err := c . messageProcessor . SendPublic ( context . Background ( ) , encodedPublicKey , rawMessage )
// TODO: this is probably best done by polling the database instead
for {
select {
case <- c . quit :
return nil , nil
case <- time . After ( 5 * time . Second ) :
return nil , errors . New ( "no registration query response received" )
case response := <- c . pushNotificationQueryResponses :
if bytes . Compare ( response . MessageId , messageID ) != 0 {
// Not for us, queue back
c . pushNotificationQueryResponses <- response
// This is not accurate, we should then shrink the timeout
// Also we should handle multiple responses
continue
}
2020-07-07 09:00:04 +00:00
2020-07-09 16:52:26 +00:00
if len ( response . Info ) == 0 {
return nil , errors . New ( "empty response from the server" )
2020-07-07 09:00:04 +00:00
}
2020-07-09 16:52:26 +00:00
var pushNotificationInfo [ ] * PushNotificationInfo
for _ , info := range response . Info {
if bytes . Compare ( info . PublicKey , hashedPublicKey ) != 0 {
continue
}
pushNotificationInfo = append ( pushNotificationInfo , & PushNotificationInfo {
PublicKey : publicKey ,
AccessToken : info . AccessToken ,
InstallationID : info . InstallationId ,
} )
}
return pushNotificationInfo , nil
2020-07-07 09:00:04 +00:00
}
2020-07-09 16:52:26 +00:00
}
}
2020-07-07 09:00:04 +00:00
2020-07-09 16:52:26 +00:00
func ( s * Client ) listenToPublicKeyQueryTopic ( hashedPublicKey [ ] byte ) error {
encodedPublicKey := hex . EncodeToString ( hashedPublicKey )
return s . messageProcessor . JoinPublic ( encodedPublicKey )
2020-06-30 07:50:59 +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
}
2020-07-07 13:55:24 +00:00
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-06-30 07:50:59 +00:00
}