Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 55 additions & 23 deletions gcm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type GCMRegistrationOpts struct {
AppID string
InstanceID string
Expiry time.Duration
LegacyIID *LegacyIIDConfig
}

func setGCMAppID(values url.Values, appID string) {
Expand Down Expand Up @@ -53,25 +54,34 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede
appID = NewGCMAppID(authorizationEntity)
}

if opts != nil && opts.InstanceID != "" {
values.Set("appid", opts.InstanceID)
}

if opts != nil && opts.Expiry != 0 {
ttl := strconv.Itoa(int(opts.Expiry.Seconds()))
values.Set("ttl", ttl)
if opts != nil && opts.LegacyIID != nil {
var err error
values, err = legacyIIDRegistrationValues(authorizationEntity, creds, opts)
if err != nil {
return nil, err
}
} else {
if opts != nil && opts.InstanceID != "" {
values.Set("appid", opts.InstanceID)
}
if opts != nil && opts.Expiry != 0 {
values.Set("ttl", strconv.Itoa(int(opts.Expiry.Seconds())))
}
setGCMAppID(values, appID)
values.Set("scope", "GCM")
values.Set("X-scope", "GCM")
values.Set("device", fmt.Sprint(creds.AndroidID))
values.Set("gmsv", strings.Split(chromeVersion, ".")[0])
values.Set("sender", authorizationEntity)
}

setGCMAppID(values, appID)
values.Set("scope", "GCM")
values.Set("X-scope", "GCM")
values.Set("device", fmt.Sprint(creds.AndroidID))
values.Set("gmsv", strings.Split(chromeVersion, ".")[0])
values.Set("sender", authorizationEntity)

res, err := postRequest(ctx, registerURL, strings.NewReader(values.Encode()), func(header *http.Header) {
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Set("Authorization", fmt.Sprintf("AidLogin %d:%d", creds.AndroidID, creds.SecurityToken))
if opts != nil && opts.LegacyIID != nil {
header.Set("app", opts.LegacyIID.PackageName)
header.Set("User-Agent", fmt.Sprintf("Android-GCM/1.5 (%s %s)", opts.LegacyIID.Device, opts.LegacyIID.BuildID))
}
})
if err != nil {
return nil, errors.Wrap(err, "request GCM register")
Expand All @@ -92,18 +102,40 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede
}, nil
}

func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string) error {
func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string, options ...*GCMRegistrationOpts) error {
if len(options) > 1 {
return errors.New("multiple GCM registration options")
}
var opts *GCMRegistrationOpts
if len(options) == 1 {
opts = options[0]
}
values := url.Values{}
setGCMAppID(values, appID)
values.Set("scope", "GCM")
values.Set("X-scope", "GCM")
values.Set("device", fmt.Sprint(creds.AndroidID))
values.Set("gmsv", strings.Split(chromeVersion, ".")[0])
values.Set("sender", authorizationEntity)
values.Set("delete", "true")
deletedAppID := appID
if opts != nil && opts.LegacyIID != nil {
var err error
values, err = legacyIIDRegistrationValues(authorizationEntity, creds, opts)
if err != nil {
return err
}
values.Set("delete", "1")
deletedAppID = opts.LegacyIID.PackageName
} else {
setGCMAppID(values, appID)
values.Set("scope", "GCM")
values.Set("X-scope", "GCM")
values.Set("device", fmt.Sprint(creds.AndroidID))
values.Set("gmsv", strings.Split(chromeVersion, ".")[0])
values.Set("sender", authorizationEntity)
values.Set("delete", "true")
}
res, err := postRequest(ctx, registerURL, strings.NewReader(values.Encode()), func(header *http.Header) {
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Set("Authorization", fmt.Sprintf("AidLogin %d:%d", creds.AndroidID, creds.SecurityToken))
if opts != nil && opts.LegacyIID != nil {
header.Set("app", opts.LegacyIID.PackageName)
header.Set("User-Agent", fmt.Sprintf("Android-GCM/1.5 (%s %s)", opts.LegacyIID.Device, opts.LegacyIID.BuildID))
}
})
if err != nil {
return errors.Wrap(err, "failed to unregister with GCM")
Expand All @@ -113,7 +145,7 @@ func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCre
if err != nil {
return errors.Wrap(err, "read GCM unregister response")
}
if response.Get("token") == "" && response.Get("deleted") != appID {
if response.Get("token") == "" && response.Get("deleted") != deletedAppID {
return errors.New("GCM unregister response missing confirmation")
}

Expand Down
85 changes: 85 additions & 0 deletions legacy_iid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package pushreceiver

import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"fmt"
"net/url"
"strconv"
"strings"
)

type LegacyIIDConfig struct {
PackageName string `json:"package_name"`
CertificateSHA1 string `json:"certificate_sha1"`
FirebaseAppID string `json:"firebase_app_id"`
AppVersionCode uint64 `json:"app_version_code"`
AppVersionName string `json:"app_version_name"`
TargetSDKVersion uint32 `json:"target_sdk_version"`
OSVersion uint32 `json:"os_version"`
GMSVersion uint64 `json:"gms_version"`
ClientVersion string `json:"client_version"`
Device string `json:"device"`
BuildID string `json:"build_id"`
}

func (config LegacyIIDConfig) Validate() error {
for _, value := range []string{config.PackageName, config.FirebaseAppID, config.ClientVersion, config.Device, config.BuildID} {
if value == "" || strings.ContainsAny(value, "\x00\r\n") {
return fmt.Errorf("invalid legacy IID application metadata")
}
}
certificate, err := hex.DecodeString(config.CertificateSHA1)
if err != nil || len(certificate) != sha1.Size {
return fmt.Errorf("invalid legacy IID application certificate")
}
if config.AppVersionCode == 0 || config.TargetSDKVersion == 0 || config.OSVersion == 0 || config.GMSVersion == 0 {
return fmt.Errorf("invalid legacy IID application version")
}
return nil
}

func NewLegacyInstanceID() (string, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", err
}
publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return "", err
}
digest := sha1.Sum(publicKey)
digest[0] = (digest[0] & 15) + 112
return base64.RawURLEncoding.EncodeToString(digest[:8]), nil
}

func legacyIIDRegistrationValues(sender string, creds GCMCredentials, opts *GCMRegistrationOpts) (url.Values, error) {
config := opts.LegacyIID
if err := config.Validate(); err != nil {
return nil, err
}
if value, err := strconv.ParseUint(sender, 10, 64); err != nil || value == 0 {
return nil, fmt.Errorf("invalid legacy IID sender")
}
iid, err := base64.RawURLEncoding.DecodeString(opts.InstanceID)
if err != nil || len(iid) != 8 || iid[0]&0xf0 != 0x70 || base64.RawURLEncoding.EncodeToString(iid) != opts.InstanceID {
return nil, fmt.Errorf("invalid legacy instance ID")
}
if creds.AndroidID == 0 || creds.SecurityToken == 0 {
return nil, fmt.Errorf("missing legacy IID check-in credentials")
}
appVersion := strconv.FormatUint(config.AppVersionCode, 10)
return url.Values{
"app": {config.PackageName}, "cert": {strings.ToLower(config.CertificateSHA1)},
"app_ver": {appVersion}, "target_ver": {strconv.FormatUint(uint64(config.TargetSDKVersion), 10)},
"device": {strconv.FormatUint(creds.AndroidID, 10)}, "sender": {sender},
"X-scope": {"*"}, "X-subtype": {sender}, "X-appid": {opts.InstanceID},
"X-gmp_app_id": {config.FirebaseAppID}, "X-gmsv": {strconv.FormatUint(config.GMSVersion, 10)},
"X-osv": {strconv.FormatUint(uint64(config.OSVersion), 10)}, "X-app_ver": {appVersion},
"X-app_ver_name": {config.AppVersionName}, "X-cliv": {config.ClientVersion},
}, nil
}
Loading