Bug 1337348 - Ensure array buffers and Base64-encoded strings can be passed as app server keys. r=baku
MozReview-Commit-ID: HgpSjhCKGgI
--- a/dom/push/Push.js
+++ b/dom/push/Push.js
@@ -93,35 +93,55 @@ Push.prototype = {
console.debug("subscribe()", this._scope);
let histogram = Services.telemetry.getHistogramById("PUSH_API_USED");
histogram.add(true);
return this.askPermission().then(() =>
this.createPromise((resolve, reject) => {
let callback = new PushSubscriptionCallback(this, resolve, reject);
- if (!options || !options.applicationServerKey) {
+ if (!options || options.applicationServerKey === null) {
PushService.subscribe(this._scope, this._principal, callback);
return;
}
- let appServerKey = options.applicationServerKey;
- let keyView = new this._window.Uint8Array(ArrayBuffer.isView(appServerKey) ?
- appServerKey.buffer : appServerKey);
+ let keyView = this._normalizeAppServerKey(options.applicationServerKey);
if (keyView.byteLength === 0) {
callback._rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
return;
}
PushService.subscribeWithKey(this._scope, this._principal,
- appServerKey.length, appServerKey,
+ keyView.byteLength, keyView,
callback);
})
);
},
+ _normalizeAppServerKey: function(appServerKey) {
+ let key;
+ if (typeof appServerKey == "string") {
+ try {
+ key = Cu.cloneInto(ChromeUtils.base64URLDecode(appServerKey, {
+ padding: "reject",
+ }), this._window);
+ } catch (e) {
+ throw new this._window.DOMException(
+ "String contains an invalid character",
+ "InvalidCharacterError"
+ );
+ }
+ } else if (this._window.ArrayBuffer.isView(appServerKey)) {
+ key = appServerKey.buffer;
+ } else {
+ // `appServerKey` is an array buffer.
+ key = appServerKey;
+ }
+ return new this._window.Uint8Array(key);
+ },
+
getSubscription: function() {
console.debug("getSubscription()", this._scope);
return this.createPromise((resolve, reject) => {
let callback = new PushSubscriptionCallback(this, resolve, reject);
PushService.getSubscription(this._scope, this._principal, callback);
});
},
--- a/dom/push/PushManager.cpp
+++ b/dom/push/PushManager.cpp
@@ -1,16 +1,17 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#include "mozilla/dom/PushManager.h"
+#include "mozilla/Base64.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/PushManagerBinding.h"
#include "mozilla/dom/PushSubscription.h"
#include "mozilla/dom/PushSubscriptionOptionsBinding.h"
#include "mozilla/dom/PushUtil.h"
@@ -575,26 +576,58 @@ PushManager::PerformSubscriptionActionFr
RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
if (!proxy) {
p->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
return p.forget();
}
nsTArray<uint8_t> appServerKey;
if (!aOptions.mApplicationServerKey.IsNull()) {
- const OwningArrayBufferViewOrArrayBuffer& bufferSource =
- aOptions.mApplicationServerKey.Value();
- if (!PushUtil::CopyBufferSourceToArray(bufferSource, appServerKey) ||
- appServerKey.IsEmpty()) {
- p->MaybeReject(NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
+ nsresult rv = NormalizeAppServerKey(aOptions.mApplicationServerKey.Value(),
+ appServerKey);
+ if (NS_FAILED(rv)) {
+ p->MaybeReject(rv);
return p.forget();
}
}
RefPtr<GetSubscriptionRunnable> r =
new GetSubscriptionRunnable(proxy, mScope, aAction, Move(appServerKey));
MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
return p.forget();
}
+nsresult
+PushManager::NormalizeAppServerKey(const OwningArrayBufferViewOrArrayBufferOrString& aSource,
+ nsTArray<uint8_t>& aAppServerKey)
+{
+ if (aSource.IsString()) {
+ NS_ConvertUTF16toUTF8 base64Key(aSource.GetAsString());
+ FallibleTArray<uint8_t> decodedKey;
+ nsresult rv = Base64URLDecode(base64Key,
+ Base64URLDecodePaddingPolicy::Reject,
+ decodedKey);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_DOM_INVALID_CHARACTER_ERR;
+ }
+ aAppServerKey = decodedKey;
+ } else if (aSource.IsArrayBuffer()) {
+ if (!PushUtil::CopyArrayBufferToArray(aSource.GetAsArrayBuffer(),
+ aAppServerKey)) {
+ return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
+ }
+ } else if (aSource.IsArrayBufferView()) {
+ if (!PushUtil::CopyArrayBufferViewToArray(aSource.GetAsArrayBufferView(),
+ aAppServerKey)) {
+ return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
+ }
+ } else {
+ MOZ_CRASH("Uninitialized union: expected string, buffer, or view");
+ }
+ if (aAppServerKey.IsEmpty()) {
+ return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
+ }
+ return NS_OK;
+}
+
} // namespace dom
} // namespace mozilla
--- a/dom/push/PushManager.h
+++ b/dom/push/PushManager.h
@@ -42,16 +42,17 @@ class nsIPrincipal;
namespace mozilla {
namespace dom {
namespace workers {
class WorkerPrivate;
}
+class OwningArrayBufferViewOrArrayBufferOrString;
class Promise;
class PushManagerImpl;
struct PushSubscriptionOptionsInit;
class PushManager final : public nsISupports
, public nsWrapperCache
{
public:
@@ -99,16 +100,20 @@ public:
already_AddRefed<Promise>
PermissionState(const PushSubscriptionOptionsInit& aOptions,
ErrorResult& aRv);
private:
~PushManager();
+ nsresult
+ NormalizeAppServerKey(const OwningArrayBufferViewOrArrayBufferOrString& aSource,
+ nsTArray<uint8_t>& aAppServerKey);
+
// The following are only set and accessed on the main thread.
nsCOMPtr<nsIGlobalObject> mGlobal;
RefPtr<PushManagerImpl> mImpl;
// Only used on the worker thread.
nsString mScope;
};
} // namespace dom
--- a/dom/push/test/test_data.html
+++ b/dom/push/test/test_data.html
@@ -55,44 +55,16 @@ http://creativecommons.org/licenses/publ
controlledFrame = yield injectControlledFrame();
});
var pushSubscription;
add_task(function* subscribe() {
pushSubscription = yield registration.pushManager.subscribe();
});
- function base64UrlDecode(s) {
- s = s.replace(/-/g, '+').replace(/_/g, '/');
-
- // Replace padding if it was stripped by the sender.
- // See http://tools.ietf.org/html/rfc4648#section-4
- switch (s.length % 4) {
- case 0:
- break; // No pad chars in this case
- case 2:
- s += '==';
- break; // Two pad chars
- case 3:
- s += '=';
- break; // One pad char
- default:
- throw new Error('Illegal base64url string!');
- }
-
- // With correct padding restored, apply the standard base64 decoder
- var decoded = atob(s);
-
- var array = new Uint8Array(new ArrayBuffer(decoded.length));
- for (var i = 0; i < decoded.length; i++) {
- array[i] = decoded.charCodeAt(i);
- }
- return array;
- }
-
add_task(function* compareJSONSubscription() {
var json = pushSubscription.toJSON();
is(json.endpoint, pushSubscription.endpoint, "Wrong endpoint");
["p256dh", "auth"].forEach(keyName => {
isDeeply(
base64UrlDecode(json.keys[keyName]),
new Uint8Array(pushSubscription.getKey(keyName)),
--- a/dom/push/test/test_register_key.html
+++ b/dom/push/test/test_register_key.html
@@ -190,18 +190,117 @@ http://creativecommons.org/licenses/publ
"Wrong exception type in worker for mismatched key");
is(errorInfo.name, "InvalidStateError",
"Wrong exception name in worker for mismatched key");
} finally {
isTestingMismatchedKey = false;
}
});
+ add_task(function* validKeyBuffer() {
+ var key = yield generateKey();
+ var pushSubscription = yield registration.pushManager.subscribe({
+ applicationServerKey: key.buffer,
+ });
+ is(pushSubscription.endpoint, "https://example.com/push/3",
+ "Wrong endpoint for subscription created with key buffer");
+ var subscriptionKey = pushSubscription.options.applicationServerKey;
+ isDeeply(new Uint8Array(subscriptionKey), key,
+ "App server key getter should match given key");
+ });
+
+ add_task(function* validKeyBufferInWorker() {
+ var key = yield generateKey();
+ var data = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: key.buffer,
+ });
+ is(data.endpoint, "https://example.com/push/4",
+ "Wrong endpoint for subscription with key buffer created in worker");
+ isDeeply(new Uint8Array(data.key), key,
+ "App server key getter should match given key for subscription created in worker");
+ });
+
+ add_task(function* validKeyString() {
+ var base64Key = "BOp8kf30nj6mKFFSPw_w3JAMS99Bac8zneMJ6B6lmKixUO5XTf4AtdPgYUgWke-XE25JHdcooyLgJML1R57jhKY";
+ var key = base64UrlDecode(base64Key);
+ var pushSubscription = yield registration.pushManager.subscribe({
+ applicationServerKey: base64Key,
+ });
+ is(pushSubscription.endpoint, "https://example.com/push/5",
+ "Wrong endpoint for subscription created with Base64-encoded key");
+ isDeeply(new Uint8Array(pushSubscription.options.applicationServerKey), key,
+ "App server key getter should match Base64-decoded key");
+ });
+
+ add_task(function* validKeyStringInWorker() {
+ var base64Key = "BOp8kf30nj6mKFFSPw_w3JAMS99Bac8zneMJ6B6lmKixUO5XTf4AtdPgYUgWke-XE25JHdcooyLgJML1R57jhKY";
+ var key = base64UrlDecode(base64Key);
+ var data = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: base64Key,
+ });
+ is(data.endpoint, "https://example.com/push/6",
+ "Wrong endpoint for subscription created with Base64-encoded key in worker");
+ isDeeply(new Uint8Array(data.key), key,
+ "App server key getter should match decoded key for subscription created in worker");
+ });
+
+ add_task(function* invalidKeyString() {
+ try {
+ yield registration.pushManager.subscribe({
+ applicationServerKey: "!@#$^&*",
+ });
+ ok(false, "Should reject for invalid Base64-encoded keys");
+ } catch (error) {
+ ok(error instanceof DOMException,
+ "Wrong exception type for invalid Base64-encoded key");
+ is(error.name, "InvalidCharacterError",
+ "Wrong exception name for invalid Base64-encoded key");
+ }
+ });
+
+ add_task(function* invalidKeyStringInWorker() {
+ var errorInfo = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: "!@#$^&*",
+ });
+ ok(errorInfo.isDOMException,
+ "Wrong exception type in worker for invalid Base64-encoded key");
+ is(errorInfo.name, "InvalidCharacterError",
+ "Wrong exception name in worker for invalid Base64-encoded key");
+ });
+
+ add_task(function* emptyKeyString() {
+ try {
+ yield registration.pushManager.subscribe({
+ applicationServerKey: "",
+ });
+ ok(false, "Should reject for empty key strings");
+ } catch (error) {
+ ok(error instanceof DOMException,
+ "Wrong exception type for empty key string");
+ is(error.name, "InvalidAccessError",
+ "Wrong exception name for empty key string");
+ }
+ });
+
+ add_task(function* emptyKeyStringInWorker() {
+ var errorInfo = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: "",
+ });
+ ok(errorInfo.isDOMException,
+ "Wrong exception type in worker for empty key string");
+ is(errorInfo.name, "InvalidAccessError",
+ "Wrong exception name in worker for empty key string");
+ });
+
add_task(function* unsubscribe() {
- is(subscriptions, 2, "Wrong subscription count");
+ is(subscriptions, 6, "Wrong subscription count");
controlledFrame.remove();
});
add_task(function* unregister() {
yield registration.unregister();
});
</script>
--- a/dom/push/test/test_utils.js
+++ b/dom/push/test/test_utils.js
@@ -238,8 +238,36 @@ function waitForActive(swr) {
sw.addEventListener('statechange', function onStateChange(evt) {
if (sw.state === 'activated') {
sw.removeEventListener('statechange', onStateChange);
resolve(swr);
}
});
});
}
+
+function base64UrlDecode(s) {
+ s = s.replace(/-/g, '+').replace(/_/g, '/');
+
+ // Replace padding if it was stripped by the sender.
+ // See http://tools.ietf.org/html/rfc4648#section-4
+ switch (s.length % 4) {
+ case 0:
+ break; // No pad chars in this case
+ case 2:
+ s += '==';
+ break; // Two pad chars
+ case 3:
+ s += '=';
+ break; // One pad char
+ default:
+ throw new Error('Illegal base64url string!');
+ }
+
+ // With correct padding restored, apply the standard base64 decoder
+ var decoded = atob(s);
+
+ var array = new Uint8Array(new ArrayBuffer(decoded.length));
+ for (var i = 0; i < decoded.length; i++) {
+ array[i] = decoded.charCodeAt(i);
+ }
+ return array;
+}
--- a/dom/webidl/PushManager.webidl
+++ b/dom/webidl/PushManager.webidl
@@ -4,17 +4,17 @@
* You can obtain one at http://mozilla.org/MPL/2.0/.
*
* The origin of this IDL file is
* https://w3c.github.io/push-api/
*/
dictionary PushSubscriptionOptionsInit {
// boolean userVisibleOnly = false;
- BufferSource? applicationServerKey = null;
+ (BufferSource or DOMString)? applicationServerKey = null;
};
// The main thread JS implementation. Please see comments in
// dom/push/PushManager.h for the split between PushManagerImpl and PushManager.
[JSImplementation="@mozilla.org/push/PushManager;1",
ChromeOnly, Constructor(DOMString scope)]
interface PushManagerImpl {
Promise<PushSubscription> subscribe(optional PushSubscriptionOptionsInit options);