--- 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());