Bug 1337348 - Ensure array buffers and Base64-encoded strings can be passed as app server keys. r=baku
authorKit Cambridge <kit@yakshaving.ninja>
Tue, 07 Feb 2017 13:56:01 -0800
changeset 341378 d0174f989cfd222d838f80d1572c1f40866e8f42
parent 341377 09cbce8b9b211068380dd426ae743ec6735d53ae
child 341379 aa6dfc22160fdd7ee33362b374d0bfd2b7bb8bb9
push id31331
push userkwierso@gmail.com
push dateWed, 08 Feb 2017 23:48:12 +0000
treeherdermozilla-central@438b18daff7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1337348
milestone54.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 1337348 - Ensure array buffers and Base64-encoded strings can be passed as app server keys. r=baku MozReview-Commit-ID: HgpSjhCKGgI
dom/push/Push.js
dom/push/PushManager.cpp
dom/push/PushManager.h
dom/push/test/test_data.html
dom/push/test/test_register_key.html
dom/push/test/test_utils.js
dom/webidl/PushManager.webidl
--- 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);