2020-07-22 07:41:40 +00:00
package pushnotificationserver
2020-06-30 08:30:58 +00:00
import (
2020-07-07 09:00:04 +00:00
"context"
2020-06-30 13:14:25 +00:00
"crypto/ecdsa"
2020-07-09 16:52:26 +00:00
"encoding/hex"
2020-06-30 13:14:25 +00:00
2020-06-30 14:55:24 +00:00
"github.com/golang/protobuf/proto"
2020-07-01 08:37:54 +00:00
"github.com/google/uuid"
2020-08-20 07:26:00 +00:00
"github.com/pkg/errors"
2020-07-22 07:41:40 +00:00
"go.uber.org/zap"
2020-06-30 14:55:24 +00:00
2020-07-13 08:53:13 +00:00
"github.com/status-im/status-go/eth-node/crypto"
2020-06-30 13:14:25 +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 08:30:58 +00:00
"github.com/status-im/status-go/protocol/protobuf"
)
2020-06-30 13:14:25 +00:00
const encryptedPayloadKeyLength = 16
2020-07-13 10:39:33 +00:00
const defaultGorushURL = "https://gorush.status.im"
2020-06-30 13:14:25 +00:00
2020-06-30 08:30:58 +00:00
type Config struct {
2020-08-20 07:26:00 +00:00
Enabled bool
2020-06-30 08:30:58 +00:00
// Identity is our identity key
Identity * ecdsa . PrivateKey
// GorushUrl is the url for the gorush service
GorushURL string
2020-07-03 10:08:47 +00:00
Logger * zap . Logger
2020-06-30 08:30:58 +00:00
}
type Server struct {
2020-07-06 08:54:22 +00:00
persistence Persistence
config * Config
messageProcessor * common . MessageProcessor
2020-06-30 08:30:58 +00:00
}
2020-07-06 08:54:22 +00:00
func New ( config * Config , persistence Persistence , messageProcessor * common . MessageProcessor ) * Server {
2020-07-13 10:39:33 +00:00
if len ( config . GorushURL ) == 0 {
config . GorushURL = defaultGorushURL
}
2020-07-06 08:54:22 +00:00
return & Server { persistence : persistence , config : config , messageProcessor : messageProcessor }
2020-06-30 08:30:58 +00:00
}
2020-07-22 07:41:40 +00:00
func ( s * Server ) Start ( ) error {
2020-08-20 07:26:00 +00:00
if s . config . Logger == nil {
logger , err := zap . NewDevelopment ( )
if err != nil {
return errors . Wrap ( err , "failed to create a logger" )
}
s . config . Logger = logger
}
2020-07-22 07:41:40 +00:00
s . config . Logger . Info ( "starting push notification server" )
if s . config . Identity == nil {
s . config . Logger . Info ( "Identity nil" )
// Pull identity from database
identity , err := s . persistence . GetIdentity ( )
if err != nil {
return err
}
if identity == nil {
identity , err = crypto . GenerateKey ( )
if err != nil {
return err
}
if err := s . persistence . SaveIdentity ( identity ) ; err != nil {
return err
}
}
s . config . Identity = identity
}
pks , err := s . persistence . GetPushNotificationRegistrationPublicKeys ( )
if err != nil {
return err
}
// listen to all topics for users registered
for _ , pk := range pks {
if err := s . listenToPublicKeyQueryTopic ( pk ) ; err != nil {
return err
}
}
s . config . Logger . Info ( "started push notification server" , zap . String ( "identity" , types . EncodeHex ( crypto . FromECDSAPub ( & s . config . Identity . PublicKey ) ) ) )
return nil
}
// HandlePushNotificationRegistration builds a response for the registration and sends it back to the user
func ( s * Server ) HandlePushNotificationRegistration ( publicKey * ecdsa . PublicKey , payload [ ] byte ) error {
response := s . buildPushNotificationRegistrationResponse ( publicKey , payload )
if response == nil {
return nil
}
encodedMessage , err := proto . Marshal ( response )
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 ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION_RESPONSE ,
// we skip encryption as might be sent from an ephemeral key
SkipEncryption : true ,
}
_ , err = s . messageProcessor . SendPrivate ( context . Background ( ) , publicKey , rawMessage )
return err
}
// HandlePushNotificationQuery builds a response for the query and sends it back to the user
func ( s * Server ) HandlePushNotificationQuery ( publicKey * ecdsa . PublicKey , messageID [ ] byte , query protobuf . PushNotificationQuery ) error {
response := s . buildPushNotificationQueryResponse ( & query )
if response == nil {
return nil
}
response . MessageId = messageID
encodedMessage , err := proto . Marshal ( response )
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 ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY_RESPONSE ,
// we skip encryption as sent from an ephemeral key
SkipEncryption : true ,
}
_ , err = s . messageProcessor . SendPrivate ( context . Background ( ) , publicKey , rawMessage )
return err
}
// HandlePushNotificationRequest will send a gorush notification and send a response back to the user
func ( s * Server ) HandlePushNotificationRequest ( publicKey * ecdsa . PublicKey ,
request protobuf . PushNotificationRequest ) error {
s . config . Logger . Debug ( "handling pn request" )
response := s . buildPushNotificationRequestResponseAndSendNotification ( & request )
if response == nil {
return nil
}
encodedMessage , err := proto . Marshal ( response )
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 ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_RESPONSE ,
// We skip encryption here as the message has been sent from an ephemeral key
SkipEncryption : true ,
}
_ , err = s . messageProcessor . SendPrivate ( context . Background ( ) , publicKey , rawMessage )
return err
}
// 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 ( s * Server ) 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 ) ... )
a := crypto . Keccak256 ( signatureMaterial )
return a
}
func ( s * Server ) verifyGrantSignature ( clientPublicKey * ecdsa . PublicKey , accessToken string , grant [ ] byte ) error {
signatureMaterial := s . buildGrantSignatureMaterial ( clientPublicKey , & s . config . Identity . PublicKey , accessToken )
recoveredPublicKey , err := crypto . SigToPub ( signatureMaterial , grant )
if err != nil {
return err
}
if ! common . IsPubKeyEqual ( recoveredPublicKey , clientPublicKey ) {
return errors . New ( "pubkey mismatch" )
}
return nil
}
func ( s * Server ) generateSharedKey ( publicKey * ecdsa . PublicKey ) ( [ ] byte , error ) {
return ecies . ImportECDSA ( s . config . Identity ) . GenerateShared (
2020-06-30 14:55:24 +00:00
ecies . ImportECDSAPublic ( publicKey ) ,
encryptedPayloadKeyLength ,
encryptedPayloadKeyLength ,
)
}
2020-07-22 07:41:40 +00:00
func ( s * Server ) validateUUID ( u string ) error {
2020-07-01 08:37:54 +00:00
if len ( u ) == 0 {
return errors . New ( "empty uuid" )
}
_ , err := uuid . Parse ( u )
return err
}
2020-07-22 07:41:40 +00:00
func ( s * Server ) decryptRegistration ( publicKey * ecdsa . PublicKey , payload [ ] byte ) ( [ ] byte , error ) {
sharedKey , err := s . generateSharedKey ( publicKey )
2020-07-01 10:09:40 +00:00
if err != nil {
return nil , err
}
2020-07-07 09:00:04 +00:00
return common . Decrypt ( payload , sharedKey )
2020-07-01 10:09:40 +00:00
}
2020-07-22 07:41:40 +00:00
// validateRegistration validates a new message against the last one received for a given installationID and and public key
2020-07-01 08:53:05 +00:00
// and return the decrypted message
2020-07-22 07:41:40 +00:00
func ( s * Server ) validateRegistration ( publicKey * ecdsa . PublicKey , payload [ ] byte ) ( * protobuf . PushNotificationRegistration , error ) {
2020-06-30 13:14:25 +00:00
if payload == nil {
2020-07-02 08:08:19 +00:00
return nil , ErrEmptyPushNotificationRegistrationPayload
2020-06-30 13:14:25 +00:00
}
if publicKey == nil {
2020-07-02 08:08:19 +00:00
return nil , ErrEmptyPushNotificationRegistrationPublicKey
2020-06-30 13:14:25 +00:00
}
2020-07-13 08:53:13 +00:00
decryptedPayload , err := s . decryptRegistration ( publicKey , payload )
2020-06-30 13:14:25 +00:00
if err != nil {
2020-07-01 08:53:05 +00:00
return nil , err
2020-06-30 08:30:58 +00:00
}
2020-06-30 13:14:25 +00:00
2020-07-02 08:08:19 +00:00
registration := & protobuf . PushNotificationRegistration { }
2020-06-30 14:55:24 +00:00
2020-07-02 08:08:19 +00:00
if err := proto . Unmarshal ( decryptedPayload , registration ) ; err != nil {
return nil , ErrCouldNotUnmarshalPushNotificationRegistration
2020-06-30 14:55:24 +00:00
}
2020-07-02 08:08:19 +00:00
if registration . Version < 1 {
return nil , ErrInvalidPushNotificationRegistrationVersion
2020-07-01 08:37:54 +00:00
}
2020-07-13 08:53:13 +00:00
if err := s . validateUUID ( registration . InstallationId ) ; err != nil {
2020-07-02 08:08:19 +00:00
return nil , ErrMalformedPushNotificationRegistrationInstallationID
2020-07-01 08:37:54 +00:00
}
2020-07-30 14:47:24 +00:00
previousVersion , err := s . persistence . GetPushNotificationRegistrationVersion ( common . HashPublicKey ( publicKey ) , registration . InstallationId )
2020-07-01 10:09:40 +00:00
if err != nil {
return nil , err
}
2020-07-30 14:47:24 +00:00
if registration . Version <= previousVersion {
2020-07-02 08:08:19 +00:00
return nil , ErrInvalidPushNotificationRegistrationVersion
2020-07-01 10:09:40 +00:00
}
2020-07-22 07:41:40 +00:00
// unregistering message
2020-07-02 08:08:19 +00:00
if registration . Unregister {
return registration , nil
2020-07-01 08:37:54 +00:00
}
2020-07-13 08:53:13 +00:00
if err := s . validateUUID ( registration . AccessToken ) ; err != nil {
2020-07-02 08:08:19 +00:00
return nil , ErrMalformedPushNotificationRegistrationAccessToken
2020-07-01 08:37:54 +00:00
}
2020-07-13 08:53:13 +00:00
if len ( registration . Grant ) == 0 {
return nil , ErrMalformedPushNotificationRegistrationGrant
}
if err := s . verifyGrantSignature ( publicKey , registration . AccessToken , registration . Grant ) ; err != nil {
s . config . Logger . Error ( "failed to verify grant" , zap . Error ( err ) )
return nil , ErrMalformedPushNotificationRegistrationGrant
}
2020-07-22 07:41:40 +00:00
if len ( registration . DeviceToken ) == 0 {
2020-07-02 08:08:19 +00:00
return nil , ErrMalformedPushNotificationRegistrationDeviceToken
2020-06-30 14:55:24 +00:00
}
2020-06-30 13:14:25 +00:00
2020-07-02 14:19:21 +00:00
if registration . TokenType == protobuf . PushNotificationRegistration_UNKNOWN_TOKEN_TYPE {
return nil , ErrUnknownPushNotificationRegistrationTokenType
}
2020-07-02 08:08:19 +00:00
return registration , nil
2020-06-30 08:30:58 +00:00
}
2020-06-30 13:14:25 +00:00
2020-07-22 07:41:40 +00:00
// buildPushNotificationQueryResponse check if we have the client information and send them back
func ( s * Server ) buildPushNotificationQueryResponse ( query * protobuf . PushNotificationQuery ) * protobuf . PushNotificationQueryResponse {
2020-07-09 16:52:26 +00:00
2020-07-22 07:41:40 +00:00
s . config . Logger . Debug ( "handling push notification query" )
2020-07-02 13:57:50 +00:00
response := & protobuf . PushNotificationQueryResponse { }
if query == nil || len ( query . PublicKeys ) == 0 {
return response
}
2020-07-14 14:07:19 +00:00
registrations , err := s . persistence . GetPushNotificationRegistrationByPublicKeys ( query . PublicKeys )
2020-07-02 13:57:50 +00:00
if err != nil {
2020-07-14 14:07:19 +00:00
s . config . Logger . Error ( "failed to retrieve registration" , zap . Error ( err ) )
2020-07-02 13:57:50 +00:00
return response
}
for _ , idAndResponse := range registrations {
registration := idAndResponse . Registration
2020-07-30 14:47:24 +00:00
2020-07-02 13:57:50 +00:00
info := & protobuf . PushNotificationQueryInfo {
PublicKey : idAndResponse . ID ,
2020-07-13 08:53:13 +00:00
Grant : registration . Grant ,
2020-07-17 11:41:49 +00:00
Version : registration . Version ,
2020-07-02 13:57:50 +00:00
InstallationId : registration . InstallationId ,
}
2020-07-22 07:41:40 +00:00
// if instructed to only allow from contacts, send back a list
2020-07-20 08:32:00 +00:00
if registration . AllowFromContactsOnly {
2020-07-22 07:41:40 +00:00
info . AllowedKeyList = registration . AllowedKeyList
2020-07-02 13:57:50 +00:00
} else {
info . AccessToken = registration . AccessToken
}
response . Info = append ( response . Info , info )
}
response . Success = true
return response
}
2020-07-22 07:41:40 +00:00
// buildPushNotificationRequestResponseAndSendNotification will build a response
// and fire-and-forget send a query to the gorush instance
func ( s * Server ) buildPushNotificationRequestResponseAndSendNotification ( request * protobuf . PushNotificationRequest ) * protobuf . PushNotificationResponse {
2020-07-03 08:02:28 +00:00
response := & protobuf . PushNotificationResponse { }
// We don't even send a response in this case
if request == nil || len ( request . MessageId ) == 0 {
2020-07-14 14:07:19 +00:00
s . config . Logger . Warn ( "empty message id" )
2020-07-03 08:02:28 +00:00
return nil
}
response . MessageId = request . MessageId
2020-07-10 13:26:06 +00:00
// TODO: filter by chat id
2020-07-22 07:41:40 +00:00
// collect successful requests & registrations
2020-07-03 08:02:28 +00:00
var requestAndRegistrations [ ] * RequestAndRegistration
for _ , pn := range request . Requests {
2020-07-14 14:07:19 +00:00
registration , err := s . persistence . GetPushNotificationRegistrationByPublicKeyAndInstallationID ( pn . PublicKey , pn . InstallationId )
2020-07-03 08:02:28 +00:00
report := & protobuf . PushNotificationReport {
PublicKey : pn . PublicKey ,
InstallationId : pn . InstallationId ,
}
2020-08-18 11:28:05 +00:00
if pn . Type != protobuf . PushNotification_MESSAGE {
s . config . Logger . Warn ( "unhandled type" )
continue
}
2020-07-03 08:02:28 +00:00
if err != nil {
2020-07-14 14:07:19 +00:00
s . config . Logger . Error ( "failed to retrieve registration" , zap . Error ( err ) )
2020-07-03 08:02:28 +00:00
report . Error = protobuf . PushNotificationReport_UNKNOWN_ERROR_TYPE
2020-07-30 14:47:24 +00:00
} else if registration == nil {
2020-07-14 14:07:19 +00:00
s . config . Logger . Warn ( "empty registration" )
2020-07-03 08:02:28 +00:00
report . Error = protobuf . PushNotificationReport_NOT_REGISTERED
} else if registration . AccessToken != pn . AccessToken {
report . Error = protobuf . PushNotificationReport_WRONG_TOKEN
} else {
// For now we just assume that the notification will be successful
requestAndRegistrations = append ( requestAndRegistrations , & RequestAndRegistration {
Request : pn ,
Registration : registration ,
} )
report . Success = true
}
response . Reports = append ( response . Reports , report )
}
2020-07-22 07:41:40 +00:00
s . config . Logger . Debug ( "built pn request" )
2020-07-03 08:02:28 +00:00
if len ( requestAndRegistrations ) == 0 {
2020-07-14 14:07:19 +00:00
s . config . Logger . Warn ( "no request and registration" )
2020-07-03 08:02:28 +00:00
return response
}
// This can be done asynchronously
goRushRequest := PushNotificationRegistrationToGoRushRequest ( requestAndRegistrations )
2020-07-30 06:55:56 +00:00
err := sendGoRushNotification ( goRushRequest , s . config . GorushURL , s . config . Logger )
2020-07-03 08:02:28 +00:00
if err != nil {
2020-07-14 14:07:19 +00:00
s . config . Logger . Error ( "failed to send go rush notification" , zap . Error ( err ) )
2020-07-03 08:02:28 +00:00
// TODO: handle this error?
2020-07-22 07:41:40 +00:00
// GoRush will not let us know that the sending of the push notification has failed,
// so this likely mean that the actual HTTP request has failed, or there was some unexpected error
2020-07-03 08:02:28 +00:00
}
return response
}
2020-07-22 07:41:40 +00:00
// listenToPublicKeyQueryTopic listen to a topic derived from the hashed public key
func ( s * Server ) listenToPublicKeyQueryTopic ( hashedPublicKey [ ] byte ) error {
if s . messageProcessor == nil {
return nil
}
encodedPublicKey := hex . EncodeToString ( hashedPublicKey )
return s . messageProcessor . JoinPublic ( encodedPublicKey )
}
// buildPushNotificationRegistrationResponse will check the registration is valid, save it, and listen to the topic for the queries
func ( s * Server ) buildPushNotificationRegistrationResponse ( publicKey * ecdsa . PublicKey , payload [ ] byte ) * protobuf . PushNotificationRegistrationResponse {
2020-07-07 13:55:24 +00:00
2020-07-14 14:07:19 +00:00
s . config . Logger . Info ( "handling push notification registration" )
2020-07-02 08:08:19 +00:00
response := & protobuf . PushNotificationRegistrationResponse {
2020-07-07 09:00:04 +00:00
RequestId : common . Shake256 ( payload ) ,
2020-07-02 08:08:19 +00:00
}
2020-07-22 07:41:40 +00:00
registration , err := s . validateRegistration ( publicKey , payload )
2020-07-01 10:09:40 +00:00
if err != nil {
2020-07-02 08:08:19 +00:00
if err == ErrInvalidPushNotificationRegistrationVersion {
response . Error = protobuf . PushNotificationRegistrationResponse_VERSION_MISMATCH
} else {
response . Error = protobuf . PushNotificationRegistrationResponse_MALFORMED_MESSAGE
}
2020-07-07 13:55:24 +00:00
s . config . Logger . Warn ( "registration did not validate" , zap . Error ( err ) )
2020-07-02 08:08:19 +00:00
return response
2020-07-01 10:09:40 +00:00
}
2020-07-02 08:08:19 +00:00
if registration . Unregister {
2020-07-30 13:37:32 +00:00
s . config . Logger . Info ( "unregistering client" )
2020-07-02 08:08:19 +00:00
// We save an empty registration, only keeping version and installation-id
2020-07-30 14:47:24 +00:00
if err := s . persistence . UnregisterPushNotificationRegistration ( common . HashPublicKey ( publicKey ) , registration . InstallationId , registration . Version ) ; err != nil {
2020-07-02 08:08:19 +00:00
response . Error = protobuf . PushNotificationRegistrationResponse_INTERNAL_ERROR
2020-07-07 13:55:24 +00:00
s . config . Logger . Error ( "failed to unregister " , zap . Error ( err ) )
2020-07-02 08:08:19 +00:00
return response
}
2020-07-07 13:55:24 +00:00
} else if err := s . persistence . SavePushNotificationRegistration ( common . HashPublicKey ( publicKey ) , registration ) ; err != nil {
2020-07-02 08:08:19 +00:00
response . Error = protobuf . PushNotificationRegistrationResponse_INTERNAL_ERROR
2020-07-07 13:55:24 +00:00
s . config . Logger . Error ( "failed to save registration" , zap . Error ( err ) )
2020-07-02 08:08:19 +00:00
return response
}
2020-07-09 16:52:26 +00:00
if err := s . listenToPublicKeyQueryTopic ( common . HashPublicKey ( publicKey ) ) ; err != nil {
response . Error = protobuf . PushNotificationRegistrationResponse_INTERNAL_ERROR
s . config . Logger . Error ( "failed to listen to topic" , zap . Error ( err ) )
return response
}
2020-07-02 08:08:19 +00:00
response . Success = true
2020-07-14 14:07:19 +00:00
s . config . Logger . Info ( "handled push notification registration successfully" )
2020-07-07 13:55:24 +00:00
2020-07-02 08:08:19 +00:00
return response
2020-07-01 10:09:40 +00:00
}