Bug 1256488 - Add a Base64 URL-decoder for C++ and chrome JS callers. r=mt,baku
authorKit Cambridge <kcambridge@mozilla.com>
Tue, 22 Mar 2016 12:09:04 -0700
changeset 332250 296093225409c25a837753a753780d14ae20f965
parent 332249 eb272cc767ac5a4f2bdcb6d8c1cce7f13db4d65d
child 332251 f0131a8cfd627a4beeece0c890f20c0c8123788c
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt, baku
bugs1256488
milestone48.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 1256488 - Add a Base64 URL-decoder for C++ and chrome JS callers. r=mt,baku MozReview-Commit-ID: IrDwImYfHRu
dom/apps/Webapps.jsm
dom/base/ChromeUtils.cpp
dom/base/ChromeUtils.h
dom/base/test/unit/test_chromeutils_base64.js
dom/base/test/unit/xpcshell.ini
dom/push/PushCrypto.jsm
dom/push/PushServiceAndroidGCM.jsm
dom/push/PushServiceWebSocket.jsm
dom/push/PushSubscription.cpp
dom/push/PushSubscription.h
dom/push/test/xpcshell/test_crypto.js
dom/push/test/xpcshell/test_notification_data.js
dom/push/test/xpcshell/test_notification_http2.js
dom/webidl/PushSubscription.webidl
dom/webidl/ThreadSafeChromeUtils.webidl
services/mobileid/MobileIdentityManager.jsm
toolkit/identity/IdentityCryptoService.cpp
toolkit/identity/nsIIdentityCryptoService.idl
xpcom/io/Base64.cpp
xpcom/io/Base64.h
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -4422,20 +4422,22 @@ this.DOMApplicationRegistry = {
         jwtData = receiptParts[0];
       }
 
       let segments = jwtData.split('.');
       if (segments.length != 3) {
         return "INVALID_SEGMENTS_NUMBER";
       }
 
-      // We need to translate the base64 alphabet used in JWT to our base64 alphabet
-      // before calling atob.
-      let decodedReceipt = JSON.parse(atob(segments[1].replace(/-/g, '+')
-                                                      .replace(/_/g, '/')));
+      let jwtBuffer = ChromeUtils.base64URLDecode(segments[1], {
+        // JWT/JWS prohibits padding per RFC 7515, section 2.
+        padding: "reject",
+      });
+      let textDecoder = new TextDecoder("utf-8");
+      let decodedReceipt = JSON.parse(textDecoder.decode(jwtBuffer));
       if (!decodedReceipt) {
         return "INVALID_RECEIPT_ENCODING";
       }
 
       // Required values for a receipt
       if (!decodedReceipt.typ) {
         return "RECEIPT_TYPE_REQUIRED";
       }
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -1,15 +1,16 @@
 /* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
 /* 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 "ChromeUtils.h"
 
+#include "mozilla/Base64.h"
 #include "mozilla/BasePrincipal.h"
 
 namespace mozilla {
 namespace dom {
 
 /* static */ void
 ThreadSafeChromeUtils::NondeterministicGetWeakMapKeys(GlobalObject& aGlobal,
                                                       JS::Handle<JS::Value> aMap,
@@ -46,16 +47,71 @@ ThreadSafeChromeUtils::NondeterministicG
       aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
     } else {
       aRetval.set(objRet ? JS::ObjectValue(*objRet) : JS::UndefinedValue());
     }
   }
 }
 
 /* static */ void
+ThreadSafeChromeUtils::Base64URLEncode(GlobalObject& aGlobal,
+                                       const ArrayBufferViewOrArrayBuffer& aSource,
+                                       const Base64URLEncodeOptions& aOptions,
+                                       nsACString& aResult,
+                                       ErrorResult& aRv)
+{
+  size_t length = 0;
+  uint8_t* data = nullptr;
+  if (aSource.IsArrayBuffer()) {
+    const ArrayBuffer& buffer = aSource.GetAsArrayBuffer();
+    buffer.ComputeLengthAndData();
+    length = buffer.Length();
+    data = buffer.Data();
+  } else if (aSource.IsArrayBufferView()) {
+    const ArrayBufferView& view = aSource.GetAsArrayBufferView();
+    view.ComputeLengthAndData();
+    length = view.Length();
+    data = view.Data();
+  } else {
+    MOZ_CRASH("Uninitialized union: expected buffer or view");
+  }
+
+  nsresult rv = mozilla::Base64URLEncode(length, data, aOptions, aResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aResult.Truncate();
+    aRv.Throw(rv);
+  }
+}
+
+/* static */ void
+ThreadSafeChromeUtils::Base64URLDecode(GlobalObject& aGlobal,
+                                       const nsACString& aString,
+                                       const Base64URLDecodeOptions& aOptions,
+                                       JS::MutableHandle<JSObject*> aRetval,
+                                       ErrorResult& aRv)
+{
+  FallibleTArray<uint8_t> data;
+  nsresult rv = mozilla::Base64URLDecode(aString, aOptions, data);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aRv.Throw(rv);
+    return;
+  }
+
+  JS::Rooted<JSObject*> buffer(aGlobal.Context(),
+                               ArrayBuffer::Create(aGlobal.Context(),
+                                                   data.Length(),
+                                                   data.Elements()));
+  if (NS_WARN_IF(!buffer)) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return;
+  }
+  aRetval.set(buffer);
+}
+
+/* static */ void
 ChromeUtils::OriginAttributesToSuffix(dom::GlobalObject& aGlobal,
                                       const dom::OriginAttributesDictionary& aAttrs,
                                       nsCString& aSuffix)
 
 {
   GenericOriginAttributes attrs(aAttrs);
   attrs.CreateSuffix(aSuffix);
 }
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -15,16 +15,18 @@
 namespace mozilla {
 
 namespace devtools {
 class HeapSnapshot;
 } // namespace devtools
 
 namespace dom {
 
+class ArrayBufferViewOrArrayBuffer;
+
 class ThreadSafeChromeUtils
 {
 public:
   // Implemented in devtools/shared/heapsnapshot/HeapSnapshot.cpp
   static void SaveHeapSnapshot(GlobalObject& global,
                                const HeapSnapshotBoundaries& boundaries,
                                nsAString& filePath,
                                ErrorResult& rv);
@@ -38,16 +40,28 @@ public:
                                              JS::Handle<JS::Value> aMap,
                                              JS::MutableHandle<JS::Value> aRetval,
                                              ErrorResult& aRv);
 
   static void NondeterministicGetWeakSetKeys(GlobalObject& aGlobal,
                                              JS::Handle<JS::Value> aSet,
                                              JS::MutableHandle<JS::Value> aRetval,
                                              ErrorResult& aRv);
+
+  static void Base64URLEncode(GlobalObject& aGlobal,
+                              const ArrayBufferViewOrArrayBuffer& aSource,
+                              const Base64URLEncodeOptions& aOptions,
+                              nsACString& aResult,
+                              ErrorResult& aRv);
+
+  static void Base64URLDecode(GlobalObject& aGlobal,
+                              const nsACString& aString,
+                              const Base64URLDecodeOptions& aOptions,
+                              JS::MutableHandle<JSObject*> aRetval,
+                              ErrorResult& aRv);
 };
 
 class ChromeUtils : public ThreadSafeChromeUtils
 {
 public:
   static void
   OriginAttributesToSuffix(GlobalObject& aGlobal,
                            const dom::OriginAttributesDictionary& aAttrs,
new file mode 100644
--- /dev/null
+++ b/dom/base/test/unit/test_chromeutils_base64.js
@@ -0,0 +1,103 @@
+"use strict";
+
+function run_test() {
+  test_base64URLEncode();
+  test_base64URLDecode();
+}
+
+// Test vectors from RFC 4648, section 10.
+let textTests = {
+  "": "",
+  "f": "Zg",
+  "fo": "Zm8",
+  "foo": "Zm9v",
+  "foob": "Zm9vYg",
+  "fooba": "Zm9vYmE",
+  "foobar": "Zm9vYmFy",
+}
+
+// Examples from RFC 4648, section 9.
+let binaryTests = [{
+  decoded: new Uint8Array([0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e]),
+  encoded: "FPucA9l-",
+}, {
+  decoded: new Uint8Array([0x14, 0xfb, 0x9c, 0x03, 0xd9]),
+  encoded: "FPucA9k",
+}, {
+  decoded: new Uint8Array([0x14, 0xfb, 0x9c, 0x03]),
+  encoded: "FPucAw",
+}];
+
+function padEncodedValue(value) {
+  switch (value.length % 4) {
+    case 0:
+      return value;
+    case 2:
+      return value + "==";
+    case 3:
+      return value + "=";
+    default:
+      throw new TypeError("Invalid encoded value");
+  }
+}
+
+function testEncode(input, encoded) {
+  equal(ChromeUtils.base64URLEncode(input, { pad: false }),
+        encoded, encoded + " without padding");
+  equal(ChromeUtils.base64URLEncode(input, { pad: true }),
+        padEncodedValue(encoded), encoded + " with padding");
+}
+
+function test_base64URLEncode() {
+  throws(_ => ChromeUtils.base64URLEncode(new Uint8Array(0)), /TypeError/,
+         "Should require encoding options");
+  throws(_ => ChromeUtils.base64URLEncode(new Uint8Array(0), {}), /TypeError/,
+         "Encoding should require the padding option");
+
+  for (let {decoded, encoded} of binaryTests) {
+    testEncode(decoded, encoded);
+  }
+
+  let textEncoder = new TextEncoder("utf-8");
+  for (let decoded of Object.keys(textTests)) {
+    let input = textEncoder.encode(decoded);
+    testEncode(input, textTests[decoded]);
+  }
+}
+
+function testDecode(input, decoded) {
+  let buffer = ChromeUtils.base64URLDecode(input, { padding: "reject" });
+  deepEqual(new Uint8Array(buffer), decoded, input + " with padding rejected");
+
+  let paddedValue = padEncodedValue(input);
+  buffer = ChromeUtils.base64URLDecode(paddedValue, { padding: "ignore" });
+  deepEqual(new Uint8Array(buffer), decoded, input + " with padding ignored");
+
+  if (paddedValue.length > input.length) {
+    throws(_ => ChromeUtils.base64URLDecode(paddedValue, { padding: "reject" }),
+           paddedValue + " with padding rejected should throw");
+
+    throws(_ => ChromeUtils.base64URLDecode(input, { padding: "require" }),
+           input + " with padding required should throw");
+
+    buffer = ChromeUtils.base64URLDecode(paddedValue, { padding: "require" });
+    deepEqual(new Uint8Array(buffer), decoded, paddedValue + " with padding required");
+  }
+}
+
+function test_base64URLDecode() {
+  throws(_ => ChromeUtils.base64URLDecode(""), /TypeError/,
+         "Should require decoding options");
+  throws(_ => ChromeUtils.base64URLEncode("", {}), /TypeError/,
+         "Decoding should require the padding option");
+
+  for (let {decoded, encoded} of binaryTests) {
+    testDecode(encoded, decoded);
+  }
+
+  let textEncoder = new TextEncoder("utf-8");
+  for (let decoded of Object.keys(textTests)) {
+    let expectedBuffer = textEncoder.encode(decoded);
+    testDecode(textTests[decoded], expectedBuffer);
+  }
+}
--- a/dom/base/test/unit/xpcshell.ini
+++ b/dom/base/test/unit/xpcshell.ini
@@ -46,8 +46,9 @@ head = head_xml.js
 [test_xhr_document.js]
 [test_xhr_standalone.js]
 [test_xml_parser.js]
 head = head_xml.js
 [test_xml_serializer.js]
 head = head_xml.js
 [test_xmlserializer.js]
 [test_cancelPrefetch.js]
+[test_chromeutils_base64.js]
--- a/dom/push/PushCrypto.jsm
+++ b/dom/push/PushCrypto.jsm
@@ -5,18 +5,17 @@
 
 'use strict';
 
 const Cu = Components.utils;
 
 Cu.importGlobalProperties(['crypto']);
 
 this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
-                         'getCryptoParams',
-                         'base64UrlDecode'];
+                         'getCryptoParams'];
 
 var UTF8 = new TextEncoder('utf-8');
 
 // Legacy encryption scheme (draft-thomson-http-encryption-02).
 var AESGCM128_ENCODING = 'aesgcm128';
 var AESGCM128_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
 
 // New encryption scheme (draft-ietf-httpbis-encryption-encoding-01).
@@ -113,44 +112,16 @@ function chunkArray(array, size) {
     index += size;
   }
   if (index < array.byteLength) {
     result.push(new Uint8Array(array, start + index));
   }
   return result;
 }
 
-this.base64UrlDecode = function(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;
-};
-
 this.concatArray = function(arrays) {
   var size = arrays.reduce((total, a) => total + a.byteLength, 0);
   var index = 0;
   return arrays.reduce((result, a) => {
     result.set(new Uint8Array(a), index);
     index += a.byteLength;
     return result;
   }, new Uint8Array(size));
@@ -221,29 +192,34 @@ this.PushCrypto = {
     }
 
     // The last chunk of data must be less than aRs, if it is not return an
     // error.
     if (aData.byteLength % (aRs + 16) === 0) {
       return Promise.reject(new Error('Data truncated'));
     }
 
-    let senderKey = base64UrlDecode(aSenderPublicKey)
+    let senderKey = ChromeUtils.base64URLDecode(aSenderPublicKey, {
+      // draft-ietf-httpbis-encryption-encoding-01 prohibits padding.
+      padding: "reject",
+    });
+
     return Promise.all([
       crypto.subtle.importKey('raw', senderKey, ECDH_KEY,
                               false, ['deriveBits']),
       crypto.subtle.importKey('jwk', aPrivateKey, ECDH_KEY,
                               false, ['deriveBits'])
     ])
     .then(([appServerKey, subscriptionPrivateKey]) =>
           crypto.subtle.deriveBits({ name: 'ECDH', public: appServerKey },
                                    subscriptionPrivateKey, 256))
     .then(ikm => this._deriveKeyAndNonce(aPadSize,
                                          new Uint8Array(ikm),
-                                         base64UrlDecode(aSalt),
+                                         ChromeUtils.base64URLDecode(aSalt,
+                                                    { padding: "reject" }),
                                          aPublicKey,
                                          senderKey,
                                          aAuthenticationSecret))
     .then(r =>
       // AEAD_AES_128_GCM expands ciphertext to be 16 octets longer.
       Promise.all(chunkArray(aData, aRs + 16).map((slice, index) =>
         this._decodeChunk(aPadSize, slice, index, r[1], r[0]))))
     .then(r => concatArray(r));
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -17,17 +17,16 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/Preferences.jsm"); /*global: Preferences */
 Cu.import("resource://gre/modules/Promise.jsm"); /*global: Promise */
 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global: XPCOMUtils */
 
 const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("Push");
 
 const {
   PushCrypto,
-  base64UrlDecode,
   concatArray,
   getCryptoParams,
 } = Cu.import("resource://gre/modules/PushCrypto.jsm");
 
 this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
 
 XPCOMUtils.defineLazyGetter(this, "console", () => {
   let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
@@ -113,17 +112,20 @@ this.PushServiceAndroidGCM = {
         let headers = {
           encryption_key: data.enckey,
           crypto_key: data.cryptokey,
           encryption: data.enc,
           encoding: data.con,
         };
         cryptoParams = getCryptoParams(headers);
         // Ciphertext is (urlsafe) Base 64 encoded.
-        message = base64UrlDecode(data.message);
+        message = ChromeUtils.base64URLDecode(data.message, {
+          // The Push server may append padding.
+          padding: "ignore",
+        });
       }
 
       console.debug("Delivering message to main PushService:", message, cryptoParams);
       this._mainPushService.receivedPushMessage(
         data.channelID, message, cryptoParams, (record) => {
           // Always update the stored record.
           return record;
         });
--- a/dom/push/PushServiceWebSocket.jsm
+++ b/dom/push/PushServiceWebSocket.jsm
@@ -16,17 +16,16 @@ Cu.import("resource://gre/modules/Promis
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
 const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
 const {
   PushCrypto,
-  base64UrlDecode,
   getCryptoParams,
 } = Cu.import("resource://gre/modules/PushCrypto.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 
 if (AppConstants.MOZ_B2G) {
@@ -931,17 +930,20 @@ this.PushServiceWebSocket = {
         update.version,
         null,
         null,
         record => record
       );
     } else {
       let params = getCryptoParams(update.headers);
       if (params) {
-        let message = base64UrlDecode(update.data);
+        let message = ChromeUtils.base64URLDecode(update.data, {
+          // The Push server may append padding.
+          padding: "ignore",
+        });
         promise = this._mainPushService.receivedPushMessage(
           update.channelID,
           update.version,
           message,
           params,
           record => record
         );
       } else {
--- a/dom/push/PushSubscription.cpp
+++ b/dom/push/PushSubscription.cpp
@@ -326,31 +326,41 @@ PushSubscription::GetKey(JSContext* aCx,
                                  mAuthSecret.Length(),
                                  mAuthSecret.Elements()));
   } else {
     aKey.set(nullptr);
   }
 }
 
 void
-PushSubscription::ToJSON(PushSubscriptionJSON& aJSON)
+PushSubscription::ToJSON(PushSubscriptionJSON& aJSON, ErrorResult& aRv)
 {
   aJSON.mEndpoint.Construct();
   aJSON.mEndpoint.Value() = mEndpoint;
 
+  Base64URLEncodeOptions encodeOptions;
+  encodeOptions.mPad = false;
+
   aJSON.mKeys.mP256dh.Construct();
   nsresult rv = Base64URLEncode(mRawP256dhKey.Length(),
                                 mRawP256dhKey.Elements(),
+                                encodeOptions,
                                 aJSON.mKeys.mP256dh.Value());
-  Unused << NS_WARN_IF(NS_FAILED(rv));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aRv.Throw(rv);
+    return;
+  }
 
   aJSON.mKeys.mAuth.Construct();
   rv = Base64URLEncode(mAuthSecret.Length(), mAuthSecret.Elements(),
-                       aJSON.mKeys.mAuth.Value());
-  Unused << NS_WARN_IF(NS_FAILED(rv));
+                       encodeOptions, aJSON.mKeys.mAuth.Value());
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aRv.Throw(rv);
+    return;
+  }
 }
 
 already_AddRefed<Promise>
 PushSubscription::UnsubscribeFromWorker(ErrorResult& aRv)
 {
   WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
   MOZ_ASSERT(worker);
   worker->AssertIsOnWorkerThread();
--- a/dom/push/PushSubscription.h
+++ b/dom/push/PushSubscription.h
@@ -68,17 +68,17 @@ public:
               const Nullable<ArrayBuffer>& aP256dhKey,
               const Nullable<ArrayBuffer>& aAuthSecret,
               ErrorResult& aRv);
 
   already_AddRefed<Promise>
   Unsubscribe(ErrorResult& aRv);
 
   void
-  ToJSON(PushSubscriptionJSON& aJSON);
+  ToJSON(PushSubscriptionJSON& aJSON, ErrorResult& aRv);
 
 protected:
   ~PushSubscription();
 
 private:
   already_AddRefed<Promise>
   UnsubscribeFromWorker(ErrorResult& aRv);
 
--- a/dom/push/test/xpcshell/test_crypto.js
+++ b/dom/push/test/xpcshell/test_crypto.js
@@ -1,12 +1,11 @@
 'use strict';
 
 const {
-  base64UrlDecode,
   getCryptoParams,
   PushCrypto,
 } = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
 
 function run_test() {
   run_next_test();
 }
 
@@ -133,17 +132,19 @@ add_task(function* test_crypto_decodeMsg
     crv: 'P-256',
     d: '4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg',
     ext: true,
     key_ops: ['deriveBits'],
     kty: 'EC',
     x: 'sd85ZCbEG6dEkGMCmDyGBIt454Qy-Yo-1xhbaT2Jlk4',
     y: 'vr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs',
   };
-  let publicKey = base64UrlDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs');
+  let publicKey = ChromeUtils.base64URLDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs', {
+    padding: "reject",
+  });
 
   let expectedSuccesses = [{
     desc: 'padSize = 2, rs = 24, pad = 0',
     result: 'Some message',
     data: 'Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU',
     senderPublicKey: 'BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo',
     salt: 'zCU18Rw3A5aB_Xi-vfixmA',
     rs: 24,
@@ -172,18 +173,23 @@ add_task(function* test_crypto_decodeMsg
     data: 'oY4e5eDatDVt2fpQylxbPJM-3vrfhDasfPc8Q1PWt4tPfMVbz_sDNL_cvr0DXXkdFzS1lxsJsj550USx4MMl01ihjImXCjrw9R5xFgFrCAqJD3GwXA1vzS4T5yvGVbUp3SndMDdT1OCcEofTn7VC6xZ-zP8rzSQfDCBBxmPU7OISzr8Z4HyzFCGJeBfqiZ7yUfNlKF1x5UaZ4X6iU_TXx5KlQy_toV1dXZ2eEAMHJUcSdArvB6zRpFdEIxdcHcJyo1BIYgAYTDdAIy__IJVCPY_b2CE5W_6ohlYKB7xDyH8giNuWWXAgBozUfScLUVjPC38yJTpAUi6w6pXgXUWffende5FreQpnMFL1L4G-38wsI_-ISIOzdO8QIrXHxmtc1S5xzYu8bMqSgCinvCEwdeGFCmighRjj8t1zRWo0D14rHbQLPR_b1P5SvEeJTtS9Nm3iibM',
     senderPublicKey: 'BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk',
     salt: '5LIDBXbvkBvvb7ZdD-T4PQ',
     rs: 3,
     authSecret: 'g2rWVHUCpUxgcL9Tz7vyeQ',
     padSize: 2,
   }];
   for (let test of expectedSuccesses) {
-    let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
-    let result = yield PushCrypto.decodeMsg(base64UrlDecode(test.data),
+    let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
+      padding: "reject",
+    }) : null;
+    let data = ChromeUtils.base64URLDecode(test.data, {
+      padding: "reject",
+    });
+    let result = yield PushCrypto.decodeMsg(data,
                                             privateKey, publicKey,
                                             test.senderPublicKey, test.salt,
                                             test.rs, authSecret, test.padSize);
     let decoder = new TextDecoder('utf-8');
     equal(decoder.decode(new Uint8Array(result)), test.result, test.desc);
   }
 
   let expectedFailures = [{
@@ -218,17 +224,22 @@ add_task(function* test_crypto_decodeMsg
     authSecret: 'BhbpNTWyO5wVJmVKTV6XaA',
     padSize: 2,
   }, {
     desc: 'Truncated input',
     data: 'AlDjj6NvT5HGyrHbT8M5D6XBFSra6xrWS9B2ROaCIjwSu3RyZ1iyuv0',
     rs: 25,
   }];
   for (let test of expectedFailures) {
-    let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
+    let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
+      padding: "reject",
+    }) : null;
+    let data = ChromeUtils.base64URLDecode(test.data, {
+      padding: "reject",
+    });
     yield rejects(
-      PushCrypto.decodeMsg(base64UrlDecode(test.data), privateKey, publicKey,
+      PushCrypto.decodeMsg(data, privateKey, publicKey,
                            test.senderPublicKey, test.salt, test.rs,
                            authSecret, test.padSize),
       test.desc
     );
   }
 });
--- a/dom/push/test/xpcshell/test_notification_data.js
+++ b/dom/push/test/xpcshell/test_notification_data.js
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 'use strict';
 
 const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
-const {base64UrlDecode} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
 
 let db;
 let userAgentID = 'f5b47f8d-771f-4ea3-b999-91c135f8766d';
 
 function run_test() {
   do_get_profile();
   setPrefs({
     userAgentID: userAgentID,
@@ -22,19 +21,23 @@ function putRecord(channelID, scope, pub
     channelID: channelID,
     pushEndpoint: 'https://example.org/push/' + channelID,
     scope: scope,
     pushCount: 0,
     lastPush: 0,
     originAttributes: '',
     quota: Infinity,
     systemRecord: true,
-    p256dhPublicKey: base64UrlDecode(publicKey),
+    p256dhPublicKey: ChromeUtils.base64URLDecode(publicKey, {
+      padding: "reject",
+    }),
     p256dhPrivateKey: privateKey,
-    authenticationSecret: base64UrlDecode(authSecret),
+    authenticationSecret: ChromeUtils.base64URLDecode(authSecret, {
+      padding: "reject",
+    }),
   });
 }
 
 let ackDone;
 let server;
 add_task(function* test_notification_ack_data_setup() {
   db = PushServiceWebSocket.newPushDB();
   do_register_cleanup(() => {return db.drop().then(_ => db.close());});
--- a/dom/push/test/xpcshell/test_notification_http2.js
+++ b/dom/push/test/xpcshell/test_notification_http2.js
@@ -1,17 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 'use strict';
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 const {PushDB, PushService, PushServiceHttp2} = serviceExports;
-const {base64UrlDecode} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
 
 var prefs;
 var tlsProfile;
 var pushEnabled;
 var pushConnectionEnabled;
 
 var serverPort = -1;
 
@@ -124,27 +123,31 @@ add_task(function* test_pushNotification
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     quota: Infinity,
     systemRecord: true,
   }, {
     subscriptionUri: serverURL + '/pushNotifications/subscription4',
     pushEndpoint: serverURL + '/pushEndpoint4',
     pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint4',
     scope: 'https://example.com/page/4',
-    p256dhPublicKey: base64UrlDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'),
+    p256dhPublicKey: ChromeUtils.base64URLDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU', {
+      padding: "reject",
+    }),
     p256dhPrivateKey: {
       crv: 'P-256',
       d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw',
       ext: true,
       key_ops: ["deriveBits"],
       kty: 'EC',
       x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ',
       y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'
     },
-    authenticationSecret: base64UrlDecode('cwDVC1iwAn8E37mkR3tMSg'),
+    authenticationSecret: ChromeUtils.base64URLDecode('cwDVC1iwAn8E37mkR3tMSg', {
+      padding: "reject",
+    }),
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     quota: Infinity,
     systemRecord: true,
   }];
 
   for (let record of records) {
     yield db.put(record);
--- a/dom/webidl/PushSubscription.webidl
+++ b/dom/webidl/PushSubscription.webidl
@@ -33,10 +33,11 @@ dictionary PushSubscriptionJSON
 interface PushSubscription
 {
     readonly attribute USVString endpoint;
     ArrayBuffer? getKey(PushEncryptionKeyName name);
     [Throws, UseCounter]
     Promise<boolean> unsubscribe();
 
     // Implements the custom serializer specified in Push API, section 9.
+    [Throws]
     PushSubscriptionJSON toJSON();
 };
--- a/dom/webidl/ThreadSafeChromeUtils.webidl
+++ b/dom/webidl/ThreadSafeChromeUtils.webidl
@@ -50,16 +50,38 @@ interface ThreadSafeChromeUtils {
    * garbage collector and the cycle collector.
    *
    * @param aSet weak set or other JavaScript value
    * @returns If aSet is a weak set object, return the keys of the weak
    *          set as an array.  Otherwise, return undefined.
    */
   [Throws, NewObject]
   static any nondeterministicGetWeakSetKeys(any aSet);
+
+  /**
+   * Converts a buffer to a Base64 URL-encoded string per RFC 4648.
+   *
+   * @param source The buffer to encode.
+   * @param options Additional encoding options.
+   * @returns The encoded string.
+   */
+  [Throws]
+  static ByteString base64URLEncode(BufferSource source,
+                                    Base64URLEncodeOptions options);
+
+  /**
+   * Decodes a Base64 URL-encoded string per RFC 4648.
+   *
+   * @param string The string to decode.
+   * @param options Additional decoding options.
+   * @returns The decoded buffer.
+   */
+  [Throws, NewObject]
+  static ArrayBuffer base64URLDecode(ByteString string,
+                                     Base64URLDecodeOptions options);
 };
 
 /**
  * A JS object whose properties specify what portion of the heap graph to
  * write. The recognized properties are:
  *
  * * globals: [ global, ... ]
  *   Dump only nodes that either:
@@ -83,8 +105,36 @@ interface ThreadSafeChromeUtils {
  * set of globals, the root has an edge to each global, and an edge for each
  * incoming JS reference to the selected Zones.
  */
 dictionary HeapSnapshotBoundaries {
   sequence<object> globals;
   object           debugger;
   boolean          runtime;
 };
+
+dictionary Base64URLEncodeOptions {
+  /** Specifies whether the output should be padded with "=" characters. */
+  required boolean pad;
+};
+
+enum Base64URLDecodePadding {
+  /**
+   * Fails decoding if the input is unpadded. RFC 4648, section 3.2 requires
+   * padding, unless the referring specification prohibits it.
+   */
+  "require",
+
+  /** Tolerates padded and unpadded input. */
+  "ignore",
+
+  /**
+   * Fails decoding if the input is padded. This follows the strict base64url
+   * variant used in JWS (RFC 7515, Appendix C) and HTTP Encrypted
+   * Content-Encoding (draft-ietf-httpbis-encryption-encoding-01).
+   */
+  "reject"
+};
+
+dictionary Base64URLDecodeOptions {
+  /** Specifies the padding mode for decoding the input. */
+  required Base64URLDecodePadding padding;
+};
--- a/services/mobileid/MobileIdentityManager.jsm
+++ b/services/mobileid/MobileIdentityManager.jsm
@@ -997,20 +997,22 @@ this.MobileIdentityManager = {
         }
 
         // Get the verified phone number from the assertion.
         let segments = assertion.split(".");
         if (!segments) {
           return Promise.reject(ERROR_INVALID_ASSERTION);
         }
 
-        // We need to translate the base64 alphabet used in JWT to our base64
-        // alphabet before calling atob.
-        let decodedPayload = JSON.parse(atob(segments[1].replace(/-/g, '+')
-                                                        .replace(/_/g, '/')));
+        let payloadBuffer = ChromeUtils.base64URLDecode(segments[1], {
+          // `IdentityCryptoService` pads output.
+          padding: "require",
+        });
+        let textDecoder = new TextDecoder("utf-8");
+        let decodedPayload = JSON.parse(textDecoder.decode(payloadBuffer));
 
         if (!decodedPayload || !decodedPayload.verifiedMSISDN) {
           return Promise.reject(ERROR_INVALID_ASSERTION);
         }
 
         this.ui.verified(decodedPayload.verifiedMSISDN);
 
         this.success(aPromiseId, assertion);
--- a/toolkit/identity/IdentityCryptoService.cpp
+++ b/toolkit/identity/IdentityCryptoService.cpp
@@ -39,38 +39,16 @@ HexEncode(const SECItem * it, nsACString
   result.SetLength(it->len * 2);
   char * p = result.BeginWriting();
   for (unsigned int i = 0; i < it->len; ++i) {
     *p++ = digits[it->data[i] >> 4];
     *p++ = digits[it->data[i] & 0x0f];
   }
 }
 
-nsresult
-Base64UrlEncodeImpl(const nsACString & utf8Input, nsACString & result)
-{
-  nsresult rv = Base64Encode(utf8Input, result);
-
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  nsACString::char_type * out = result.BeginWriting();
-  nsACString::size_type length = result.Length();
-  // base64url encoding is defined in RFC 4648. It replaces the last two
-  // alphabet characters of base64 encoding with '-' and '_' respectively.
-  for (unsigned int i = 0; i < length; ++i) {
-    if (out[i] == '+') {
-      out[i] = '-';
-    } else if (out[i] == '/') {
-      out[i] = '_';
-    }
-  }
-
-  return NS_OK;
-}
-
 #define DSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("DS160"))
 #define RSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("RS256"))
 
 class KeyPair : public nsIIdentityKeyPair, public nsNSSShutDownObject
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIIDENTITYKEYPAIR
@@ -232,17 +210,21 @@ IdentityCryptoService::GenerateKeyPair(
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 IdentityCryptoService::Base64UrlEncode(const nsACString & utf8Input,
                                        nsACString & result)
 {
-  return Base64UrlEncodeImpl(utf8Input, result);
+  dom::Base64URLEncodeOptions options;
+  options.mPad = true;
+  return Base64URLEncode(utf8Input.Length(),
+    reinterpret_cast<const uint8_t*>(utf8Input.BeginReading()), options,
+    result);
 }
 
 KeyPair::KeyPair(SECKEYPrivateKey * privateKey, SECKEYPublicKey * publicKey)
   : mPrivateKey(privateKey)
   , mPublicKey(publicKey)
 {
   MOZ_ASSERT(!NS_IsMainThread());
 }
@@ -526,19 +508,19 @@ SignRunnable::Run()
         mRv = MapSECStatus(PK11_HashBuf(hashAlg, hash,
                     const_cast<uint8_t*>(reinterpret_cast<const uint8_t *>(
                                             mTextToSign.get())),
                                       mTextToSign.Length()));
         if (NS_SUCCEEDED(mRv)) {
           mRv = MapSECStatus(PK11_Sign(mPrivateKey, &sig, &hashItem));
         }
         if (NS_SUCCEEDED(mRv)) {
-          nsDependentCSubstring sigString(
-            reinterpret_cast<const char*>(sig.data), sig.len);
-          mRv = Base64UrlEncodeImpl(sigString, mSignature);
+          dom::Base64URLEncodeOptions encodeOptions;
+          encodeOptions.mPad = true;
+          mRv = Base64URLEncode(sig.len, sig.data, encodeOptions, mSignature);
         }
         SECITEM_FreeItem(&sig, false);
       }
     }
 
     NS_DispatchToMainThread(this);
   } else {
     // Back on Main Thread
--- a/toolkit/identity/nsIIdentityCryptoService.idl
+++ b/toolkit/identity/nsIIdentityCryptoService.idl
@@ -12,17 +12,17 @@ interface nsIIdentitySignCallback;
  *
  * A"hex" prefix means "hex-encoded string representation of a byte sequence"
  * e.g. "ae34bcdf123"
  *
  * A "base64url" prefix means "base-64-URL-encoded string repressentation of a
  * byte sequence.
  * e.g. "eyJhbGciOiJSUzI1NiJ9"
  * http://en.wikipedia.org/wiki/Base64#Variants_summary_table
- * we use the no-padding approach to base64-url-encoding
+ * we use the padded approach to base64-url-encoding
  *
  * Callbacks take an "in nsresult rv" argument that indicates whether the async
  * operation succeeded. On success, rv will be a success code
  * (NS_SUCCEEDED(rv) / Components.isSuccessCode(rv)) and the remaining
  * arguments are as defined in the documentation for the callback. When the
  * operation fails, rv will be a failure code (NS_FAILED(rv) /
  * !Components.isSuccessCode(rv)) and the values of the remaining arguments will
  * be unspecified.
--- a/xpcom/io/Base64.cpp
+++ b/xpcom/io/Base64.cpp
@@ -3,16 +3,17 @@
 /* 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 "Base64.h"
 
 #include "nsIInputStream.h"
 #include "nsString.h"
+#include "nsTArray.h"
 
 #include "plbase64.h"
 
 namespace {
 
 // BEGIN base64 encode code copied and modified from NSPR
 const unsigned char* base =
   (unsigned char*)"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -223,16 +224,46 @@ EncodeInputStream(nsIInputStream* aInput
   }
 
   return NS_OK;
 }
 
 static const char kBase64URLAlphabet[] =
   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
 
+// Maps an encoded character to a value in the Base64 URL alphabet, per
+// RFC 4648, Table 2. Invalid input characters map to UINT8_MAX.
+static const uint8_t kBase64URLDecodeTable[] = {
+  255, 255, 255, 255, 255, 255, 255, 255,
+  255, 255, 255, 255, 255, 255, 255, 255,
+  255, 255, 255, 255, 255, 255, 255, 255,
+  255, 255, 255, 255, 255, 255, 255, 255,
+  255, 255, 255, 255, 255, 255, 255, 255,
+  255, 255, 255, 255, 255,
+  62 /* - */,
+  255, 255,
+  52, 53, 54, 55, 56, 57, 58, 59, 60, 61, /* 0 - 9 */
+  255, 255, 255, 255, 255, 255, 255,
+  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+  16, 17, 18, 19, 20, 21, 22, 23, 24, 25, /* A - Z */
+  255, 255, 255, 255,
+  63 /* _ */,
+  255,
+  26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
+  42, 43, 44, 45, 46, 47, 48, 49, 50, 51, /* a - z */
+  255, 255, 255, 255,
+};
+
+bool
+Base64URLCharToValue(char aChar, uint8_t* aValue) {
+  uint8_t index = static_cast<uint8_t>(aChar);
+  *aValue = kBase64URLDecodeTable[index & 0x7f];
+  return (*aValue != 255) && !(index & ~0x7f);
+}
+
 } // namespace
 
 namespace mozilla {
 
 nsresult
 Base64EncodeInputStream(nsIInputStream* aInputStream,
                         nsACString& aDest,
                         uint32_t aCount,
@@ -354,30 +385,131 @@ Base64Decode(const nsAString& aBinaryDat
   } else {
     aString.Truncate();
   }
 
   return rv;
 }
 
 nsresult
-Base64URLEncode(uint32_t aLength, const uint8_t* aData, nsACString& aString)
+Base64URLDecode(const nsACString& aString,
+                const dom::Base64URLDecodeOptions& aOptions,
+                FallibleTArray<uint8_t>& aOutput)
+{
+  // Don't decode empty strings.
+  if (aString.IsEmpty()) {
+    aOutput.Clear();
+    return NS_OK;
+  }
+
+  // Check for overflow.
+  uint32_t sourceLength = aString.Length();
+  if (sourceLength > UINT32_MAX / 3) {
+    return NS_ERROR_FAILURE;
+  }
+  const char* source = aString.BeginReading();
+
+  // The decoded length may be 1-2 bytes over, depending on the final quantum.
+  uint32_t decodedLength = (sourceLength * 3) / 4;
+
+  // Determine whether to check for and ignore trailing padding.
+  bool maybePadded = false;
+  switch (aOptions.mPadding) {
+    case dom::Base64URLDecodePadding::Require:
+      if (sourceLength % 4) {
+        // Padded input length must be a multiple of 4.
+        return NS_ERROR_INVALID_ARG;
+      }
+      maybePadded = true;
+      break;
+
+    case dom::Base64URLDecodePadding::Ignore:
+      // Check for padding only if the length is a multiple of 4.
+      maybePadded = !(sourceLength % 4);
+      break;
+
+    // If we're expecting unpadded input, no need for additional checks.
+    // `=` isn't in the decode table, so padded strings will fail to decode.
+    default:
+      MOZ_FALLTHROUGH_ASSERT("Invalid decode padding option");
+    case dom::Base64URLDecodePadding::Reject:
+      break;
+  }
+  if (maybePadded && source[sourceLength - 1] == '=') {
+    if (source[sourceLength - 2] == '=') {
+      sourceLength -= 2;
+    } else {
+      sourceLength -= 1;
+    }
+  }
+
+  if (NS_WARN_IF(!aOutput.SetCapacity(decodedLength, mozilla::fallible))) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  aOutput.SetLengthAndRetainStorage(decodedLength);
+  uint8_t* output = aOutput.Elements();
+
+  for (; sourceLength >= 4; sourceLength -= 4) {
+    uint8_t w, x, y, z;
+    if (!Base64URLCharToValue(*source++, &w) ||
+        !Base64URLCharToValue(*source++, &x) ||
+        !Base64URLCharToValue(*source++, &y) ||
+        !Base64URLCharToValue(*source++, &z)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    *output++ = w << 2 | x >> 4;
+    *output++ = x << 4 | y >> 2;
+    *output++ = y << 6 | z;
+  }
+
+  if (sourceLength == 3) {
+    uint8_t w, x, y;
+    if (!Base64URLCharToValue(*source++, &w) ||
+        !Base64URLCharToValue(*source++, &x) ||
+        !Base64URLCharToValue(*source++, &y)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    *output++ = w << 2 | x >> 4;
+    *output++ = x << 4 | y >> 2;
+  } else if (sourceLength == 2) {
+    uint8_t w, x;
+    if (!Base64URLCharToValue(*source++, &w) ||
+        !Base64URLCharToValue(*source++, &x)) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    *output++ = w << 2 | x >> 4;
+  } else if (sourceLength) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  // Set the length to the actual number of decoded bytes.
+  aOutput.TruncateLength(output - aOutput.Elements());
+  return NS_OK;
+}
+
+nsresult
+Base64URLEncode(uint32_t aLength, const uint8_t* aData,
+                const dom::Base64URLEncodeOptions& aOptions,
+                nsACString& aString)
 {
   // Don't encode empty strings.
   if (aLength == 0) {
     aString.Truncate();
     return NS_OK;
   }
 
   // Check for overflow.
-  if ((static_cast<uint64_t>(aLength) * 6 + 7) / 8 > UINT32_MAX) {
+  if (aLength > (UINT32_MAX / 4) * 3) {
     return NS_ERROR_FAILURE;
   }
 
-  if (!aString.SetLength((aLength * 8 + 5) / 6, fallible)) {
+  // Allocate a buffer large enough to hold the encoded string with padding.
+  // Add one byte for null termination.
+  uint32_t encodedLength = ((aLength + 2) / 3) * 4;
+  if (NS_WARN_IF(!aString.SetCapacity(encodedLength + 1, fallible))) {
     aString.Truncate();
     return NS_ERROR_FAILURE;
   }
 
   char* rawBuffer = aString.BeginWriting();
 
   uint32_t index = 0;
   for (; index + 3 <= aLength; index += 3) {
@@ -395,12 +527,28 @@ Base64URLEncode(uint32_t aLength, const 
     *rawBuffer++ = kBase64URLAlphabet[((aData[index] & 0x3) << 4)];
   } else if (remaining == 2) {
     *rawBuffer++ = kBase64URLAlphabet[aData[index] >> 2];
     *rawBuffer++ = kBase64URLAlphabet[((aData[index] & 0x3) << 4) |
                                       (aData[index + 1] >> 4)];
     *rawBuffer++ = kBase64URLAlphabet[((aData[index + 1] & 0xf) << 2)];
   }
 
+  uint32_t length = rawBuffer - aString.BeginWriting();
+  if (aOptions.mPad) {
+    if (length % 4 == 2) {
+      *rawBuffer++ = '=';
+      *rawBuffer++ = '=';
+      length += 2;
+    } else if (length % 4 == 3) {
+      *rawBuffer++ = '=';
+      length += 1;
+    }
+  }
+
+  // Null terminate and truncate to the actual number of characters.
+  *rawBuffer = '\0';
+  aString.SetLength(length);
+
   return NS_OK;
 }
 
 } // namespace mozilla
--- a/xpcom/io/Base64.h
+++ b/xpcom/io/Base64.h
@@ -4,16 +4,18 @@
  * 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/. */
 
 #ifndef mozilla_Base64_h__
 #define mozilla_Base64_h__
 
 #include "nsString.h"
 
+#include "mozilla/dom/ThreadSafeChromeUtilsBinding.h"
+
 class nsIInputStream;
 
 namespace mozilla {
 
 nsresult
 Base64EncodeInputStream(nsIInputStream* aInputStream,
                         nsACString& aDest,
                         uint32_t aCount,
@@ -31,17 +33,27 @@ Base64Encode(const nsAString& aString, n
 
 nsresult
 Base64Decode(const nsACString& aBinaryData, nsACString& aString);
 nsresult
 Base64Decode(const nsAString& aBinaryData, nsAString& aString);
 
 /**
  * Converts |aData| to an unpadded, Base64 URL-encoded string per RFC 4648.
- * Aims to encode the data in constant time. The caller may free |aData| once
- * this function returns.
+ * Aims to encode the data in constant time. The caller retains ownership
+ * of |aData|.
  */
 nsresult
-Base64URLEncode(uint32_t aLength, const uint8_t* aData, nsACString& aString);
+Base64URLEncode(uint32_t aLength, const uint8_t* aData,
+                const dom::Base64URLEncodeOptions& aOptions,
+                nsACString& aString);
+
+/**
+ * Decodes a Base64 URL-encoded |aString| into |aOutput|.
+ */
+nsresult
+Base64URLDecode(const nsACString& aString,
+                const dom::Base64URLDecodeOptions& aOptions,
+                FallibleTArray<uint8_t>& aOutput);
 
 } // namespace mozilla
 
 #endif