272 lines
7.9 KiB
Go
272 lines
7.9 KiB
Go
package push_notification_client
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"io"
|
|
|
|
"golang.org/x/crypto/sha3"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/status-im/status-go/eth-node/crypto"
|
|
"github.com/status-im/status-go/eth-node/crypto/ecies"
|
|
"github.com/status-im/status-go/protocol/encryption"
|
|
"github.com/status-im/status-go/protocol/protobuf"
|
|
)
|
|
|
|
const accessTokenKeyLength = 16
|
|
|
|
type PushNotificationServer struct {
|
|
key *ecdsa.PublicKey
|
|
registered bool
|
|
}
|
|
|
|
type Config struct {
|
|
// 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
|
|
// ContactIDs is the public keys for each contact that we allow notifications from
|
|
ContactIDs []*ecdsa.PublicKey
|
|
// MutedChatIDs is the IDs of the chats we don't want to receive notifications from
|
|
MutedChatIDs []string
|
|
// 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
|
|
}
|
|
|
|
type Client struct {
|
|
persistence *Persistence
|
|
config *Config
|
|
|
|
// lastPushNotificationPreferences is the latest known push notification preferences message
|
|
lastPushNotificationPreferences *protobuf.PushNotificationPreferences
|
|
|
|
// 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
|
|
}
|
|
|
|
func New(persistence *Persistence) *Client {
|
|
return &Client{persistence: persistence, reader: rand.Reader}
|
|
}
|
|
|
|
// This likely will return a channel as it's an asynchrous operation
|
|
func fetchNotificationInfoFor(publicKey *ecdsa.PublicKey) error {
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
func (p *Client) HandleMessageSent(publicKey *ecdsa.PublicKey, spec *encryption.ProtocolMessageSpec, messageIDs [][]byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *Client) NotifyOnMessageID(messageID []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *Client) mutedChatIDsHashes() [][]byte {
|
|
var mutedChatListHashes [][]byte
|
|
|
|
for _, chatID := range p.config.MutedChatIDs {
|
|
mutedChatListHashes = append(mutedChatListHashes, shake256(chatID))
|
|
}
|
|
|
|
return mutedChatListHashes
|
|
}
|
|
|
|
func (p *Client) reEncryptTokenPair(token []byte, pair *protobuf.PushNotificationTokenPair) (*protobuf.PushNotificationTokenPair, error) {
|
|
publicKey, err := crypto.DecompressPubkey(pair.PublicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.encryptTokenPair(publicKey, token)
|
|
}
|
|
|
|
func (p *Client) encryptTokenPair(publicKey *ecdsa.PublicKey, token []byte) (*protobuf.PushNotificationTokenPair, error) {
|
|
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
|
|
}
|
|
|
|
return &protobuf.PushNotificationTokenPair{
|
|
Token: encryptedToken,
|
|
PublicKey: crypto.CompressPubkey(publicKey),
|
|
}, nil
|
|
}
|
|
|
|
func (p *Client) allowedUserList(token []byte) ([]*protobuf.PushNotificationTokenPair, error) {
|
|
var tokenPairs []*protobuf.PushNotificationTokenPair
|
|
for _, publicKey := range p.config.ContactIDs {
|
|
tokenPair, err := p.encryptTokenPair(publicKey, token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tokenPairs = append(tokenPairs, tokenPair)
|
|
|
|
}
|
|
return tokenPairs, nil
|
|
}
|
|
|
|
func (p *Client) reEncryptAllowedUserList(token []byte, oldTokenPairs []*protobuf.PushNotificationTokenPair) ([]*protobuf.PushNotificationTokenPair, error) {
|
|
var tokenPairs []*protobuf.PushNotificationTokenPair
|
|
for _, tokenPair := range oldTokenPairs {
|
|
tokenPair, err := p.reEncryptTokenPair(token, tokenPair)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tokenPairs = append(tokenPairs, tokenPair)
|
|
|
|
}
|
|
return tokenPairs, nil
|
|
}
|
|
|
|
func (p *Client) buildPushNotificationOptionsMessage(token string) (*protobuf.PushNotificationOptions, error) {
|
|
allowedUserList, err := p.allowedUserList([]byte(token))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
options := &protobuf.PushNotificationOptions{
|
|
InstallationId: p.config.InstallationID,
|
|
Token: p.DeviceToken,
|
|
Enabled: p.config.RemoteNotificationsEnabled,
|
|
BlockedChatList: p.mutedChatIDsHashes(),
|
|
AllowedUserList: allowedUserList,
|
|
}
|
|
return options, nil
|
|
}
|
|
|
|
func (p *Client) buildPushNotificationPreferencesMessage() (*protobuf.PushNotificationPreferences, error) {
|
|
pushNotificationPreferences := &protobuf.PushNotificationPreferences{}
|
|
|
|
if p.lastPushNotificationPreferences != nil {
|
|
pushNotificationPreferences = p.lastPushNotificationPreferences
|
|
}
|
|
|
|
// Increment version
|
|
pushNotificationPreferences.Version += 1
|
|
|
|
// Generate new token
|
|
token := uuid.New().String()
|
|
pushNotificationPreferences.AccessToken = token
|
|
|
|
// build options for this device
|
|
ourOptions, err := p.buildPushNotificationOptionsMessage(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
options := []*protobuf.PushNotificationOptions{ourOptions}
|
|
// Re-encrypt token for previous options that don't belong to this
|
|
// device
|
|
for _, option := range pushNotificationPreferences.Options {
|
|
if option.InstallationId != p.config.InstallationID {
|
|
newAllowedUserList, err := p.reEncryptAllowedUserList([]byte(token), option.AllowedUserList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
option.AllowedUserList = newAllowedUserList
|
|
options = append(options, option)
|
|
}
|
|
}
|
|
|
|
pushNotificationPreferences.Options = options
|
|
|
|
return pushNotificationPreferences, nil
|
|
}
|
|
|
|
func (p *Client) Register(deviceToken string) error {
|
|
return nil
|
|
}
|
|
|
|
// HandlePushNotificationRegistrationResponse should check whether the response was successful or not, retry if necessary otherwise store the result in the database
|
|
func (p *Client) HandlePushNotificationRegistrationResponse(response *protobuf.PushNotificationRegistrationResponse) error {
|
|
return nil
|
|
}
|
|
|
|
// HandlePushNotificationAdvertisement should store any info related to push notifications
|
|
func (p *Client) HandlePushNotificationAdvertisement(info *protobuf.PushNotificationAdvertisementInfo) error {
|
|
return nil
|
|
}
|
|
|
|
// HandlePushNotificationQueryResponse should update the data in the database for a given user
|
|
func (p *Client) HandlePushNotificationQueryResponse(response *protobuf.PushNotificationQueryResponse) error {
|
|
return nil
|
|
}
|
|
|
|
// HandlePushNotificationAcknowledgement should set the request as processed
|
|
func (p *Client) HandlePushNotificationAcknowledgement(ack *protobuf.PushNotificationAcknowledgement) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *Client) SetContactIDs(contactIDs []*ecdsa.PublicKey) error {
|
|
p.config.ContactIDs = contactIDs
|
|
// Update or schedule update
|
|
return nil
|
|
}
|
|
|
|
func (p *Client) SetMutedChatIDs(chatIDs []string) error {
|
|
p.config.MutedChatIDs = chatIDs
|
|
// Update or schedule update
|
|
return nil
|
|
}
|
|
|
|
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 shake256(input string) []byte {
|
|
buf := []byte(input)
|
|
h := make([]byte, 64)
|
|
sha3.ShakeSum256(h, buf)
|
|
return h
|
|
}
|