Bug 1225968 - Add authentication secret to push API, r=kitcambridge,smaug
authorMartin Thomson <martin.thomson@gmail.com>
Wed, 09 Dec 2015 07:26:42 +1100
changeset 310064 99dd62654b77b3ca88987caad6fbd829c725a6f6
parent 310063 7d5dfdfe0150615d9ad01d2178ca43db0bf3645d
child 310065 065e03c7e41618123c31c49271f61c145544352d
push id5513
push userraliiev@mozilla.com
push dateMon, 25 Jan 2016 13:55:34 +0000
treeherdermozilla-beta@5ee97dd05b5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskitcambridge, smaug
bugs1225968
milestone45.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 1225968 - Add authentication secret to push API, r=kitcambridge,smaug
dom/interfaces/push/nsIPushClient.idl
dom/push/Push.js
dom/push/PushClient.js
dom/push/PushCrypto.jsm
dom/push/PushManager.cpp
dom/push/PushManager.h
dom/push/PushRecord.jsm
dom/push/PushService.jsm
dom/push/PushServiceHttp2.jsm
dom/push/PushServiceWebSocket.jsm
dom/webidl/PushSubscription.webidl
--- a/dom/interfaces/push/nsIPushClient.idl
+++ b/dom/interfaces/push/nsIPushClient.idl
@@ -12,23 +12,25 @@ interface nsIPrincipal;
  *
  * If status is not NS_OK, endpoint should be ignored. When subscribing to
  * a new endpoint, endpoint will be a valid URL on success, when querying for
  * the presence of an existing subscription, this will be an empty string if
  * the calling {scope+principal} does not currently have an associated
  * endpoint.
  */
 
-[scriptable, uuid(dc201064-8e5c-4a26-bd37-d1e33558a903)]
+[scriptable, uuid(d83e398f-9920-4451-b23a-6d5a5ad2fa26)]
 interface nsIPushEndpointCallback : nsISupports
 {
   void onPushEndpoint(in nsresult status,
                       in DOMString endpoint,
                       in uint32_t keyLen,
-                      [array, size_is(keyLen)] in octet key);
+                      [array, size_is(keyLen)] in octet key,
+                      in uint32_t authSecretLen,
+                      [array, size_is(authSecretLen)] in octet authSecret);
 };
 
 /**
  * Satisfies contracts similar to the Push API specification.
  *
  * If status is not NS_OK, there was a problem unsubscribing and success should
  * be ignored.  success is true if unsubscribing was successful and false if
  * there was no subscription.
--- a/dom/push/Push.js
+++ b/dom/push/Push.js
@@ -181,17 +181,18 @@ Push.prototype = {
 function PushEndpointCallback(pushManager, resolve, reject) {
   this.pushManager = pushManager;
   this.resolve = resolve;
   this.reject = reject;
 }
 
 PushEndpointCallback.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushEndpointCallback]),
-  onPushEndpoint: function(ok, endpoint, keyLen, key) {
+  onPushEndpoint: function(ok, endpoint, keyLen, key,
+                           authSecretLen, authSecretIn) {
     let {pushManager} = this;
     if (!Components.isSuccessCode(ok)) {
       this.reject(new pushManager._window.DOMException(
         "Error retrieving push subscription",
         "AbortError"
       ));
       return;
     }
@@ -203,17 +204,25 @@ PushEndpointCallback.prototype = {
 
     let publicKey = null;
     if (keyLen) {
       publicKey = new ArrayBuffer(keyLen);
       let keyView = new Uint8Array(publicKey);
       keyView.set(key);
     }
 
+    let authSecret = null;
+    if (authSecretLen) {
+      authSecret = new ArrayBuffer(authSecretLen);
+      let secretView = new Uint8Array(authSecret);
+      secretView.set(authSecretIn);
+    }
+
     let sub = new pushManager._window.PushSubscription(endpoint,
                                                        pushManager._scope,
-                                                       publicKey);
+                                                       publicKey,
+                                                       authSecret);
     sub.setPrincipal(pushManager._principal);
     this.resolve(sub);
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Push]);
--- a/dom/push/PushClient.js
+++ b/dom/push/PushClient.js
@@ -96,29 +96,36 @@ PushClient.prototype = {
     this._cpmm.sendAsyncMessage("Push:Registration", {
                                 scope: scope,
                                 requestID: requestId,
                               }, null, principal);
   },
 
   _deliverPushEndpoint: function(request, registration) {
     if (!registration) {
-      request.onPushEndpoint(Cr.NS_OK, "", 0, null);
-      return;
-    }
-    if (registration.p256dhKey) {
-      let key = new Uint8Array(registration.p256dhKey);
-      request.onPushEndpoint(Cr.NS_OK,
-                             registration.pushEndpoint,
-                             key.length,
-                             key);
+      request.onPushEndpoint(Cr.NS_OK, "", 0, null, 0, null);
       return;
     }
 
-    request.onPushEndpoint(Cr.NS_OK, registration.pushEndpoint, 0, null);
+    let key;
+    if (registration.p256dhKey) {
+      key = new Uint8Array(registration.p256dhKey);
+    }
+
+    let authSecret;
+    if (registration.authSecret) {
+      authSecret = new Uint8Array(registration.authSecret);
+    }
+
+    request.onPushEndpoint(Cr.NS_OK,
+                           registration.pushEndpoint,
+                           key ? key.length : 0,
+                           key,
+                           authSecret ? authSecret.length : 0,
+                           authSecret);
   },
 
   receiveMessage: function(aMessage) {
     console.debug("receiveMessage()", aMessage);
 
     let json = aMessage.data;
     let request = this.takeRequest(json.requestID);
 
@@ -130,17 +137,17 @@ PushClient.prototype = {
     switch (aMessage.name) {
       case "PushService:Register:OK":
       case "PushService:Registration:OK":
         this._deliverPushEndpoint(request, json.result);
         break;
 
       case "PushService:Register:KO":
       case "PushService:Registration:KO":
-        request.onPushEndpoint(Cr.NS_ERROR_FAILURE, "", 0, null);
+        request.onPushEndpoint(Cr.NS_ERROR_FAILURE, "", 0, null, 0, null);
         break;
 
       case "PushService:Unregister:OK":
         if (typeof json.result !== "boolean") {
           console.error("receiveMessage: Expected boolean for unregister response",
             json.result);
           request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
           return;
--- a/dom/push/PushCrypto.jsm
+++ b/dom/push/PushCrypto.jsm
@@ -8,20 +8,26 @@
 const Cu = Components.utils;
 
 Cu.importGlobalProperties(['crypto']);
 
 this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
                          'getEncryptionKeyParams', 'getEncryptionParams',
                          'base64UrlDecode'];
 
-var ENCRYPT_INFO = new TextEncoder('utf-8').encode('Content-Encoding: aesgcm128');
-var NONCE_INFO = new TextEncoder('utf-8').encode('Content-Encoding: nonce');
+var UTF8 = new TextEncoder('utf-8');
+var ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
+var NONCE_INFO = UTF8.encode('Content-Encoding: nonce');
+var AUTH_INFO = UTF8.encode('Content-Encoding: auth\0'); // note nul-terminus
+var P256DH_INFO = UTF8.encode('P-256\0');
 
 this.getEncryptionKeyParams = function(encryptKeyField) {
+  if (!encryptKeyField) {
+    return null;
+  }
   var params = encryptKeyField.split(',');
   return params.reduce((m, p) => {
     var pmap = p.split(';').reduce(parseHeaderFieldParams, {});
     if (pmap.keyid && pmap.dh) {
       m[pmap.keyid] = pmap.dh;
     }
     return m;
   }, {});
@@ -36,17 +42,17 @@ this.getEncryptionParams = function(encr
 };
 
 var parseHeaderFieldParams = (m, v) => {
   var i = v.indexOf('=');
   if (i >= 0) {
     // A quoted string with internal quotes is invalid for all the possible
     // values of this header field.
     m[v.substring(0, i).trim()] = v.substring(i + 1).trim()
-                                    .replace(/^"(.*)"$/, '$1');
+                                   .replace(/^"(.*)"$/, '$1');
   }
   return m;
 };
 
 function chunkArray(array, size) {
   var start = array.byteOffset || 0;
   array = array.buffer || array;
   var index = 0;
@@ -110,102 +116,148 @@ hmac.prototype.hash = function(input) {
   return this.keyPromise.then(k => crypto.subtle.sign('HMAC', k, input));
 };
 
 function hkdf(salt, ikm) {
   this.prkhPromise = new hmac(salt).hash(ikm)
     .then(prk => new hmac(prk));
 }
 
-hkdf.prototype.generate = function(info, len) {
+hkdf.prototype.extract = function(info, len) {
   var input = concatArray([info, new Uint8Array([1])]);
   return this.prkhPromise
     .then(prkh => prkh.hash(input))
     .then(h => {
       if (h.byteLength < len) {
         throw new Error('Length is too long');
       }
       return h.slice(0, len);
     });
 };
 
-/* generate a 96-bit IV for use in GCM, 48-bits of which are populated */
+/* generate a 96-bit nonce for use in GCM, 48-bits of which are populated */
 function generateNonce(base, index) {
   if (index >= Math.pow(2, 48)) {
-    throw new Error('Error generating IV - index is too large.');
+    throw new Error('Error generating nonce - index is too large.');
   }
   var nonce = base.slice(0, 12);
   nonce = new Uint8Array(nonce);
   for (var i = 0; i < 6; ++i) {
     nonce[nonce.byteLength - 1 - i] ^= (index / Math.pow(256, i)) & 0xff;
   }
   return nonce;
 }
 
 this.PushCrypto = {
 
-  generateKeys: function() {
+  generateAuthenticationSecret() {
+    return crypto.getRandomValues(new Uint8Array(12));
+  },
+
+  generateKeys() {
     return crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256'},
                                      true,
                                      ['deriveBits'])
       .then(cryptoKey =>
          Promise.all([
            crypto.subtle.exportKey('raw', cryptoKey.publicKey),
            // TODO: change this when bug 1048931 lands.
            crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
          ]));
   },
 
-  decodeMsg: function(aData, aPrivateKey, aRemotePublicKey, aSalt, aRs) {
+  decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey,
+            aSalt, aRs, aAuthenticationSecret) {
 
     if (aData.byteLength === 0) {
       // Zero length messages will be passed as null.
       return Promise.resolve(null);
     }
 
     // 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)
     return Promise.all([
-      crypto.subtle.importKey('raw', base64UrlDecode(aRemotePublicKey),
+      crypto.subtle.importKey('raw', senderKey,
                               { name: 'ECDH', namedCurve: 'P-256' },
                               false,
                               ['deriveBits']),
       crypto.subtle.importKey('jwk', aPrivateKey,
                               { name: 'ECDH', namedCurve: 'P-256' },
                               false,
                               ['deriveBits'])
     ])
-    .then(keys =>
-      crypto.subtle.deriveBits({ name: 'ECDH', public: keys[0] }, keys[1], 256))
-    .then(rawKey => {
-      var kdf = new hkdf(base64UrlDecode(aSalt), new Uint8Array(rawKey));
-      return Promise.all([
-        kdf.generate(ENCRYPT_INFO, 16)
-          .then(gcmBits =>
-                crypto.subtle.importKey('raw', gcmBits, 'AES-GCM', false,
-                                        ['decrypt'])),
-        kdf.generate(NONCE_INFO, 12)
-      ])
-    })
+    .then(keys => crypto.subtle.deriveBits({ name: 'ECDH', public: keys[0] }, keys[1], 256))
+    .then(ikm => this._deriveKeyAndNonce(new Uint8Array(ikm),
+                                         base64UrlDecode(aSalt),
+                                         base64UrlDecode(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(slice, index, r[1], r[0]))))
     .then(r => concatArray(r));
   },
 
-  _decodeChunk: function(aSlice, aIndex, aNonce, aKey) {
-    return crypto.subtle.decrypt({name: 'AES-GCM',
-                                  iv: generateNonce(aNonce, aIndex)
-                                 },
-                                 aKey, aSlice)
+  _deriveKeyAndNonce(ikm, salt, receiverKey, senderKey, authenticationSecret) {
+    var kdfPromise;
+    var context;
+    // The authenticationSecret, when present, is mixed with the ikm using HKDF.
+    // This is its primary purpose.  However, since the authentication secret
+    // was added at the same time that the info string was changed, we also use
+    // its presence to change how the final info string is calculated:
+    //
+    // 1. When there is no authenticationSecret, the context string is simply
+    // "Content-Encoding: <blah>". This corresponds to old, deprecated versions
+    // of the content encoding.  This should eventually be removed: bug 1230038.
+    //
+    // 2. When there is an authenticationSecret, the context string is:
+    // "Content-Encoding: <blah>\0P-256\0" then the length and value of both the
+    // receiver key and sender key.
+    if (authenticationSecret) {
+      // Since we are using an authentication secret, we need to run an extra
+      // round of HKDF with the authentication secret as salt.
+      var authKdf = new hkdf(authenticationSecret, ikm);
+      kdfPromise = authKdf.extract(AUTH_INFO, 32)
+        .then(ikm2 => new hkdf(salt, ikm2));
+
+      // We also use the presence of the authentication secret to indicate that
+      // we have extra context to add to the info parameter.
+      context = concatArray([
+        new Uint8Array([0]), P256DH_INFO,
+        this._encodeLength(receiverKey), receiverKey,
+        this._encodeLength(senderKey), senderKey
+      ]);
+    } else {
+      kdfPromise = Promise.resolve(new hkdf(salt, ikm));
+      context = new Uint8Array(0);
+    }
+    return kdfPromise.then(kdf => Promise.all([
+      kdf.extract(concatArray([ENCRYPT_INFO, context]), 16)
+        .then(gcmBits => crypto.subtle.importKey('raw', gcmBits, 'AES-GCM', false,
+                                                 ['decrypt'])),
+      kdf.extract(concatArray([NONCE_INFO, context]), 12)
+    ]));
+  },
+
+  _encodeLength(buffer) {
+    return new Uint8Array([0, buffer.byteLength]);
+  },
+
+  _decodeChunk(aSlice, aIndex, aNonce, aKey) {
+    let params = {
+      name: 'AES-GCM',
+      iv: generateNonce(aNonce, aIndex)
+    };
+    return crypto.subtle.decrypt(params, aKey, aSlice)
       .then(decoded => {
         decoded = new Uint8Array(decoded);
         if (decoded.length == 0) {
           return Promise.reject(new Error('Decoded array is too short!'));
         } else if (decoded[0] > decoded.length) {
           return Promise.reject(new Error ('Padding is wrong!'));
         } else {
           // All padded bytes must be zero except the first one.
--- a/dom/push/PushManager.cpp
+++ b/dom/push/PushManager.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 "mozilla/dom/PushManager.h"
 
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
+#include "mozilla/unused.h"
 #include "mozilla/dom/PushManagerBinding.h"
 #include "mozilla/dom/PushSubscriptionBinding.h"
 #include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
 
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseWorkerProxy.h"
 
 #include "nsIGlobalObject.h"
@@ -119,79 +120,93 @@ PushSubscription::Unsubscribe(ErrorResul
     new UnsubscribeResultCallback(p);
   client->Unsubscribe(mScope, mPrincipal, callback);
   return p.forget();
 }
 
 PushSubscription::PushSubscription(nsIGlobalObject* aGlobal,
                                    const nsAString& aEndpoint,
                                    const nsAString& aScope,
-                                   const nsTArray<uint8_t>& aRawP256dhKey)
+                                   const nsTArray<uint8_t>& aRawP256dhKey,
+                                   const nsTArray<uint8_t>& aAuthSecret)
   : mGlobal(aGlobal)
   , mEndpoint(aEndpoint)
   , mScope(aScope)
   , mRawP256dhKey(aRawP256dhKey)
+  , mAuthSecret(aAuthSecret)
 {
 }
 
 PushSubscription::~PushSubscription()
 {
 }
 
 JSObject*
 PushSubscription::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return PushSubscriptionBinding::Wrap(aCx, this, aGivenProto);
 }
 
 void
 PushSubscription::GetKey(JSContext* aCx,
                          PushEncryptionKeyName aType,
-                         JS::MutableHandle<JSObject*> aP256dhKey)
+                         JS::MutableHandle<JSObject*> aKey)
 {
   if (aType == PushEncryptionKeyName::P256dh && !mRawP256dhKey.IsEmpty()) {
-    aP256dhKey.set(ArrayBuffer::Create(aCx,
-                                       mRawP256dhKey.Length(),
-                                       mRawP256dhKey.Elements()));
+    aKey.set(ArrayBuffer::Create(aCx,
+                                 mRawP256dhKey.Length(),
+                                 mRawP256dhKey.Elements()));
+  } else if (aType == PushEncryptionKeyName::Auth && !mAuthSecret.IsEmpty()) {
+    aKey.set(ArrayBuffer::Create(aCx,
+                                 mAuthSecret.Length(),
+                                 mAuthSecret.Elements()));
   } else {
-    aP256dhKey.set(nullptr);
+    aKey.set(nullptr);
   }
 }
 
 void
 PushSubscription::SetPrincipal(nsIPrincipal* aPrincipal)
 {
   MOZ_ASSERT(!mPrincipal);
   mPrincipal = aPrincipal;
 }
 
 // static
 already_AddRefed<PushSubscription>
 PushSubscription::Constructor(GlobalObject& aGlobal,
                               const nsAString& aEndpoint,
                               const nsAString& aScope,
                               const Nullable<ArrayBuffer>& aP256dhKey,
+                              const Nullable<ArrayBuffer>& aAuthSecret,
                               ErrorResult& aRv)
 {
   MOZ_ASSERT(!aEndpoint.IsEmpty());
   MOZ_ASSERT(!aScope.IsEmpty());
 
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
 
   nsTArray<uint8_t> rawKey;
   if (!aP256dhKey.IsNull()) {
     const ArrayBuffer& key = aP256dhKey.Value();
     key.ComputeLengthAndData();
-    rawKey.SetLength(key.Length());
-    rawKey.ReplaceElementsAt(0, key.Length(), key.Data(), key.Length());
+    rawKey.InsertElementsAt(0, key.Data(), key.Length());
+  }
+
+  nsTArray<uint8_t> authSecret;
+  if (!aAuthSecret.IsNull()) {
+    const ArrayBuffer& sekrit = aAuthSecret.Value();
+    sekrit.ComputeLengthAndData();
+    authSecret.InsertElementsAt(0, sekrit.Data(), sekrit.Length());
   }
   RefPtr<PushSubscription> sub = new PushSubscription(global,
-                                                        aEndpoint,
-                                                        aScope,
-                                                        rawKey);
+                                                      aEndpoint,
+                                                      aScope,
+                                                      rawKey,
+                                                      authSecret);
 
   return sub.forget();
 }
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushSubscription, mGlobal, mPrincipal)
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(PushSubscription)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(PushSubscription)
@@ -254,18 +269,22 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 // WorkerPushSubscription
 
 WorkerPushSubscription::WorkerPushSubscription(const nsAString& aEndpoint,
                                                const nsAString& aScope,
-                                               const nsTArray<uint8_t>& aRawP256dhKey)
-  : mEndpoint(aEndpoint), mScope(aScope), mRawP256dhKey(aRawP256dhKey)
+                                               const nsTArray<uint8_t>& aRawP256dhKey,
+                                               const nsTArray<uint8_t>& aAuthSecret)
+  : mEndpoint(aEndpoint)
+  , mScope(aScope)
+  , mRawP256dhKey(aRawP256dhKey)
+  , mAuthSecret(aAuthSecret)
 {
   MOZ_ASSERT(!aScope.IsEmpty());
   MOZ_ASSERT(!aEndpoint.IsEmpty());
 }
 
 WorkerPushSubscription::~WorkerPushSubscription()
 {}
 
@@ -276,48 +295,64 @@ WorkerPushSubscription::WrapObject(JSCon
 }
 
 // static
 already_AddRefed<WorkerPushSubscription>
 WorkerPushSubscription::Constructor(GlobalObject& aGlobal,
                                     const nsAString& aEndpoint,
                                     const nsAString& aScope,
                                     const Nullable<ArrayBuffer>& aP256dhKey,
+                                    const Nullable<ArrayBuffer>& aAuthSecret,
                                     ErrorResult& aRv)
 {
   WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
   MOZ_ASSERT(worker);
   worker->AssertIsOnWorkerThread();
 
   nsTArray<uint8_t> rawKey;
   if (!aP256dhKey.IsNull()) {
     const ArrayBuffer& key = aP256dhKey.Value();
     key.ComputeLengthAndData();
     rawKey.SetLength(key.Length());
     rawKey.ReplaceElementsAt(0, key.Length(), key.Data(), key.Length());
   }
+
+  nsTArray<uint8_t> authSecret;
+  if (!aAuthSecret.IsNull()) {
+    const ArrayBuffer& sekrit = aAuthSecret.Value();
+    sekrit.ComputeLengthAndData();
+    authSecret.SetLength(sekrit.Length());
+    authSecret.ReplaceElementsAt(0, sekrit.Length(),
+                                 sekrit.Data(), sekrit.Length());
+  }
   RefPtr<WorkerPushSubscription> sub = new WorkerPushSubscription(aEndpoint,
-                                                                    aScope,
-                                                                    rawKey);
+                                                                  aScope,
+                                                                  rawKey,
+                                                                  authSecret);
 
   return sub.forget();
 }
 
 void
 WorkerPushSubscription::GetKey(JSContext* aCx,
                                PushEncryptionKeyName aType,
-                               JS::MutableHandle<JSObject*> aP256dhKey)
+                               JS::MutableHandle<JSObject*> aKey)
 {
   if (aType == mozilla::dom::PushEncryptionKeyName::P256dh &&
       !mRawP256dhKey.IsEmpty()) {
-    aP256dhKey.set(ArrayBuffer::Create(aCx,
-                                       mRawP256dhKey.Length(),
-                                       mRawP256dhKey.Elements()));
+    aKey.set(ArrayBuffer::Create(aCx,
+                                 mRawP256dhKey.Length(),
+                                 mRawP256dhKey.Elements()));
+  } else if (aType == mozilla::dom::PushEncryptionKeyName::Auth &&
+             !mAuthSecret.IsEmpty()) {
+    aKey.set(ArrayBuffer::Create(aCx,
+                                 mAuthSecret.Length(),
+                                 mAuthSecret.Elements()));
   } else {
-    aP256dhKey.set(nullptr);
+    aKey.set(nullptr);
   }
 }
 
 class UnsubscribeResultRunnable final : public WorkerRunnable
 {
 public:
   UnsubscribeResultRunnable(PromiseWorkerProxy* aProxy,
                             nsresult aStatus,
@@ -495,35 +530,38 @@ WorkerPushManager::WrapObject(JSContext*
 
 class GetSubscriptionResultRunnable final : public WorkerRunnable
 {
 public:
   GetSubscriptionResultRunnable(PromiseWorkerProxy* aProxy,
                                 nsresult aStatus,
                                 const nsAString& aEndpoint,
                                 const nsAString& aScope,
-                                const nsTArray<uint8_t>& aRawP256dhKey)
+                                const nsTArray<uint8_t>& aRawP256dhKey,
+                                const nsTArray<uint8_t>& aAuthSecret)
     : WorkerRunnable(aProxy->GetWorkerPrivate(), WorkerThreadModifyBusyCount)
     , mProxy(aProxy)
     , mStatus(aStatus)
     , mEndpoint(aEndpoint)
     , mScope(aScope)
     , mRawP256dhKey(aRawP256dhKey)
+    , mAuthSecret(aAuthSecret)
   { }
 
   bool
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
   {
     RefPtr<Promise> promise = mProxy->WorkerPromise();
     if (NS_SUCCEEDED(mStatus)) {
       if (mEndpoint.IsEmpty()) {
         promise->MaybeResolve(JS::NullHandleValue);
       } else {
         RefPtr<WorkerPushSubscription> sub =
-          new WorkerPushSubscription(mEndpoint, mScope, mRawP256dhKey);
+            new WorkerPushSubscription(mEndpoint, mScope,
+                                       mRawP256dhKey, mAuthSecret);
         promise->MaybeResolve(sub);
       }
     } else {
       promise->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
     }
 
     mProxy->CleanUp(aCx);
     return true;
@@ -532,16 +570,17 @@ private:
   ~GetSubscriptionResultRunnable()
   {}
 
   RefPtr<PromiseWorkerProxy> mProxy;
   nsresult mStatus;
   nsString mEndpoint;
   nsString mScope;
   nsTArray<uint8_t> mRawP256dhKey;
+  nsTArray<uint8_t> mAuthSecret;
 };
 
 class GetSubscriptionCallback final : public nsIPushEndpointCallback
 {
 public:
   NS_DECL_ISUPPORTS
 
   explicit GetSubscriptionCallback(PromiseWorkerProxy* aProxy,
@@ -549,17 +588,19 @@ public:
     : mProxy(aProxy)
     , mScope(aScope)
   {}
 
   NS_IMETHOD
   OnPushEndpoint(nsresult aStatus,
                  const nsAString& aEndpoint,
                  uint32_t aKeyLen,
-                 uint8_t* aKey) override
+                 uint8_t* aKey,
+                 uint32_t aAuthSecretLen,
+                 uint8_t* aAuthSecret) override
   {
     AssertIsOnMainThread();
     MOZ_ASSERT(mProxy, "OnPushEndpoint() called twice?");
 
     RefPtr<PromiseWorkerProxy> proxy = mProxy.forget();
 
     MutexAutoLock lock(proxy->Lock());
     if (proxy->CleanedUp()) {
@@ -567,26 +608,40 @@ public:
     }
 
     AutoJSAPI jsapi;
     jsapi.Init();
 
     nsTArray<uint8_t> rawP256dhKey(aKeyLen);
     rawP256dhKey.ReplaceElementsAt(0, aKeyLen, aKey, aKeyLen);
 
+    nsTArray<uint8_t> authSecret(aAuthSecretLen);
+    authSecret.ReplaceElementsAt(0, aAuthSecretLen,
+                                 aAuthSecret, aAuthSecretLen);
+
     RefPtr<GetSubscriptionResultRunnable> r =
       new GetSubscriptionResultRunnable(proxy,
                                         aStatus,
                                         aEndpoint,
                                         mScope,
-                                        rawP256dhKey);
+                                        rawP256dhKey,
+                                        authSecret);
     r->Dispatch(jsapi.cx());
     return NS_OK;
   }
 
+  // Convenience method for use in this file.
+  void
+  OnPushEndpointError(nsresult aStatus)
+  {
+    Unused << NS_WARN_IF(NS_FAILED(
+        OnPushEndpoint(aStatus, EmptyString(), 0, nullptr, 0, nullptr)));
+  }
+
+
 protected:
   ~GetSubscriptionCallback()
   {}
 
 private:
   RefPtr<PromiseWorkerProxy> mProxy;
   nsString mScope;
 };
@@ -614,45 +669,45 @@ public:
 
     RefPtr<GetSubscriptionCallback> callback = new GetSubscriptionCallback(mProxy, mScope);
 
     nsCOMPtr<nsIPrincipal> principal = mProxy->GetWorkerPrivate()->GetPrincipal();
 
     PushPermissionState state;
     nsresult rv = GetPermissionState(principal, state);
     if (NS_FAILED(rv)) {
-      callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
+      callback->OnPushEndpointError(NS_ERROR_FAILURE);
       return NS_OK;
     }
 
     if (state != PushPermissionState::Granted) {
       if (mAction == WorkerPushManager::GetSubscriptionAction) {
-        callback->OnPushEndpoint(NS_OK, EmptyString(), 0, nullptr);
+        callback->OnPushEndpointError(NS_OK);
         return NS_OK;
       }
-      callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
+      callback->OnPushEndpointError(NS_ERROR_FAILURE);
       return NS_OK;
     }
 
     nsCOMPtr<nsIPushClient> client =
       do_CreateInstance("@mozilla.org/push/PushClient;1");
     if (!client) {
-      callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
+      callback->OnPushEndpointError(NS_ERROR_FAILURE);
       return NS_OK;
     }
 
     if (mAction == WorkerPushManager::SubscribeAction) {
       rv = client->Subscribe(mScope, principal, callback);
     } else {
       MOZ_ASSERT(mAction == WorkerPushManager::GetSubscriptionAction);
       rv = client->GetSubscription(mScope, principal, callback);
     }
 
     if (NS_WARN_IF(NS_FAILED(rv))) {
-      callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
+      callback->OnPushEndpointError(NS_ERROR_FAILURE);
       return NS_OK;
     }
 
     return NS_OK;
   }
 
 private:
   ~GetSubscriptionRunnable()
--- a/dom/push/PushManager.h
+++ b/dom/push/PushManager.h
@@ -62,17 +62,18 @@ class PushSubscription final : public ns
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PushSubscription)
 
   explicit PushSubscription(nsIGlobalObject* aGlobal,
                             const nsAString& aEndpoint,
                             const nsAString& aScope,
-                            const nsTArray<uint8_t>& aP256dhKey);
+                            const nsTArray<uint8_t>& aP256dhKey,
+                            const nsTArray<uint8_t>& aAuthSecret);
 
   JSObject*
   WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   nsIGlobalObject*
   GetParentObject() const
   {
     return mGlobal;
@@ -82,23 +83,24 @@ public:
   GetEndpoint(nsAString& aEndpoint) const
   {
     aEndpoint = mEndpoint;
   }
 
   void
   GetKey(JSContext* cx,
          PushEncryptionKeyName aType,
-         JS::MutableHandle<JSObject*> aP256dhKey);
+         JS::MutableHandle<JSObject*> aKey);
 
   static already_AddRefed<PushSubscription>
   Constructor(GlobalObject& aGlobal,
               const nsAString& aEndpoint,
               const nsAString& aScope,
               const Nullable<ArrayBuffer>& aP256dhKey,
+              const Nullable<ArrayBuffer>& aAuthSecret,
               ErrorResult& aRv);
 
   void
   SetPrincipal(nsIPrincipal* aPrincipal);
 
   already_AddRefed<Promise>
   Unsubscribe(ErrorResult& aRv);
 
@@ -106,16 +108,17 @@ protected:
   ~PushSubscription();
 
 private:
   nsCOMPtr<nsIGlobalObject> mGlobal;
   nsCOMPtr<nsIPrincipal> mPrincipal;
   nsString mEndpoint;
   nsString mScope;
   nsTArray<uint8_t> mRawP256dhKey;
+  nsTArray<uint8_t> mAuthSecret;
 };
 
 class PushManager final : public nsISupports
                         , public nsWrapperCache
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PushManager)
@@ -156,32 +159,34 @@ class WorkerPushSubscription final : pub
                                    , public nsWrapperCache
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WorkerPushSubscription)
 
   explicit WorkerPushSubscription(const nsAString& aEndpoint,
                                   const nsAString& aScope,
-                                  const nsTArray<uint8_t>& aRawP256dhKey);
+                                  const nsTArray<uint8_t>& aRawP256dhKey,
+                                  const nsTArray<uint8_t>& aAuthSecret);
 
   nsIGlobalObject*
   GetParentObject() const
   {
     return nullptr;
   }
 
   JSObject*
   WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   static already_AddRefed<WorkerPushSubscription>
   Constructor(GlobalObject& aGlobal,
               const nsAString& aEndpoint,
               const nsAString& aScope,
               const Nullable<ArrayBuffer>& aP256dhKey,
+              const Nullable<ArrayBuffer>& aAuthSecret,
               ErrorResult& aRv);
 
   void
   GetEndpoint(nsAString& aEndpoint) const
   {
     aEndpoint = mEndpoint;
   }
 
@@ -194,16 +199,17 @@ public:
 
 protected:
   ~WorkerPushSubscription();
 
 private:
   nsString mEndpoint;
   nsString mScope;
   nsTArray<uint8_t> mRawP256dhKey;
+  nsTArray<uint8_t> mAuthSecret;
 };
 
 class WorkerPushManager final : public nsISupports
                               , public nsWrapperCache
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WorkerPushManager)
--- a/dom/push/PushRecord.jsm
+++ b/dom/push/PushRecord.jsm
@@ -31,16 +31,17 @@ const prefs = new Preferences("dom.push.
 function PushRecord(props) {
   this.pushEndpoint = props.pushEndpoint;
   this.scope = props.scope;
   this.originAttributes = props.originAttributes;
   this.pushCount = props.pushCount || 0;
   this.lastPush = props.lastPush || 0;
   this.p256dhPublicKey = props.p256dhPublicKey;
   this.p256dhPrivateKey = props.p256dhPrivateKey;
+  this.authenticationSecret = props.authenticationSecret;
   this.setQuota(props.quota);
   this.ctime = (typeof props.ctime === "number") ? props.ctime : 0;
 }
 
 PushRecord.prototype = {
   setQuota(suggestedQuota) {
     if (!isNaN(suggestedQuota) && suggestedQuota >= 0) {
       this.quota = suggestedQuota;
@@ -215,16 +216,17 @@ PushRecord.prototype = {
   },
 
   toSubscription() {
     return {
       pushEndpoint: this.pushEndpoint,
       lastPush: this.lastPush,
       pushCount: this.pushCount,
       p256dhKey: this.p256dhPublicKey,
+      authenticationSecret: this.authenticationSecret,
     };
   },
 };
 
 // Define lazy getters for the principal and scope URI. IndexedDB can't store
 // `nsIPrincipal` objects, so we keep them in a private weak map.
 var principals = new WeakMap();
 Object.defineProperties(PushRecord.prototype, {
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -782,27 +782,39 @@ this.PushService = {
   },
 
   notifySubscriptionChanges: function(records) {
     records.forEach(record => {
       this._notifySubscriptionChangeObservers(record);
     });
   },
 
-  ensureP256dhKey: function(record) {
-    if (record.p256dhPublicKey && record.p256dhPrivateKey) {
+  ensureCrypto: function(record) {
+    if (record.authenticationSecret &&
+        record.p256dhPublicKey &&
+        record.p256dhPrivateKey) {
       return Promise.resolve(record);
     }
+
+    let keygen = Promise.resolve([]);
+    if (!record.p256dhPublicKey || !record.p256dhPrivateKey) {
+      keygen = PushCrypto.generateKeys();
+    }
     // We do not have a encryption key. so we need to generate it. This
     // is only going to happen on db upgrade from version 4 to higher.
-    return PushCrypto.generateKeys()
-      .then(exportedKeys => {
+    return keygen
+      .then(([pubKey, privKey]) => {
         return this.updateRecordAndNotifyApp(record.keyID, record => {
-          record.p256dhPublicKey = exportedKeys[0];
-          record.p256dhPrivateKey = exportedKeys[1];
+          if (!record.p256dhPublicKey || !record.p256dhPrivateKey) {
+            record.p256dhPublicKey = pubKey;
+            record.p256dhPrivateKey = privKey;
+          }
+          if (!record.authenticationSecret) {
+            record.authenticationSecret = PushCrypto.generateAuthenticationSecret();
+          }
           return record;
         });
       }, error => {
         return this.dropRegistrationAndNotifyApp(record.keyID).then(
           () => Promise.reject(error));
       });
   },
 
@@ -868,32 +880,36 @@ this.PushService = {
       if (!record) {
         return notified;
       }
       let decodedPromise;
       if (cryptoParams) {
         decodedPromise = PushCrypto.decodeMsg(
           message,
           record.p256dhPrivateKey,
+          record.p256dhPublicKey,
           cryptoParams.dh,
           cryptoParams.salt,
-          cryptoParams.rs
+          cryptoParams.rs,
+          cryptoParams.auth ? record.authenticationSecret : null
         );
       } else {
         decodedPromise = Promise.resolve(null);
       }
       return decodedPromise.then(message => {
         if (shouldNotify) {
           notified = this._notifyApp(record, message);
         }
         // Update quota after the delay, at which point
         // we check for visible notifications.
         setTimeout(() => this._updateQuota(keyID),
           prefs.get("quotaUpdateDelay"));
         return notified;
+      }, error => {
+        console.error("receivedPushMessage: Error decrypting message", error);
       });
     }).catch(error => {
       console.error("receivedPushMessage: Error notifying app", error);
     });
   },
 
   _updateQuota: function(keyID) {
     console.debug("updateQuota()");
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -146,47 +146,59 @@ PushChannelListener.prototype = {
   },
 
   onStopRequest: function(aRequest, aContext, aStatusCode) {
     console.debug("PushChannelListener: onStopRequest()", "status code",
       aStatusCode);
     if (Components.isSuccessCode(aStatusCode) &&
         this._mainListener &&
         this._mainListener._pushService) {
+      let requiresAuthenticationSecret = true;
 
-      var keymap = encryptKeyFieldParser(aRequest);
+      var keymap = encryptKeyFieldParser(aRequest, "Crypto-Key");
       if (!keymap) {
-        return;
+        // Backward compatibility: use the absence of Crypto-Key to indicate
+        // that the authentication secret isn't used.
+        requiresAuthenticationSecret = false;
+        keymap = encryptKeyFieldParser(aRequest, "Encryption-Key");
+        if (!keymap) {
+          return;
+        }
       }
       var enc = encryptFieldParser(aRequest);
       if (!enc || !enc.keyid) {
         return;
       }
       var dh = keymap[enc.keyid];
       var salt = enc.salt;
       var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
       if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
         return;
       }
 
       var msg = concatArray(this._message);
 
+      let cryptoParams = {
+        dh: dh,
+        salt: salt,
+        rs: rs,
+        auth: requiresAuthenticationSecret,
+      };
+
       this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
                                                          this._ackUri,
                                                          msg,
-                                                         dh,
-                                                         salt,
-                                                         rs);
+                                                         cryptoParams);
     }
   }
 };
 
-function encryptKeyFieldParser(aRequest) {
+function encryptKeyFieldParser(aRequest, name) {
   try {
-    var encryptKeyField = aRequest.getRequestHeader("Encryption-Key");
+    var encryptKeyField = aRequest.getRequestHeader(name);
     return getEncryptionKeyParams(encryptKeyField);
   } catch(e) {
     // getRequestHeader can throw.
     return null;
   }
 }
 
 function encryptFieldParser(aRequest) {
@@ -485,19 +497,20 @@ this.PushServiceHttp2 = {
     console.debug("subscribeResource()");
 
     return this._subscribeResourceInternal({
       record: aRecord,
       retries: 0
     })
     .then(result =>
       PushCrypto.generateKeys()
-      .then(exportedKeys => {
-        result.p256dhPublicKey = exportedKeys[0];
-        result.p256dhPrivateKey = exportedKeys[1];
+        .then(([publicKey, privateKey]) => {
+        result.p256dhPublicKey = publicKey;
+        result.p256dhPrivateKey = privateKey;
+        result.authenticationSecret = PushCrypto.generateAuthenticationSecret();
         this._conns[result.subscriptionUri] = {
           channel: null,
           listener: null,
           countUnableToConnect: 0,
           lastStartListening: 0,
           waitingForAlarm: false
         };
         this._listenForMsgs(result.subscriptionUri);
@@ -668,17 +681,17 @@ this.PushServiceHttp2 = {
   },
 
   // Start listening if subscriptions present.
   startConnections: function(aSubscriptions) {
     console.debug("startConnections()", aSubscriptions.length);
 
     for (let i = 0; i < aSubscriptions.length; i++) {
       let record = aSubscriptions[i];
-      this._mainPushService.ensureP256dhKey(record).then(record => {
+      this._mainPushService.ensureCrypto(record).then(record => {
         this._startSingleConnection(record);
       }, error => {
         console.error("startConnections: Error updating record",
           record.keyID, error);
       });
     }
   },
 
@@ -795,24 +808,19 @@ this.PushServiceHttp2 = {
       this._resubscribe(aSubscriptionUri);
     } else if (Math.floor(aRequest.responseStatus / 100) == 2) { // This should be 204
       setTimeout(_ => this._listenForMsgs(aSubscriptionUri), 0);
     } else {
       this._retryAfterBackoff(aSubscriptionUri, -1);
     }
   },
 
-  _pushChannelOnStop: function(aUri, aAckUri, aMessage, dh, salt, rs) {
+  _pushChannelOnStop: function(aUri, aAckUri, aMessage, cryptoParams) {
     console.debug("pushChannelOnStop()");
 
-    let cryptoParams = {
-      dh: dh,
-      salt: salt,
-      rs: rs,
-    };
     this._mainPushService.receivedPushMessage(
       aUri, aMessage, cryptoParams, record => {
         // Always update the stored record.
         return record;
       }
     )
     .then(_ => this._ackMsgRecv(aAckUri))
     .catch(err => {
--- a/dom/push/PushServiceWebSocket.jsm
+++ b/dom/push/PushServiceWebSocket.jsm
@@ -58,31 +58,36 @@ XPCOMUtils.defineLazyGetter(this, "conso
     prefix: "PushServiceWebSocket",
   });
 });
 
 function getCryptoParams(headers) {
   if (!headers) {
     return null;
   }
-  var keymap = getEncryptionKeyParams(headers.encryption_key);
+  var requiresAuthenticationSecret = true;
+  var keymap = getEncryptionKeyParams(headers.crypto_key);
   if (!keymap) {
-    return null;
+    requiresAuthenticationSecret = false;
+    keymap = getEncryptionKeyParams(headers.encryption_key);
+    if (!keymap) {
+      return null;
+    }
   }
   var enc = getEncryptionParams(headers.encryption);
   if (!enc || !enc.keyid) {
     return null;
   }
   var dh = keymap[enc.keyid];
   var salt = enc.salt;
   var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
   if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
     return null;
   }
-  return {dh, salt, rs};
+  return {dh, salt, rs, auth: requiresAuthenticationSecret};
 }
 
 /**
  * A proxy between the PushService and the WebSocket. The listener is used so
  * that the PushService can silence messages from the WebSocket by setting
  * PushWebSocketListener._pushService to null. This is required because
  * a WebSocket can continue to send messages or errors after it has been
  * closed but the PushService may not be interested in these. It's easier to
@@ -797,17 +802,17 @@ this.PushServiceWebSocket = {
       this._UAID = reply.uaid;
       this._currentState = STATE_READY;
       prefs.observe("userAgentID", this);
 
       this._dataEnabled = !!reply.use_webpush;
       if (this._dataEnabled) {
         this._mainPushService.getAllUnexpired().then(records =>
           Promise.all(records.map(record =>
-            this._mainPushService.ensureP256dhKey(record).catch(error => {
+            this._mainPushService.ensureCrypto(record).catch(error => {
               console.error("finishHandshake: Error updating record",
                 record.keyID, error);
             })
           ))
         ).then(sendRequests);
       } else {
         sendRequests();
       }
@@ -1009,16 +1014,17 @@ this.PushServiceWebSocket = {
       }).then(record => {
         if (!this._dataEnabled) {
           return record;
         }
         return PushCrypto.generateKeys()
           .then(([publicKey, privateKey]) => {
             record.p256dhPublicKey = publicKey;
             record.p256dhPrivateKey = privateKey;
+            record.authenticationSecret = PushCrypto.generateAuthenticationSecret();
             return record;
           });
       });
     }
 
     this._queueRequest({channelID: record.channelID,
                         messageType: action});
     return Promise.resolve();
--- a/dom/webidl/PushSubscription.webidl
+++ b/dom/webidl/PushSubscription.webidl
@@ -6,21 +6,23 @@
 * The origin of this IDL file is
 * https://w3c.github.io/push-api/
 */
 
 interface Principal;
 
 enum PushEncryptionKeyName
 {
-  "p256dh"
+  "p256dh",
+  "auth"
 };
 
 [Exposed=(Window,Worker), Func="nsContentUtils::PushEnabled",
- ChromeConstructor(DOMString pushEndpoint, DOMString scope, ArrayBuffer? key)]
+ ChromeConstructor(DOMString pushEndpoint, DOMString scope,
+                   ArrayBuffer? key, ArrayBuffer? authSecret)]
 interface PushSubscription
 {
     readonly attribute USVString endpoint;
     ArrayBuffer? getKey(PushEncryptionKeyName name);
     [Throws, UseCounter]
     Promise<boolean> unsubscribe();
     jsonifier;