Bug 1225968 - Refactoring to move some of the push crypto logic, r=kitcambridge
authorMartin Thomson <martin.thomson@gmail.com>
Fri, 04 Dec 2015 08:24:47 +1100
changeset 275944 9d619f94515405f77fac0994233c57fe9ca91a56
parent 275943 065e03c7e41618123c31c49271f61c145544352d
child 275945 c456aa0d72e4c77869d28085d05b3e341f6c6195
push id29776
push usercbook@mozilla.com
push dateWed, 09 Dec 2015 11:02:31 +0000
treeherdermozilla-central@319be5e7ce30 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskitcambridge
bugs1225968
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1225968 - Refactoring to move some of the push crypto logic, r=kitcambridge
dom/push/PushCrypto.jsm
dom/push/PushServiceHttp2.jsm
dom/push/PushServiceWebSocket.jsm
--- a/dom/push/PushCrypto.jsm
+++ b/dom/push/PushCrypto.jsm
@@ -5,46 +5,80 @@
 
 'use strict';
 
 const Cu = Components.utils;
 
 Cu.importGlobalProperties(['crypto']);
 
 this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
-                         'getEncryptionKeyParams', 'getEncryptionParams',
+                         'getCryptoParams',
                          'base64UrlDecode'];
 
 var UTF8 = new TextEncoder('utf-8');
 var ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
 var NONCE_INFO = UTF8.encode('Content-Encoding: nonce');
 var AUTH_INFO = UTF8.encode('Content-Encoding: auth\0'); // note nul-terminus
 var P256DH_INFO = UTF8.encode('P-256\0');
+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 = '';
 
-this.getEncryptionKeyParams = function(encryptKeyField) {
+function getEncryptionKeyParams(encryptKeyField) {
   if (!encryptKeyField) {
     return null;
   }
   var params = encryptKeyField.split(',');
   return params.reduce((m, p) => {
     var pmap = p.split(';').reduce(parseHeaderFieldParams, {});
     if (pmap.keyid && pmap.dh) {
       m[pmap.keyid] = pmap.dh;
     }
+    if (!m[DEFAULT_KEYID] && pmap.dh) {
+      m[DEFAULT_KEYID] = pmap.dh;
+    }
     return m;
   }, {});
-};
+}
 
-this.getEncryptionParams = function(encryptField) {
+function getEncryptionParams(encryptField) {
   var p = encryptField.split(',', 1)[0];
   if (!p) {
     return null;
   }
   return p.split(';').reduce(parseHeaderFieldParams, {});
-};
+}
+
+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);
+    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)) {
+    return null;
+  }
+  return {dh, salt, rs, auth: requiresAuthenticationSecret};
+}
 
 var parseHeaderFieldParams = (m, v) => {
   var i = v.indexOf('=');
   if (i >= 0) {
     // A quoted string with internal quotes is invalid for all the possible
     // values of this header field.
     m[v.substring(0, i).trim()] = v.substring(i + 1).trim()
                                    .replace(/^"(.*)"$/, '$1');
@@ -148,23 +182,20 @@ function generateNonce(base, index) {
 
 this.PushCrypto = {
 
   generateAuthenticationSecret() {
     return crypto.getRandomValues(new Uint8Array(12));
   },
 
   generateKeys() {
-    return crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256'},
-                                     true,
-                                     ['deriveBits'])
+    return crypto.subtle.generateKey(ECDH_KEY, true, ['deriveBits'])
       .then(cryptoKey =>
          Promise.all([
            crypto.subtle.exportKey('raw', cryptoKey.publicKey),
-           // TODO: change this when bug 1048931 lands.
            crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
          ]));
   },
 
   decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey,
             aSalt, aRs, aAuthenticationSecret) {
 
     if (aData.byteLength === 0) {
@@ -175,29 +206,27 @@ this.PushCrypto = {
     // The last chunk of data must be less than aRs, if it is not return an
     // error.
     if (aData.byteLength % (aRs + 16) === 0) {
       return Promise.reject(new Error('Data truncated'));
     }
 
     let senderKey = base64UrlDecode(aSenderPublicKey)
     return Promise.all([
-      crypto.subtle.importKey('raw', senderKey,
-                              { name: 'ECDH', namedCurve: 'P-256' },
-                              false,
-                              ['deriveBits']),
-      crypto.subtle.importKey('jwk', aPrivateKey,
-                              { name: 'ECDH', namedCurve: 'P-256' },
-                              false,
-                              ['deriveBits'])
+      crypto.subtle.importKey('raw', senderKey, ECDH_KEY,
+                              false, ['deriveBits']),
+      crypto.subtle.importKey('jwk', aPrivateKey, ECDH_KEY,
+                              false, ['deriveBits'])
     ])
-    .then(keys => crypto.subtle.deriveBits({ name: 'ECDH', public: keys[0] }, keys[1], 256))
+    .then(([appServerKey, subscriptionPrivateKey]) =>
+          crypto.subtle.deriveBits({ name: 'ECDH', public: appServerKey },
+                                   subscriptionPrivateKey, 256))
     .then(ikm => this._deriveKeyAndNonce(new Uint8Array(ikm),
                                          base64UrlDecode(aSalt),
-                                         base64UrlDecode(aPublicKey),
+                                         aPublicKey,
                                          senderKey,
                                          aAuthenticationSecret))
     .then(r =>
       // AEAD_AES_128_GCM expands ciphertext to be 16 octets longer.
       Promise.all(chunkArray(aData, aRs + 16).map((slice, index) =>
         this._decodeChunk(slice, index, r[1], r[0]))))
     .then(r => concatArray(r));
   },
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -17,18 +17,17 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 const {
   PushCrypto,
   concatArray,
-  getEncryptionKeyParams,
-  getEncryptionParams,
+  getCryptoParams,
 } = Cu.import("resource://gre/modules/PushCrypto.jsm");
 
 this.EXPORTED_SYMBOLS = ["PushServiceHttp2"];
 
 XPCOMUtils.defineLazyGetter(this, "console", () => {
   let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
     maxLogLevelPref: "dom.push.loglevel",
@@ -146,70 +145,35 @@ PushChannelListener.prototype = {
   },
 
   onStopRequest: function(aRequest, aContext, aStatusCode) {
     console.debug("PushChannelListener: onStopRequest()", "status code",
       aStatusCode);
     if (Components.isSuccessCode(aStatusCode) &&
         this._mainListener &&
         this._mainListener._pushService) {
-      let requiresAuthenticationSecret = true;
-
-      var keymap = encryptKeyFieldParser(aRequest, "Crypto-Key");
-      if (!keymap) {
-        // Backward compatibility: use the absence of Crypto-Key to indicate
-        // that the authentication secret isn't used.
-        requiresAuthenticationSecret = false;
-        keymap = encryptKeyFieldParser(aRequest, "Encryption-Key");
-        if (!keymap) {
-          return;
-        }
-      }
-      var enc = encryptFieldParser(aRequest);
-      if (!enc || !enc.keyid) {
-        return;
-      }
-      var dh = keymap[enc.keyid];
-      var salt = enc.salt;
-      var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
-      if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
-        return;
-      }
-
-      var msg = concatArray(this._message);
-
-      let cryptoParams = {
-        dh: dh,
-        salt: salt,
-        rs: rs,
-        auth: requiresAuthenticationSecret,
+      let headers = {
+        encryption_key: getHeaderField(aRequest, "Encryption-Key"),
+        crypto_key: getHeaderField(aRequest, "Crypto-Key"),
+        encryption: getHeaderField(aRequest, "Encryption"),
       };
+      let cryptoParams = getCryptoParams(headers);
+      let msg = concatArray(this._message);
 
       this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
                                                          this._ackUri,
                                                          msg,
                                                          cryptoParams);
     }
   }
 };
 
-function encryptKeyFieldParser(aRequest, name) {
+function getHeaderField(aRequest, name) {
   try {
-    var encryptKeyField = aRequest.getRequestHeader(name);
-    return getEncryptionKeyParams(encryptKeyField);
-  } catch(e) {
-    // getRequestHeader can throw.
-    return null;
-  }
-}
-
-function encryptFieldParser(aRequest) {
-  try {
-    var encryptField = aRequest.getRequestHeader("Encryption");
-    return getEncryptionParams(encryptField);
+    return aRequest.getRequestHeader(name);
   } catch(e) {
     // getRequestHeader can throw.
     return null;
   }
 }
 
 var PushServiceDelete = function(resolve, reject) {
   this._resolve = resolve;
--- a/dom/push/PushServiceWebSocket.jsm
+++ b/dom/push/PushServiceWebSocket.jsm
@@ -17,18 +17,17 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
 const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
 const {
   PushCrypto,
   base64UrlDecode,
-  getEncryptionKeyParams,
-  getEncryptionParams,
+  getCryptoParams,
 } = Cu.import("resource://gre/modules/PushCrypto.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 
 if (AppConstants.MOZ_B2G) {
   XPCOMUtils.defineLazyServiceGetter(this, "gPowerManagerService",
@@ -54,42 +53,16 @@ this.EXPORTED_SYMBOLS = ["PushServiceWeb
 XPCOMUtils.defineLazyGetter(this, "console", () => {
   let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
     maxLogLevelPref: "dom.push.loglevel",
     prefix: "PushServiceWebSocket",
   });
 });
 
-function getCryptoParams(headers) {
-  if (!headers) {
-    return null;
-  }
-  var requiresAuthenticationSecret = true;
-  var keymap = getEncryptionKeyParams(headers.crypto_key);
-  if (!keymap) {
-    requiresAuthenticationSecret = false;
-    keymap = getEncryptionKeyParams(headers.encryption_key);
-    if (!keymap) {
-      return null;
-    }
-  }
-  var enc = getEncryptionParams(headers.encryption);
-  if (!enc || !enc.keyid) {
-    return null;
-  }
-  var dh = keymap[enc.keyid];
-  var salt = enc.salt;
-  var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
-  if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
-    return null;
-  }
-  return {dh, salt, rs, auth: requiresAuthenticationSecret};
-}
-
 /**
  * A proxy between the PushService and the WebSocket. The listener is used so
  * that the PushService can silence messages from the WebSocket by setting
  * PushWebSocketListener._pushService to null. This is required because
  * a WebSocket can continue to send messages or errors after it has been
  * closed but the PushService may not be interested in these. It's easier to
  * stop listening than to have checks at specific points.
  */