2020-07-01 12:04:09 +00:00
package push_notification_server
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-07-03 08:02:28 +00:00
"errors"
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-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-07-03 10:08:47 +00:00
"go.uber.org/zap"
2020-06-30 08:30:58 +00:00
)
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 {
// 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-06-30 14:55:24 +00:00
func ( p * Server ) generateSharedKey ( publicKey * ecdsa . PublicKey ) ( [ ] byte , error ) {
return ecies . ImportECDSA ( p . config . Identity ) . GenerateShared (
ecies . ImportECDSAPublic ( publicKey ) ,
encryptedPayloadKeyLength ,
encryptedPayloadKeyLength ,
)
}
2020-07-01 08:37:54 +00:00
func ( p * Server ) validateUUID ( u string ) error {
if len ( u ) == 0 {
return errors . New ( "empty uuid" )
}
_ , err := uuid . Parse ( u )
return err
}
2020-07-01 10:09:40 +00:00
func ( p * Server ) decryptRegistration ( publicKey * ecdsa . PublicKey , payload [ ] byte ) ( [ ] byte , error ) {
sharedKey , err := p . generateSharedKey ( publicKey )
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-01 08:53:05 +00:00
// ValidateRegistration validates a new message against the last one received for a given installationID and and public key
// and return the decrypted message
2020-07-13 08:53:13 +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-13 08:53:13 +00:00
previousRegistration , err := s . persistence . GetPushNotificationRegistrationByPublicKeyAndInstallationID ( common . HashPublicKey ( publicKey ) , registration . InstallationId )
2020-07-01 10:09:40 +00:00
if err != nil {
return nil , err
}
2020-07-02 08:08:19 +00:00
if previousRegistration != nil && registration . Version <= previousRegistration . Version {
return nil , ErrInvalidPushNotificationRegistrationVersion
2020-07-01 10:09:40 +00:00
}
2020-07-01 08:37:54 +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-02 08:08:19 +00:00
if len ( registration . Token ) == 0 {
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-14 14:07:19 +00:00
func ( s * Server ) HandlePushNotificationQuery ( query * protobuf . PushNotificationQuery ) * protobuf . PushNotificationQueryResponse {
2020-07-09 16:52:26 +00:00
2020-07-14 14:07:19 +00:00
s . config . Logger . Info ( "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
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-20 08:32:00 +00:00
if registration . AllowFromContactsOnly {
2020-07-02 13:57:50 +00:00
info . AllowedUserList = registration . AllowedUserList
} else {
info . AccessToken = registration . AccessToken
}
response . Info = append ( response . Info , info )
}
response . Success = true
return response
}
2020-07-14 14:07:19 +00:00
func ( s * Server ) HandlePushNotificationRequest ( request * protobuf . PushNotificationRequest ) * protobuf . PushNotificationResponse {
s . config . Logger . Info ( "handling pn request" )
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-03 08:02:28 +00:00
// Collect successful requests & registrations
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 ,
}
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
} 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 {
2020-07-14 14:07:19 +00:00
s . config . Logger . Warn ( "invalid access token" )
2020-07-03 08:02:28 +00:00
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-14 14:07:19 +00:00
s . config . Logger . Info ( "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-14 14:07:19 +00:00
//TODO: REMOVE ME
s . config . Logger . Info ( "REQUEST" , zap . Any ( "REQUEST" , goRushRequest ) )
err := sendGoRushNotification ( goRushRequest , s . config . GorushURL )
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?
}
return response
}
2020-07-07 13:55:24 +00:00
func ( s * Server ) HandlePushNotificationRegistration ( publicKey * ecdsa . PublicKey , payload [ ] byte ) * protobuf . PushNotificationRegistrationResponse {
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-07 13:55:24 +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 {
// We save an empty registration, only keeping version and installation-id
emptyRegistration := & protobuf . PushNotificationRegistration {
Version : registration . Version ,
InstallationId : registration . InstallationId ,
}
2020-07-07 13:55:24 +00:00
if err := s . persistence . SavePushNotificationRegistration ( common . HashPublicKey ( publicKey ) , emptyRegistration ) ; 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
}
2020-07-06 08:54:22 +00:00
2020-07-10 13:26:06 +00:00
func ( s * Server ) Start ( ) error {
2020-07-14 14:07:19 +00:00
s . config . Logger . Info ( "starting push notification server" )
2020-07-13 10:39:33 +00:00
if s . config . Identity == nil {
2020-07-14 14:07:19 +00:00
s . config . Logger . Info ( "Identity nil" )
2020-07-13 10:39:33 +00:00
// 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
}
2020-07-10 13:26:06 +00:00
pks , err := s . persistence . GetPushNotificationRegistrationPublicKeys ( )
if err != nil {
return err
}
for _ , pk := range pks {
if err := s . listenToPublicKeyQueryTopic ( pk ) ; err != nil {
return err
}
}
2020-07-14 14:07:19 +00:00
s . config . Logger . Info ( "started push notification server" , zap . String ( "identity" , types . EncodeHex ( crypto . FromECDSAPub ( & s . config . Identity . PublicKey ) ) ) )
2020-07-10 13:26:06 +00:00
return nil
}
2020-07-09 16:52:26 +00:00
func ( s * Server ) listenToPublicKeyQueryTopic ( hashedPublicKey [ ] byte ) error {
2020-07-10 07:45:40 +00:00
if s . messageProcessor == nil {
return nil
}
2020-07-09 16:52:26 +00:00
encodedPublicKey := hex . EncodeToString ( hashedPublicKey )
return s . messageProcessor . JoinPublic ( encodedPublicKey )
}
2020-07-06 08:54:22 +00:00
func ( p * Server ) HandlePushNotificationRegistration2 ( publicKey * ecdsa . PublicKey , payload [ ] byte ) error {
2020-07-07 09:00:04 +00:00
response := p . HandlePushNotificationRegistration ( publicKey , payload )
if response == nil {
return nil
}
encodedMessage , err := proto . Marshal ( response )
if err != nil {
return err
}
rawMessage := & common . RawMessage {
Payload : encodedMessage ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION_RESPONSE ,
}
2020-07-06 08:54:22 +00:00
2020-07-07 09:00:04 +00:00
_ , err = p . messageProcessor . SendPrivate ( context . Background ( ) , publicKey , rawMessage )
return err
2020-07-06 08:54:22 +00:00
}
2020-07-09 16:52:26 +00:00
func ( p * Server ) HandlePushNotificationQuery2 ( publicKey * ecdsa . PublicKey , messageID [ ] byte , query protobuf . PushNotificationQuery ) error {
2020-07-07 09:00:04 +00:00
response := p . HandlePushNotificationQuery ( & query )
if response == nil {
return nil
}
2020-07-09 16:52:26 +00:00
response . MessageId = messageID
2020-07-07 09:00:04 +00:00
encodedMessage , err := proto . Marshal ( response )
if err != nil {
return err
}
rawMessage := & common . RawMessage {
2020-07-21 15:41:10 +00:00
Payload : encodedMessage ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY_RESPONSE ,
SkipNegotiation : true ,
2020-07-07 09:00:04 +00:00
}
_ , err = p . messageProcessor . SendPrivate ( context . Background ( ) , publicKey , rawMessage )
return err
2020-07-06 08:54:22 +00:00
}
2020-07-14 14:07:19 +00:00
func ( s * Server ) HandlePushNotificationRequest2 ( publicKey * ecdsa . PublicKey ,
2020-07-06 08:54:22 +00:00
request protobuf . PushNotificationRequest ) error {
2020-07-14 14:07:19 +00:00
s . config . Logger . Info ( "handling pn request" )
response := s . HandlePushNotificationRequest ( & request )
2020-07-07 09:00:04 +00:00
if response == nil {
return nil
}
encodedMessage , err := proto . Marshal ( response )
if err != nil {
return err
}
2020-07-06 08:54:22 +00:00
2020-07-07 09:00:04 +00:00
rawMessage := & common . RawMessage {
2020-07-21 15:41:10 +00:00
Payload : encodedMessage ,
MessageType : protobuf . ApplicationMetadataMessage_PUSH_NOTIFICATION_RESPONSE ,
SkipNegotiation : true ,
2020-07-07 09:00:04 +00:00
}
2020-07-14 14:07:19 +00:00
_ , err = s . messageProcessor . SendPrivate ( context . Background ( ) , publicKey , rawMessage )
2020-07-07 09:00:04 +00:00
return err
2020-07-06 08:54:22 +00:00
}
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 ( 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
}