Bug 1265593 - Forward app server keys to Autopush on Android. r=nalexander
authorKit Cambridge <kcambridge@mozilla.com>
Tue, 22 Mar 2016 12:09:31 -0700
changeset 338864 ca252a2e21711cf5dd7843de01e04b6143d8900e
parent 338863 9be4384539f40e387d951245919c5da64e338f3a
child 338865 253aa0a9809b9c3f7a4e02a3beb5baca3fc5fe13
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs1265593
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1265593 - Forward app server keys to Autopush on Android. r=nalexander MozReview-Commit-ID: 3J4mM1k0pcY
dom/push/PushServiceAndroidGCM.jsm
mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
mobile/android/base/java/org/mozilla/gecko/push/PushService.java
mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -203,35 +203,42 @@ this.PushServiceAndroidGCM = {
 
   disconnect: function() {
     console.debug("disconnect");
   },
 
   register: function(record) {
     console.debug("register:", record);
     let ctime = Date.now();
+    let appServerKey = record.appServerKey ?
+      ChromeUtils.base64URLEncode(record.appServerKey, {
+        // The Push server requires padding.
+        pad: true,
+      }) : null;
     // Caller handles errors.
     return Messaging.sendRequestForResult({
       type: "PushServiceAndroidGCM:SubscribeChannel",
+      appServerKey: appServerKey,
     }).then(data => {
       console.debug("Got data:", data);
       return PushCrypto.generateKeys()
         .then(exportedKeys =>
           new PushRecordAndroidGCM({
             // Straight from autopush.
             channelID: data.channelID,
             pushEndpoint: data.endpoint,
             // Common to all PushRecord implementations.
             scope: record.scope,
             originAttributes: record.originAttributes,
             ctime: ctime,
             // Cryptography!
             p256dhPublicKey: exportedKeys[0],
             p256dhPrivateKey: exportedKeys[1],
             authenticationSecret: PushCrypto.generateAuthenticationSecret(),
+            appServerKey: record.appServerKey,
           })
       );
     });
   },
 
   unregister: function(record) {
     console.debug("unregister: ", record);
     return Messaging.sendRequestForResult({
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.push;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import org.mozilla.gecko.push.RegisterUserAgentResponse;
 import org.mozilla.gecko.push.SubscribeChannelResponse;
 import org.mozilla.gecko.push.autopush.AutopushClient;
 import org.mozilla.gecko.push.autopush.AutopushClientException;
 import org.mozilla.gecko.sync.Utils;
 
 import java.util.concurrent.Executor;
@@ -90,19 +91,19 @@ public class PushClient {
     }
 
     public void unregisterUserAgent(@NonNull String uaid, @NonNull String secret) throws LocalException, AutopushClientException {
         final Delegate<Void> delegate = new Delegate<>();
         autopushClient.unregisterUserAgent(uaid, secret, delegate);
         delegate.responseOrThrow(); // For side-effects only.
     }
 
-    public SubscribeChannelResponse subscribeChannel(@NonNull String uaid, @NonNull String secret) throws LocalException, AutopushClientException {
+    public SubscribeChannelResponse subscribeChannel(@NonNull String uaid, @NonNull String secret, @Nullable String appServerKey) throws LocalException, AutopushClientException {
         final Delegate<SubscribeChannelResponse> delegate = new Delegate<>();
-        autopushClient.subscribeChannel(uaid, secret, delegate);
+        autopushClient.subscribeChannel(uaid, secret, appServerKey, delegate);
         return delegate.responseOrThrow();
     }
 
     public void unsubscribeChannel(@NonNull String uaid, @NonNull String secret, @NonNull String chid) throws LocalException, AutopushClientException {
         final Delegate<Void> delegate = new Delegate<>();
         autopushClient.unsubscribeChannel(uaid, secret, chid, delegate);
         delegate.responseOrThrow(); // For side-effects only.
     }
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.push;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.util.Log;
 
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.gcm.GcmTokenClient;
 import org.mozilla.gecko.push.autopush.AutopushClientException;
 import org.mozilla.gecko.util.ThreadUtils;
 
@@ -96,34 +97,34 @@ public class PushManager {
             Log.e(LOG_TAG, "Cannot unregisterUserAgent with null registration uaid or secret!");
             return null;
         }
 
         unregisterUserAgentOnBackgroundThread(registration);
         return registration;
     }
 
-    public PushSubscription subscribeChannel(final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException {
+    public PushSubscription subscribeChannel(final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, @Nullable String appServerKey, final long now) throws ProfileNeedsConfigurationException, AutopushClientException, PushClient.LocalException, GcmTokenClient.NeedsGooglePlayServicesException, IOException {
         Log.i(LOG_TAG, "Subscribing to channel for service: " + service + "; for profile named: " + profileName);
         final PushRegistration registration = advanceRegistration(profileName, now);
-        final PushSubscription subscription = subscribeChannel(registration, profileName, service, serviceData, System.currentTimeMillis());
+        final PushSubscription subscription = subscribeChannel(registration, profileName, service, serviceData, appServerKey, System.currentTimeMillis());
         return subscription;
     }
 
-    protected PushSubscription subscribeChannel(final @NonNull PushRegistration registration, final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, final long now) throws AutopushClientException, PushClient.LocalException {
+    protected PushSubscription subscribeChannel(final @NonNull PushRegistration registration, final @NonNull String profileName, final @NonNull String service, final @NonNull JSONObject serviceData, @Nullable String appServerKey, final long now) throws AutopushClientException, PushClient.LocalException {
         final String uaid = registration.uaid.value;
         final String secret = registration.secret;
         if (uaid == null || secret == null) {
             throw new IllegalStateException("Cannot subscribeChannel with null uaid or secret!");
         }
 
         // Verify endpoint is not null?
         final PushClient pushClient = pushClientFactory.getPushClient(registration.autopushEndpoint, registration.debug);
 
-        final SubscribeChannelResponse result = pushClient.subscribeChannel(uaid, secret);
+        final SubscribeChannelResponse result = pushClient.subscribeChannel(uaid, secret, appServerKey);
         if (registration.debug) {
             Log.i(LOG_TAG, "Got chid: " + result.channelID + " and endpoint: " + result.endpoint);
         } else {
             Log.i(LOG_TAG, "Got chid and endpoint.");
         }
 
         final PushSubscription subscription = new PushSubscription(result.channelID, profileName, result.endpoint, service, serviceData);
         registration.putSubscription(result.channelID, subscription);
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -317,29 +317,30 @@ public class PushService implements Bund
                 // subscription based; there's no concept of unregistering all subscriptions
                 // simultaneously.
                 callback.sendError("Not yet implemented!");
                 return;
             }
             if ("PushServiceAndroidGCM:SubscribeChannel".equals(event)) {
                 final String service = SERVICE_WEBPUSH;
                 final JSONObject serviceData;
+                final String appServerKey = message.getString("appServerKey");
                 try {
                     serviceData = new JSONObject();
                     serviceData.put("profileName", geckoProfile.getName());
                     serviceData.put("profilePath", geckoProfile.getDir().getAbsolutePath());
                 } catch (JSONException e) {
                     Log.e(LOG_TAG, "Got exception in " + event, e);
                     callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
                     return;
                 }
 
                 final PushSubscription subscription;
                 try {
-                    subscription = pushManager.subscribeChannel(geckoProfile.getName(), service, serviceData, System.currentTimeMillis());
+                    subscription = pushManager.subscribeChannel(geckoProfile.getName(), service, serviceData, appServerKey, System.currentTimeMillis());
                 } catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) {
                     Log.e(LOG_TAG, "Got exception in " + event, e);
                     callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
                     return;
                 }
 
                 final JSONObject json = new JSONObject();
                 try {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
@@ -335,17 +335,17 @@ public class AutopushClient {
         final ExtendedJSONObject body = new ExtendedJSONObject();
         body.put("type", TYPE);
         body.put("token", token);
 
         resource.put(body);
     }
 
 
-    public void subscribeChannel(final String uaid, final String secret, RequestDelegate<SubscribeChannelResponse> delegate) {
+    public void subscribeChannel(final String uaid, final String secret, final String appServerKey, RequestDelegate<SubscribeChannelResponse> delegate) {
         final BaseResource resource;
         try {
             resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription"));
         } catch (Exception e) {
             invokeHandleError(delegate, e);
             return;
         }
 
@@ -361,16 +361,17 @@ public class AutopushClient {
                 } catch (Exception e) {
                     delegate.handleError(e);
                     return;
                 }
             }
         };
 
         final ExtendedJSONObject body = new ExtendedJSONObject();
+        body.put("key", appServerKey);
         resource.post(body);
     }
 
     public void unsubscribeChannel(final String uaid, final String secret, final String channelID, RequestDelegate<Void> delegate) {
         final BaseResource resource;
         try {
             resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription/" + channelID));
         } catch (Exception e) {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java
@@ -14,16 +14,17 @@ import org.mozilla.gecko.gcm.GcmTokenCli
 import org.robolectric.RuntimeEnvironment;
 
 import java.util.UUID;
 
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Matchers.isNull;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 
 @RunWith(TestRunner.class)
 public class TestPushManager {
@@ -41,17 +42,17 @@ public class TestPushManager {
         // Configure a mock PushClient.
         pushClient = mock(PushClient.class);
         doReturn(new RegisterUserAgentResponse("opaque-uaid", "opaque-secret"))
                 .when(pushClient)
                 .registerUserAgent(anyString());
 
         doReturn(new SubscribeChannelResponse("opaque-chid", "https://localhost:8085/opaque-push-endpoint"))
                 .when(pushClient)
-                .subscribeChannel(anyString(), anyString());
+                .subscribeChannel(anyString(), anyString(), isNull(String.class));
 
         PushManager.PushClientFactory pushClientFactory = mock(PushManager.PushClientFactory.class);
         doReturn(pushClient).when(pushClientFactory).getPushClient(anyString(), anyBoolean());
 
         manager = new PushManager(state, gcmTokenClient, pushClientFactory);
     }
 
     private void assertOnlyConfigured(PushRegistration registration, String endpoint, boolean debug) {
@@ -135,42 +136,42 @@ public class TestPushManager {
     public void testSubscribeChannel() throws Exception {
         manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis());
         PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis());
         assertRegistered(registration, "http://localhost:8080", false);
 
         // We should be able to register with non-null serviceData.
         final JSONObject webpushData = new JSONObject();
         webpushData.put("version", 5);
-        PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, System.currentTimeMillis());
+        PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, null, System.currentTimeMillis());
         assertSubscribed(subscription);
 
         subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid);
         Assert.assertNotNull(subscription);
         Assert.assertEquals(5, subscription.serviceData.get("version"));
 
         // We should be able to register with null serviceData.
-        subscription = manager.subscribeChannel("default", "sync", null, System.currentTimeMillis());
+        subscription = manager.subscribeChannel("default", "sync", null, null, System.currentTimeMillis());
         assertSubscribed(subscription);
 
         subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid);
         Assert.assertNotNull(subscription);
         Assert.assertNull(subscription.serviceData);
     }
 
     @Test
     public void testUnsubscribeChannel() throws Exception {
         manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis());
         PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis());
         assertRegistered(registration, "http://localhost:8080", false);
 
         // We should be able to register with non-null serviceData.
         final JSONObject webpushData = new JSONObject();
         webpushData.put("version", 5);
-        PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, System.currentTimeMillis());
+        PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, null, System.currentTimeMillis());
         assertSubscribed(subscription);
 
         // No exception is success.
         manager.unsubscribeChannel(subscription.chid);
     }
 
     public void testUnsubscribeUnknownChannel() throws Exception {
         manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis());
@@ -219,17 +220,17 @@ public class TestPushManager {
     @Test
     public void testStartupAfterSubscription() throws Exception {
         PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis());
         assertOnlyConfigured(registration, "http://localhost:8080", true);
 
         registration = manager.registerUserAgent("default", System.currentTimeMillis());
         assertRegistered(registration, "http://localhost:8080", true);
 
-        PushSubscription subscription = manager.subscribeChannel("default", "webpush", null, System.currentTimeMillis());
+        PushSubscription subscription = manager.subscribeChannel("default", "webpush", null, null, System.currentTimeMillis());
         assertSubscribed(subscription);
 
         manager.startup(System.currentTimeMillis());
 
         // Rather tautological.
         registration = manager.registrationForSubscription(subscription.chid);
         PushSubscription updatedSubscription = registration.getSubscription(subscription.chid);
         Assert.assertEquals(subscription.chid, updatedSubscription.chid);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java
@@ -1,30 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.push.autopush.test;
 
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.push.RegisterUserAgentResponse;
 import org.mozilla.gecko.push.SubscribeChannelResponse;
 import org.mozilla.gecko.push.autopush.AutopushClient;
 import org.mozilla.gecko.push.autopush.AutopushClient.RequestDelegate;
 import org.mozilla.gecko.push.autopush.AutopushClientException;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.net.BaseResource;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.startsWith;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 /**
@@ -103,30 +109,54 @@ public class TestLiveAutopushClient {
 
         final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class);
         Assert.assertNotNull(registerResponse);
         Assert.assertNotNull(registerResponse.uaid);
         Assert.assertNotNull(registerResponse.secret);
 
         // We should be able to subscribe to a channel.
         final RequestDelegate<SubscribeChannelResponse> subscribeDelegate = mock(RequestDelegate.class);
-        client.subscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeDelegate);
+        client.subscribeChannel(registerResponse.uaid, registerResponse.secret, null, subscribeDelegate);
 
         final SubscribeChannelResponse subscribeResponse = assertSuccess(subscribeDelegate, SubscribeChannelResponse.class);
         Assert.assertNotNull(subscribeResponse);
         Assert.assertNotNull(subscribeResponse.channelID);
         Assert.assertNotNull(subscribeResponse.endpoint);
         Assert.assertThat(subscribeResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL)));
+        Assert.assertThat(subscribeResponse.endpoint, containsString("/v1/"));
 
         // And we should be able to unsubscribe.
         final RequestDelegate<Void> unsubscribeDelegate = mock(RequestDelegate.class);
         client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, unsubscribeDelegate);
 
         Assert.assertNull(assertSuccess(unsubscribeDelegate, Void.class));
 
+        // We should be able to create a restricted subscription by specifying
+        // an ECDSA public key using the P-256 curve.
+        final RequestDelegate<SubscribeChannelResponse> subscribeWithKeyDelegate = mock(RequestDelegate.class);
+        final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA");
+        keyPairGenerator.initialize(256);
+        final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+        final PublicKey publicKey = keyPair.getPublic();
+        String appServerKey = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
+        client.subscribeChannel(registerResponse.uaid, registerResponse.secret, appServerKey, subscribeWithKeyDelegate);
+
+        final SubscribeChannelResponse subscribeWithKeyResponse = assertSuccess(subscribeWithKeyDelegate, SubscribeChannelResponse.class);
+        Assert.assertNotNull(subscribeWithKeyResponse);
+        Assert.assertNotNull(subscribeWithKeyResponse.channelID);
+        Assert.assertNotNull(subscribeWithKeyResponse.endpoint);
+        Assert.assertThat(subscribeWithKeyResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL)));
+        Assert.assertThat(subscribeWithKeyResponse.endpoint, containsString("/v2/"));
+
+        // And we should be able to drop the restricted subscription.
+        final RequestDelegate<Void> unsubscribeWithKeyDelegate = mock(RequestDelegate.class);
+        client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeWithKeyResponse.channelID, unsubscribeWithKeyDelegate);
+
+        Assert.assertNull(assertSuccess(unsubscribeWithKeyDelegate, Void.class));
+
         // Trying to unsubscribe a second time should give a 410.
         final RequestDelegate<Void> reunsubscribeDelegate = mock(RequestDelegate.class);
         client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, reunsubscribeDelegate);
 
         final AutopushClientException reunsubscribeFailureException = assertFailure(reunsubscribeDelegate, Void.class);
         Assert.assertThat(reunsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class));
         Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) reunsubscribeFailureException).isGone());