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 290509 7e9d1b43cf767da76f1223427bbd4da980a3a37d
parent 290508 2c90f268c62bcd3c55776ec9ffa8eead9ddf27d7
child 290510 5eecbb62c31e3f6974cdaf8d71d153aab1f7fe57
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
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_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;
   }