Bug 1257821 - Support the new `aesgcm` content encoding scheme. r=mt
authorKit Cambridge <kcambridge@mozilla.com>
Fri, 18 Mar 2016 09:01:50 -0700
changeset 290450 7e9d1b43cf767da76f1223427bbd4da980a3a37d
parent 290449 2c90f268c62bcd3c55776ec9ffa8eead9ddf27d7
child 290451 5eecbb62c31e3f6974cdaf8d71d153aab1f7fe57
push id30120
push userryanvm@gmail.com
push dateSat, 26 Mar 2016 02:08:36 +0000
treeherdermozilla-central@8a4359ad909f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt
bugs1257821
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 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_crypto.js
dom/push/test/xpcshell/test_notification_data.js
dom/push/test/xpcshell/test_notification_http2.js
dom/push/test/xpcshell/xpcshell.ini
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 (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).
+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) {
@@ -48,36 +56,50 @@ function getEncryptionParams(encryptFiel
 }
 
 this.getCryptoParams = function(headers) {
   if (!headers) {
     return null;
   }
 
   var requiresAuthenticationSecret = true;
-  var keymap = getEncryptionKeyParams(headers.crypto_key);
-  if (!keymap) {
-    requiresAuthenticationSecret = false;
-    keymap = getEncryptionKeyParams(headers.encryption_key);
+  var keymap;
+  var padSize;
+  if (headers.encoding == AESGCM_ENCODING) {
+    // aesgcm uses the Crypto-Key header, 2 bytes for the pad length, and an
+    // authentication secret.
+    keymap = getEncryptionKeyParams(headers.crypto_key);
+    padSize = 2;
+  } else if (headers.encoding == AESGCM128_ENCODING) {
+    // aesgcm128 uses Crypto-Key or Encryption-Key, and 1 byte for the pad
+    // length.
+    keymap = getEncryptionKeyParams(headers.crypto_key);
+    padSize = 1;
     if (!keymap) {
-      return null;
+      // Encryption-Key header indicates unauthenticated encryption.
+      requiresAuthenticationSecret = false;
+      keymap = getEncryptionKeyParams(headers.encryption_key);
     }
   }
+  if (!keymap) {
+    return null;
+  }
+
   var enc = getEncryptionParams(headers.encryption);
   if (!enc) {
     return null;
   }
   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)) {
+  if (!dh || !salt || isNaN(rs) || (rs <= padSize)) {
     return null;
   }
-  return {dh, salt, rs, auth: requiresAuthenticationSecret};
+  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 +212,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 +236,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 +280,72 @@ 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 :
+                                   AESGCM128_ENCRYPT_INFO;
     } else {
+      if (padSize == 2) {
+        throw new Error("aesgcm encoding requires an authentication secret");
+      }
       kdfPromise = Promise.resolve(new hkdf(salt, ikm));
       context = new Uint8Array(0);
+      encryptInfo = AESGCM128_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 = decoded[0];
+    if (padSize == 2) {
+      pad = (pad << 8) | decoded[1];
+    }
+    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);
new file mode 100644
--- /dev/null
+++ b/dom/push/test/xpcshell/test_crypto.js
@@ -0,0 +1,245 @@
+'use strict';
+
+const {
+  base64UrlDecode,
+  getCryptoParams,
+  PushCrypto,
+} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function* test_crypto_getCryptoParams() {
+  let testData = [
+  // These headers should parse correctly.
+  {
+    desc: 'aesgcm with multiple keys',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'keyid=p256dh;dh=Iy1Je2Kv11A,p256ecdsa=o2M8QfiEKuI',
+      encryption: 'keyid=p256dh;salt=upk1yFkp1xI',
+    },
+    params: {
+      dh: 'Iy1Je2Kv11A',
+      salt: 'upk1yFkp1xI',
+      rs: 4096,
+      auth: true,
+      padSize: 2,
+    },
+  }, {
+    desc: 'aesgcm with quoted key param',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh="byfHbUffc-k"',
+      encryption: 'salt=C11AvAsp6Gc',
+    },
+    params: {
+      dh: 'byfHbUffc-k',
+      salt: 'C11AvAsp6Gc',
+      rs: 4096,
+      auth: true,
+      padSize: 2,
+    },
+  }, {
+    desc: 'aesgcm with Crypto-Key and rs = 24',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh="ybuT4VDz-Bg"',
+      encryption: 'salt=H7U7wcIoIKs; rs=24',
+    },
+    params: {
+      dh: 'ybuT4VDz-Bg',
+      salt: 'H7U7wcIoIKs',
+      rs: 24,
+      auth: true,
+      padSize: 2,
+    },
+  }, {
+    desc: 'aesgcm128 with Encryption-Key and rs = 2',
+    headers: {
+      encoding: 'aesgcm128',
+      encryption_key: 'keyid=legacy; dh=LqrDQuVl9lY',
+      encryption: 'keyid=legacy; salt=YngI8B7YapM; rs=2',
+    },
+    params: {
+      dh: 'LqrDQuVl9lY',
+      salt: 'YngI8B7YapM',
+      rs: 2,
+      auth: false,
+      padSize: 1,
+    },
+  }, {
+    desc: 'aesgcm128 with Encryption-Key',
+    headers: {
+      encoding: 'aesgcm128',
+      encryption_key: 'keyid=v2; dh=VA6wmY1IpiE',
+      encryption: 'keyid=v2; salt=khtpyXhpDKM',
+    },
+    params: {
+      dh: 'VA6wmY1IpiE',
+      salt: 'khtpyXhpDKM',
+      rs: 4096,
+      auth: false,
+      padSize: 1,
+    }
+  }, {
+    desc: 'aesgcm128 with Crypto-Key',
+    headers: {
+      encoding: 'aesgcm128',
+      crypto_key: 'keyid=v2; dh=VA6wmY1IpiE',
+      encryption: 'keyid=v2; salt=F0Im7RtGgNY',
+    },
+    params: {
+      dh: 'VA6wmY1IpiE',
+      salt: 'F0Im7RtGgNY',
+      rs: 4096,
+      auth: true,
+      padSize: 1,
+    },
+  },
+
+  // These headers should be rejected.
+  {
+    desc: 'Invalid encoding',
+    headers: {
+      encoding: 'nonexistent',
+    },
+    params: null,
+  }, {
+    desc: 'Invalid record size',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh=pbmv1QkcEDY',
+      encryption: 'dh=Esao8aTBfIk;rs=bad',
+    },
+    params: null,
+  }, {
+    desc: 'Insufficiently large record size',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh=fK0EXaw5IU8',
+      encryption: 'salt=orbLLmlbJfM;rs=1',
+    },
+    params: null,
+  }, {
+    desc: 'aesgcm with Encryption-Key',
+    headers: {
+      encoding: 'aesgcm',
+      encryption_key: 'dh=FplK5KkvUF0',
+      encryption: 'salt=p6YHhFF3BQY',
+    },
+    params: null,
+  }];
+
+  for (let test of testData) {
+    let params = getCryptoParams(test.headers);
+    deepEqual(params, test.params, test.desc);
+  }
+});
+
+add_task(function* test_crypto_decodeMsg() {
+  let privateKey = {
+    "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 expectedSuccesses = [{
+    desc: 'padSize = 2, rs = 24, pad = 0',
+    result: 'Some message',
+    data: 'Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU',
+    senderPublicKey: 'BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo',
+    salt: 'zCU18Rw3A5aB_Xi-vfixmA',
+    rs: 24,
+    authSecret: 'aTDc6JebzR6eScy2oLo4RQ',
+    padSize: 2,
+  }, {
+    desc: 'padSize = 2, rs = 8, pad = 16',
+    result: 'Yet another message',
+    data: 'uEC5B_tR-fuQ3delQcrzrDCp40W6ipMZjGZ78USDJ5sMj-6bAOVG3AK6JqFl9E6AoWiBYYvMZfwThVxmDnw6RHtVeLKFM5DWgl1EwkOohwH2EhiDD0gM3io-d79WKzOPZE9rDWUSv64JstImSfX_ADQfABrvbZkeaWxh53EG59QMOElFJqHue4dMURpsMXg',
+    senderPublicKey: 'BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ',
+    salt: 'ZFhzj0S-n29g9P2p4-I7tA',
+    rs: 8,
+    authSecret: '6plwZnSpVUbF7APDXus3UQ',
+    padSize: 2,
+  }, {
+    desc: 'padSize = 1, rs = 4096, pad = 2',
+    result: 'aesgcm128 encrypted message',
+    data: 'ljBJ44NPzJFH9EuyT5xWMU4vpZ90MdAqaq1TC1kOLRoPNHtNFXeJ0GtuSaE',
+    senderPublicKey: 'BOmnfg02vNd6RZ7kXWWrCGFF92bI-rQ-bV0Pku3-KmlHwbGv4ejWqgasEdLGle5Rhmp6SKJunZw2l2HxKvrIjfI',
+    salt: 'btxxUtclbmgcc30b9rT3Bg',
+    rs: 4096,
+    padSize: 1,
+  }, {
+    desc: 'padSize = 2, rs = 3, pad = 0',
+    result: 'Small record size',
+    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,
+  }, {
+    desc: 'padSize = 1, rs = 4096, auth secret, pad = 8',
+    result: 'aesgcm128 with auth secret',
+    data: 'h0FmyldY8aT5EQ6CJrbfRn_IdDvytoLeHb9_q5CjtdFRfgDRknxLmOzavLaVG4oOiS0r',
+    senderPublicKey: 'BCXHk7O8CE-9AOp6xx7g7c-NCaNpns1PyyHpdcmDaijLbT6IdGq0ezGatBwtFc34BBfscFxdk4Tjksa2Mx5rRCM',
+    salt: 'aGBpoKklLtrLcAUCcCr7JQ',
+    rs: 4096,
+    authSecret: 'Sxb6u0gJIhGEogyLawjmCw',
+    padSize: 1,
+  }];
+  for (let test of expectedSuccesses) {
+    let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
+    let result = yield PushCrypto.decodeMsg(base64UrlDecode(test.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 = [{
+    desc: 'Missing padding',
+    data: 'anvsHj7oBQTPMhv7XSJEsvyMS4-8EtbC7HgFZsKaTg',
+    senderPublicKey: 'BMSqfc3ohqw2DDgu3nsMESagYGWubswQPGxrW1bAbYKD18dIHQBUmD3ul_lu7MyQiT5gNdzn5JTXQvCcpf-oZE4',
+    salt: 'Czx2i18rar8XWOXAVDnUuw',
+    rs: 4096,
+    padSize: 1,
+  }, {
+    desc: 'padSize > rs',
+    data: 'Ct_h1g7O55e6GvuhmpjLsGnv8Rmwvxgw8iDESNKGxk_8E99iHKDzdV8wJPyHA-6b2E6kzuVa5UWiQ7s4Zms1xzJ4FKgoxvBObXkc_r_d4mnb-j245z3AcvRmcYGk5_HZ0ci26SfhAN3lCgxGzTHS4nuHBRkGwOb4Tj4SFyBRlLoTh2jyVK2jYugNjH9tTrGOBg7lP5lajLTQlxOi91-RYZSfFhsLX3LrAkXuRoN7G1CdiI7Y3_eTgbPIPabDcLCnGzmFBTvoJSaQF17huMl_UnWoCj2WovA4BwK_TvWSbdgElNnQ4CbArJ1h9OqhDOphVu5GUGr94iitXRQR-fqKPMad0ULLjKQWZOnjuIdV1RYEZ873r62Yyd31HoveJcSDb1T8l_QK2zVF8V4k0xmK9hGuC0rF5YJPYPHgl5__usknzxMBnRrfV5_MOL5uPZwUEFsu',
+    senderPublicKey: 'BAcMdWLJRGx-kPpeFtwqR3GE1LWzd1TYh2rg6CEFu53O-y3DNLkNe_BtGtKRR4f7ZqpBMVS6NgfE2NwNPm3Ndls',
+    salt: 'NQVTKhB0rpL7ZzKkotTGlA',
+    rs: 1,
+    authSecret: '6plwZnSpVUbF7APDXus3UQ',
+    padSize: 2,
+  }, {
+    desc: 'Encrypted with padSize = 1, decrypted with padSize = 2 and auth secret',
+    data: 'fwkuwTTChcLnrzsbDI78Y2EoQzfnbMI8Ax9Z27_rwX8',
+    senderPublicKey: 'BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
+    salt: 'c6JQl9eJ0VvwrUVCQDxY7Q',
+    rs: 4096,
+    authSecret: 'BhbpNTWyO5wVJmVKTV6XaA',
+    padSize: 2,
+  }, {
+    desc: 'Truncated input',
+    data: 'AlDjj6NvT5HGyrHbT8M5D6XBFSra6xrWS9B2ROaCIjwSu3RyZ1iyuv0',
+    rs: 25,
+  }];
+  for (let test of expectedFailures) {
+    let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
+    yield rejects(
+      PushCrypto.decodeMsg(base64UrlDecode(test.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
@@ -121,47 +121,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'
       }
     },
@@ -169,48 +172,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 notifyPromise;
--- a/dom/push/test/xpcshell/xpcshell.ini
+++ b/dom/push/test/xpcshell/xpcshell.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 head = head.js head-http2.js
 tail =
 # Push notifications and alarms are currently disabled on Android.
 skip-if = toolkit == 'android'
 
 [test_clear_origin_data.js]
+[test_crypto.js]
 [test_drop_expired.js]
 [test_handler_service.js]
 support-files = PushServiceHandler.js PushServiceHandler.manifest
 [test_notification_ack.js]
 [test_notification_data.js]
 [test_notification_duplicate.js]
 [test_notification_error.js]
 [test_notification_incomplete.js]
--- 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;
   }