diff --git a/gcm.go b/gcm.go index b43de72..c4b2409 100644 --- a/gcm.go +++ b/gcm.go @@ -25,6 +25,7 @@ type GCMRegistrationOpts struct { AppID string InstanceID string Expiry time.Duration + LegacyIID *LegacyIIDConfig } func setGCMAppID(values url.Values, appID string) { @@ -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") @@ -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") @@ -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") } diff --git a/legacy_iid.go b/legacy_iid.go new file mode 100644 index 0000000..7b3da28 --- /dev/null +++ b/legacy_iid.go @@ -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 +}