Bug 1257821 - Support the new `aesgcm` content encoding scheme. r=mt draft
authorKit Cambridge <kcambridge@mozilla.com>
Fri, 18 Mar 2016 09:01:50 -0700
changeset 343666 655744c28a2cdb63be936ea6aff82d143b4b4b41
parent 342865 4037eb98974db1b1e0b5012c8a7f3a36428eaa11
child 343667 bb1e4fee3eb78f8229f7ab0ab803e8295732272c
push id13668
push userkcambridge@mozilla.com
push dateWed, 23 Mar 2016 02:23:32 +0000
reviewersmt
bugs1257821
milestone48.0a1
Bug 1257821 - Support the new `aesgcm` content encoding scheme. r=mt MozReview-Commit-ID: IPNXletzJRK
dom/push/PushCrypto.jsm
dom/push/PushService.jsm
dom/push/PushServiceAndroidGCM.jsm
dom/push/PushServiceHttp2.jsm
dom/push/test/xpcshell/test_notification_data.js
dom/push/test/xpcshell/test_notification_http2.js
testing/xpcshell/moz-http2/moz-http2.js
--- a/dom/push/PushCrypto.jsm
+++ b/dom/push/PushCrypto.jsm
@@ -9,17 +9,25 @@ const Cu = Components.utils;
 
 Cu.importGlobalProperties(['crypto']);
 
 this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
                          'getCryptoParams',
                          'base64UrlDecode'];
 
 var UTF8 = new TextEncoder('utf-8');
-var ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
+
+// Legacy encryption scheme.
+var AESGCM_128_ENCODING = 'aesgcm128';
+var AESGCM_128_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
+
+// New encryption scheme.
+var AESGCM_ENCODING = 'aesgcm';
+var AESGCM_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm');
+
 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');
 var ECDH_KEY = { name: 'ECDH', namedCurve: 'P-256' };
 // A default keyid with a name that won't conflict with a real keyid.
 var DEFAULT_KEYID = '';
 
 function getEncryptionKeyParams(encryptKeyField) {
@@ -47,19 +55,25 @@ function getEncryptionParams(encryptFiel
   return p.split(';').reduce(parseHeaderFieldParams, {});
 }
 
 this.getCryptoParams = function(headers) {
   if (!headers) {
     return null;
   }
 
+  var encoding = headers.encoding;
+  if (encoding != AESGCM_ENCODING && encoding != AESGCM_128_ENCODING) {
+    return null;
+  }
+
   var requiresAuthenticationSecret = true;
   var keymap = getEncryptionKeyParams(headers.crypto_key);
-  if (!keymap) {
+  if (!keymap && encoding == AESGCM_128_ENCODING) {
+    // Only allow the older `aesgcm128` scheme to omit the secret.
     requiresAuthenticationSecret = false;
     keymap = getEncryptionKeyParams(headers.encryption_key);
     if (!keymap) {
       return null;
     }
   }
   var enc = getEncryptionParams(headers.encryption);
   if (!enc) {
@@ -67,17 +81,18 @@ this.getCryptoParams = function(headers)
   }
   var dh = keymap[enc.keyid || DEFAULT_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, auth: requiresAuthenticationSecret};
+  var padSize = encoding == AESGCM_128_ENCODING ? 1 : 2;
+  return {dh, salt, rs, auth: requiresAuthenticationSecret, padSize};
 }
 
 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()
@@ -190,18 +205,18 @@ this.PushCrypto = {
     return crypto.subtle.generateKey(ECDH_KEY, true, ['deriveBits'])
       .then(cryptoKey =>
          Promise.all([
            crypto.subtle.exportKey('raw', cryptoKey.publicKey),
            crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
          ]));
   },
 
-  decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey,
-            aSalt, aRs, aAuthenticationSecret) {
+  decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, aSalt, aRs,
+            aAuthenticationSecret, aPadSize) {
 
     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.
@@ -214,31 +229,34 @@ this.PushCrypto = {
       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(new Uint8Array(ikm),
+    .then(ikm => this._deriveKeyAndNonce(aPadSize,
+                                         new Uint8Array(ikm),
                                          base64UrlDecode(aSalt),
                                          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]))))
+        this._decodeChunk(aPadSize, slice, index, r[1], r[0]))))
     .then(r => concatArray(r));
   },
 
-  _deriveKeyAndNonce(ikm, salt, receiverKey, senderKey, authenticationSecret) {
+  _deriveKeyAndNonce(padSize, ikm, salt, receiverKey, senderKey,
+                     authenticationSecret) {
     var kdfPromise;
     var context;
+    var encryptInfo;
     // 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.
@@ -255,48 +273,69 @@ this.PushCrypto = {
 
       // 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
       ]);
+      // Finally, we use the pad size to infer the content encoding.
+      encryptInfo = padSize == 2 ? AESGCM_ENCRYPT_INFO :
+                                   AESGCM_128_ENCRYPT_INFO;
     } else {
       kdfPromise = Promise.resolve(new hkdf(salt, ikm));
       context = new Uint8Array(0);
+      if (padSize == 2) {
+        throw new Error("aesgcm encoding requires an authentication secret");
+      }
+      encryptInfo = AESGCM_128_ENCRYPT_INFO;
     }
     return kdfPromise.then(kdf => Promise.all([
-      kdf.extract(concatArray([ENCRYPT_INFO, context]), 16)
+      kdf.extract(concatArray([encryptInfo, 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) {
+  _decodeChunk(aPadSize, 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.
-          for (var i = 1; i <= decoded[0]; i++) {
-            if (decoded[i] != 0) {
-              return Promise.reject(new Error('Padding is wrong!'));
-            }
-          }
-          return decoded.slice(decoded[0] + 1);
-        }
-      });
-  }
+      .then(decoded => this._unpadChunk(aPadSize, new Uint8Array(decoded)));
+  },
+
+  /**
+   * Removes padding from a decrypted chunk.
+   *
+   * @param {Number} padSize The size of the padding length prepended to each
+   *  chunk. For `aesgcm`, the padding length is expressed as a 16-bit unsigned
+   *  big endian integer. For `aesgcm128`, the padding is an 8-bit integer.
+   * @param {Uint8Array} decoded The decrypted, padded chunk.
+   * @returns {Uint8Array} The chunk with padding removed.
+   */
+  _unpadChunk(padSize, decoded) {
+    if (padSize < 1 || padSize > 2) {
+      throw new Error('Unsupported pad size');
+    }
+    if (decoded.length < padSize) {
+      throw new Error('Decoded array is too short!');
+    }
+    var pad = padSize == 2 ? (decoded[0] << 8) | decoded[1] : decoded[0];
+    if (pad > decoded.length) {
+      throw new Error ('Padding is wrong!');
+    }
+    // All padded bytes must be zero except the first one.
+    for (var i = padSize; i <= pad; i++) {
+      if (decoded[i] !== 0) {
+        throw new Error('Padding is wrong!');
+      }
+    }
+    return decoded.slice(pad + padSize);
+  },
 };
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -806,17 +806,18 @@ this.PushService = {
       if (cryptoParams) {
         decodedPromise = PushCrypto.decodeMsg(
           message,
           record.p256dhPrivateKey,
           record.p256dhPublicKey,
           cryptoParams.dh,
           cryptoParams.salt,
           cryptoParams.rs,
-          cryptoParams.auth ? record.authenticationSecret : null
+          cryptoParams.auth ? record.authenticationSecret : null,
+          cryptoParams.padSize
         );
       } else {
         decodedPromise = Promise.resolve(null);
       }
       return decodedPromise.then(message => {
         if (shouldNotify) {
           notified = this._notifyApp(record, message);
         }
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -109,16 +109,17 @@ this.PushServiceAndroidGCM = {
       let message = null;
       let cryptoParams = null;
 
       if (data.message && data.enc && (data.enckey || data.cryptokey)) {
         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);
       }
 
       console.debug("Delivering message to main PushService:", message, cryptoParams);
       this._mainPushService.receivedPushMessage(
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -150,16 +150,17 @@ PushChannelListener.prototype = {
       aStatusCode);
     if (Components.isSuccessCode(aStatusCode) &&
         this._mainListener &&
         this._mainListener._pushService) {
       let headers = {
         encryption_key: getHeaderField(aRequest, "Encryption-Key"),
         crypto_key: getHeaderField(aRequest, "Crypto-Key"),
         encryption: getHeaderField(aRequest, "Encryption"),
+        encoding: getHeaderField(aRequest, "Content-Encoding"),
       };
       let cryptoParams = getCryptoParams(headers);
       let msg = concatArray(this._message);
 
       this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
                                                          this._ackUri,
                                                          msg,
                                                          cryptoParams);
--- a/dom/push/test/xpcshell/test_notification_data.js
+++ b/dom/push/test/xpcshell/test_notification_data.js
@@ -122,47 +122,50 @@ add_task(function* test_notification_ack
   let allTestData = [
     {
       channelID: 'subscription1',
       version: 'v1',
       send: {
         headers: {
           encryption_key: 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
           encryption: 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
+          encoding: 'aesgcm128',
         },
         data: 'NwrrOWPxLE8Sv5Rr0Kep7n0-r_j3rsYrUw_CXPo',
         version: 'v1',
       },
       receive: {
         scope: 'https://example.com/page/1',
         data: 'Some message'
       }
     },
     {
       channelID: 'subscription2',
       version: 'v2',
       send: {
         headers: {
           encryption_key: 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
           encryption: 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
+          encoding: 'aesgcm128',
         },
         data: 'Zt9dEdqgHlyAL_l83385aEtb98ZBilz5tgnGgmwEsl5AOCNgesUUJ4p9qUU',
       },
       receive: {
         scope: 'https://example.com/page/2',
         data: 'Some message'
       }
     },
     {
       channelID: 'subscription3',
       version: 'v3',
       send: {
         headers: {
           encryption_key: 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
           encryption: 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
+          encoding: 'aesgcm128',
         },
         data: 'LKru3ZzxBZuAxYtsaCfaj_fehkrIvqbVd1iSwnwAUgnL-cTeDD-83blxHXTq7r0z9ydTdMtC3UjAcWi8LMnfY-BFzi0qJAjGYIikDA',
       },
       receive: {
         scope: 'https://example.com/page/3',
         data: 'Some message'
       }
     },
@@ -170,48 +173,68 @@ add_task(function* test_notification_ack
     // header field.  No padding or record size changes.
     {
       channelID: 'subscription1',
       version: 'v4',
       send: {
         headers: {
           crypto_key: 'keyid=v4;dh="BHqG01j7rOfp12BEDzxWXxlCaU4cdOx2DZAwCt3QuzEsnXN9lCna9QmZCkVpXsx7sAlaEmtl_VfF1lHlFS7XWcA"',
           encryption: 'keyid="v4";salt="X5-iy5rzhm4naNmMHdSYJw"',
+          encoding: 'aesgcm128',
         },
         data: '7YlxyNlZsNX4UNknHxzTqFrcrzz58W95uXBa0iY',
       },
       receive: {
         scope: 'https://example.com/page/1',
         data: 'Some message'
       }
     },
+    // A message encoded with `aesgcm` (2 bytes of padding).
+    {
+      channelID: 'subscription1',
+      version: 'v5',
+      send: {
+        headers: {
+          crypto_key: 'dh="BMh_vsnqu79ZZkMTYkxl4gWDLdPSGE72Lr4w2hksSFW398xCMJszjzdblAWXyhSwakRNEU_GopAm4UGzyMVR83w"',
+          encryption: 'salt="C14Wb7rQTlXzrgcPHtaUzw"',
+          encoding: 'aesgcm',
+        },
+        data: 'pus4kUaBWzraH34M-d_oN8e0LPpF_X6acx695AMXovDe',
+      },
+      receive: {
+        scope: 'https://example.com/page/1',
+        data: 'Another message'
+      }
+    },
     // A message with 17 bytes of padding and rs of 24
     {
       channelID: 'subscription2',
       version: 'v5',
       send: {
         headers: {
           crypto_key: 'keyid="v5"; dh="BJhyKIH5P30YUKn1bolj_LMnael1-KZT_aGXgD2CRspBfv9gcUhVAmpxToZrw7QQEKl9K83b3zcqNY6G_dFhEsI"',
           encryption: 'keyid=v5;salt="bLmqCy550eK1Ao41tD7orA";rs=24',
+          encoding: 'aesgcm128',
         },
         data: 'SQDlDg1ftLkM_ruZlmyB2bk9L78HYtkcbA-y4-uAxwL-G4KtOA-J-A_rJ007Vi6NUkQe9K4kSZeIBrIUpmGv',
       },
       receive: {
         scope: 'https://example.com/page/2',
         data: 'Some message'
       }
     },
     // A message without key identifiers.
     {
       channelID: 'subscription3',
       version: 'v6',
       send: {
         headers: {
           crypto_key: 'dh="BEgnDmVw9Gcn1fWA5t53Jtpsgfewk_pzsjSc_PBPpPmROWGQA2v8ESrSsQgosNXx0o-uMMhi9tDAUeks3380kd8"',
           encryption: 'salt=T9DM8bNxuMHRVTn4LzkJDQ',
+          encoding: 'aesgcm128',
         },
         data: '7KUCi0dBBJbWmsYTqEqhFrgTv4ZOo_BmQRQ_2kY',
       },
       receive: {
         scope: 'https://example.com/page/3',
         data: 'Some message'
       }
     },
--- a/dom/push/test/xpcshell/test_notification_http2.js
+++ b/dom/push/test/xpcshell/test_notification_http2.js
@@ -1,16 +1,17 @@
 /* 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;
 
@@ -51,16 +52,18 @@ function run_test() {
 add_task(function* test_pushNotifications() {
 
   // /pushNotifications/subscription1 will send a message with no rs and padding
   // length 1.
   // /pushNotifications/subscription2 will send a message with no rs and padding
   // length 16.
   // /pushNotifications/subscription3 will send a message with rs equal 24 and
   // padding length 16.
+  // /pushNotifications/subscription4 will send a message with no rs and padding
+  // length 256.
 
   let db = PushServiceHttp2.newPushDB();
   do_register_cleanup(() => {
     return db.drop().then(_ => db.close());
   });
 
   var serverURL = "https://localhost:" + serverPort;
 
@@ -116,16 +119,36 @@ add_task(function* test_pushNotification
       kty: 'EC',
       x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
       y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
     },
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { 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'),
+    p256dhPrivateKey: {
+      crv: 'P-256',
+      d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw',
+      ext: true,
+      key_ops: ["deriveBits"],
+      kty: 'EC',
+      x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ',
+      y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'
+    },
+    authenticationSecret: base64UrlDecode('cwDVC1iwAn8E37mkR3tMSg'),
+    originAttributes: ChromeUtils.originAttributesToSuffix(
+      { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+    quota: Infinity,
+    systemRecord: true,
   }];
 
   for (let record of records) {
     yield db.put(record);
   }
 
   let notifyPromise = Promise.all([
     promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
@@ -143,17 +166,24 @@ add_task(function* test_pushNotification
       }
     }),
     promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
       var message = subject.QueryInterface(Ci.nsIPushMessage);
       if (message && (data == "https://example.com/page/3")){
         equal(message.text(), "Some message", "decoded message is incorrect");
         return true;
       }
-    })
+    }),
+    promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
+      var message = subject.QueryInterface(Ci.nsIPushMessage);
+      if (message && (data == "https://example.com/page/4")){
+        equal(message.text(), "Yet another message", "decoded message is incorrect");
+        return true;
+      }
+    }),
   ]);
 
   PushService.init({
     serverURI: serverURL,
     db
   });
 
   yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
--- a/testing/xpcshell/moz-http2/moz-http2.js
+++ b/testing/xpcshell/moz-http2/moz-http2.js
@@ -199,17 +199,17 @@ function handleRequest(req, res) {
   // the headers to have something illegal in them
   Compressor.prototype.compress = originalCompressHeaders;
 
   var u = url.parse(req.url);
   var content = getHttpContent(u.pathname);
   var push, push1, push1a, push2, push3;
 
   // PushService tests.
-  var pushPushServer1, pushPushServer2, pushPushServer3;
+  var pushPushServer1, pushPushServer2, pushPushServer3, pushPushServer4;
 
   if (req.httpVersionMajor === 2) {
     res.setHeader('X-Connection-Http2', 'yes');
     res.setHeader('X-Http2-StreamId', '' + req.stream.id);
   } else {
     res.setHeader('X-Connection-Http2', 'no');
   }
 
@@ -603,59 +603,79 @@ function handleRequest(req, res) {
     return;
   }
 
   else if (u.pathname ==="/pushNotifications/subscription1") {
     pushPushServer1 = res.push(
       { hostname: 'localhost:' + serverPort, port: serverPort,
         path : '/pushNotificationsDeliver1', method : 'GET',
         headers: { 'Encryption-Key': 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
-                   'Encryption': 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"'
+                   'Encryption': 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
+                   'Content-Encoding': 'aesgcm128',
                  }
       });
     pushPushServer1.writeHead(200, {
       'subresource' : '1'
       });
 
     pushPushServer1.end('370aeb3963f12c4f12bf946bd0a7a9ee7d3eaff8f7aec62b530fc25cfa', 'hex');
     return;
   }
 
   else if (u.pathname ==="/pushNotifications/subscription2") {
     pushPushServer2 = res.push(
       { hostname: 'localhost:' + serverPort, port: serverPort,
         path : '/pushNotificationsDeliver3', method : 'GET',
         headers: { 'Encryption-Key': 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
-                   'Encryption': 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"'
+                   'Encryption': 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
+                   'Content-Encoding': 'aesgcm128',
                  }
       });
     pushPushServer2.writeHead(200, {
       'subresource' : '1'
       });
 
     pushPushServer2.end('66df5d11daa01e5c802ff97cdf7f39684b5bf7c6418a5cf9b609c6826c04b25e403823607ac514278a7da945', 'hex');
     return;
   }
 
   else if (u.pathname ==="/pushNotifications/subscription3") {
     pushPushServer3 = res.push(
       { hostname: 'localhost:' + serverPort, port: serverPort,
         path : '/pushNotificationsDeliver3', method : 'GET',
         headers: { 'Encryption-Key': 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
-                   'Encryption': 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24'
+                   'Encryption': 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
+                   'Content-Encoding': 'aesgcm128',
                  }
       });
     pushPushServer3.writeHead(200, {
       'subresource' : '1'
       });
 
     pushPushServer3.end('2caaeedd9cf1059b80c58b6c6827da8ff7de864ac8bea6d5775892c27c005209cbf9c4de0c3fbcddb9711d74eaeebd33f7275374cb42dd48c07168bc2cc9df63e045ce2d2a2408c66088a40c', 'hex');
     return;
   }
 
+  else if (u.pathname == "/pushNotifications/subscription4") {
+    pushPushServer4 = res.push(
+      { hostname: 'localhost:' + serverPort, port: serverPort,
+        path : '/pushNotificationsDeliver4', method : 'GET',
+        headers: { 'Crypto-Key': 'keyid="notification4";dh="BJScXUUTcs7D8jJWI1AOxSgAKkF7e56ay4Lek52TqDlWo1yGd5czaxFWfsuP4j7XNWgGYm60-LKpSUMlptxPFVQ"',
+                   'Encryption': 'keyid="notification4"; salt="sn9p2QqF3V6KBclda8vx7w"',
+                   'Content-Encoding': 'aesgcm',
+                 }
+      });
+    pushPushServer4.writeHead(200, {
+      'subresource' : '1'
+      });
+
+    pushPushServer4.end('9eba7ba6192544a39bd9e9b58e702d0748f1776b27f6616cdc55d29ed5a015a6db8f2dd82cd5751a14315546194ff1c18458ab91eb36c9760ccb042670001fd9964557a079553c3591ee131ceb259389cfffab3ab873f873caa6a72e87d262b8684c3260e5940b992234deebf57a9ff3a8775742f3cbcb152d249725a28326717e19cce8506813a155eff5df9bdba9e3ae8801d3cc2b7e7f2f1b6896e63d1fdda6f85df704b1a34db7b2dd63eba11ede154300a318c6f83c41a3d32356a196e36bc905b99195fd91ae4ff3f545c42d17f1fdc1d5bd2bf7516d0765e3a859fffac84f46160b79cedda589f74c25357cf6988cd8ba83867ebd86e4579c9d3b00a712c77fcea3b663007076e21f9819423faa830c2176ff1001c1690f34be26229a191a938517', 'hex');
+    return;
+  }
+
   else if ((u.pathname === "/pushNotificationsDeliver1") ||
            (u.pathname === "/pushNotificationsDeliver2") ||
            (u.pathname === "/pushNotificationsDeliver3")) {
     res.writeHead(410, "GONE");
     res.end("");
     return;
   }