Bug 1337348 - Ensure array buffers and Base64-encoded strings can be passed as app server keys. r=baku draft
authorKit Cambridge <kit@yakshaving.ninja>
Tue, 07 Feb 2017 13:56:01 -0800
changeset 480623 b93c6f6833fee7d1f7075dc006d0062c2df9190f
parent 479958 e677ba018b22558fef1d07b74d416fd3a28a5dc3
child 545010 8482a2a9295a907a24c0b4c3f89156476622a0f8
push id44603
push userbmo:kit@mozilla.com
push dateWed, 08 Feb 2017 17:40:17 +0000
reviewersbaku
bugs1337348
milestone54.0a1
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);