Bug 1273229 - auto-generate IDs for temp installs from dir path. r=kmag draft
authorKumar McMillan <kumar.mcmillan@gmail.com>
Fri, 03 Jun 2016 16:43:45 -0500
changeset 376274 577b3d119019be313c1f700a27f9c347309c0db8
parent 375309 ab5e81678aaae8da42e11e8daf118e2a9c90baac
child 523099 1b2caa793f738b32463e3888ef59c6a32bc89f5c
push id20520
push userbmo:kumar.mcmillan@gmail.com
push dateTue, 07 Jun 2016 15:08:43 +0000
reviewerskmag
bugs1273229
milestone49.0a1
Bug 1273229 - auto-generate IDs for temp installs from dir path. r=kmag MozReview-Commit-ID: JFuEVUZs2Ci
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -239,16 +239,23 @@ const RESTARTLESS_TYPES = new Set([
 ]);
 
 const SIGNED_TYPES = new Set([
   "webextension",
   "extension",
   "experiment",
 ]);
 
+// This is a random number array that can be used as "salt" when generating
+// an automatic ID based on the directory path of an add-on. It will prevent
+// someone from creating an ID for a permanent add-on that could be replaced
+// by a temporary add-on (because that would be confusing, I guess).
+const TEMP_INSTALL_ID_GEN_SESSION =
+  new Uint8Array(Float64Array.of(Math.random()).buffer);
+
 // Whether add-on signing is required.
 function mustSign(aType) {
   if (!SIGNED_TYPES.has(aType))
     return false;
   return REQUIRE_SIGNING || Preferences.get(PREF_XPI_SIGNATURES_REQUIRED, false);
 }
 
 // Keep track of where we are in startup for telemetry
@@ -1324,21 +1331,29 @@ var loadManifestFromDir = Task.async(fun
 
   let uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
 
   let addon;
   if (file.leafName == FILE_WEB_MANIFEST) {
     addon = yield loadManifestFromWebManifest(uri);
     if (!addon.id) {
       if (aInstallLocation == TemporaryInstallLocation) {
-        let id = Cc["@mozilla.org/uuid-generator;1"]
-            .getService(Ci.nsIUUIDGenerator)
-            .generateUUID().toString();
-        logger.info(`Generated temporary id ${id} for ${aDir.path}`);
-        addon.id = id;
+        // Generate a unique ID based on the directory path of
+        // this temporary add-on location.
+        const hasher = Cc["@mozilla.org/security/hash;1"]
+          .createInstance(Ci.nsICryptoHash);
+        hasher.init(hasher.SHA1);
+        const data = new TextEncoder().encode(aDir.path);
+        // Make it so this ID cannot be guessed.
+        const sess = TEMP_INSTALL_ID_GEN_SESSION;
+        hasher.update(sess, sess.length);
+        hasher.update(data, data.length);
+        addon.id = `${getHashStringForCrypto(hasher)}@temporary-addon`;
+        logger.info(
+          `Generated temp id ${addon.id} (${sess.join("")}) for ${aDir.path}`);
       } else {
         addon.id = aDir.leafName;
       }
     }
   } else {
     addon = loadFromRDF(uri);
   }
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
@@ -41,16 +41,81 @@ add_task(function* test_implicit_id_temp
   yield AddonManager.installTemporaryAddon(xpifile);
 
   addon = yield promiseAddonByID(IMPLICIT_ID_ID);
   do_check_neq(addon, null);
 
   addon.uninstall();
 });
 
+// We should be able to temporarily install an unsigned web extension
+// that does not have an ID in its manifest.
+add_task(function* test_unsigned_no_id_temp_install() {
+  if (!TEST_UNPACKED) {
+    do_print("This test does not apply when using packed extensions");
+    return;
+  }
+  const manifest = {
+    name: "no ID",
+    description: "extension without an ID",
+    manifest_version: 2,
+    version: "1.0"
+  };
+
+  const addonDir = writeWebManifestForExtension(manifest, gTmpD,
+                                                "the-addon-sub-dir");
+  const addon = yield AddonManager.installTemporaryAddon(addonDir);
+  ok(addon.id, "ID should have been auto-generated");
+
+  // Install the same directory again, as if re-installing or reloading.
+  const secondAddon = yield AddonManager.installTemporaryAddon(addonDir);
+  // The IDs should be the same.
+  equal(secondAddon.id, addon.id);
+
+  secondAddon.uninstall();
+  addonDir.remove(true);
+});
+
+// We should be able to install two extensions from manifests without IDs
+// at different locations and get two unique extensions.
+add_task(function* test_multiple_no_id_extensions() {
+  if (!TEST_UNPACKED) {
+    do_print("This test does not apply when using packed extensions");
+    return;
+  }
+  const manifest = {
+    name: "no ID",
+    description: "extension without an ID",
+    manifest_version: 2,
+    version: "1.0"
+  };
+
+  const firstAddonDir = writeWebManifestForExtension(manifest, gTmpD,
+                                                     "addon-sub-dir-one");
+  const secondAddonDir = writeWebManifestForExtension(manifest, gTmpD,
+                                                      "addon-sub-dir-two");
+  const [firstAddon, secondAddon] = yield Promise.all([
+    AddonManager.installTemporaryAddon(firstAddonDir),
+    AddonManager.installTemporaryAddon(secondAddonDir)
+  ]);
+
+  const allAddons = yield new Promise(resolve => {
+    AddonManager.getAllAddons(addons => resolve(addons));
+  });
+  do_print(`Found these add-ons: ${allAddons.map(a => a.name).join(", ")}`);
+  const filtered = allAddons.filter(addon => addon.name === manifest.name);
+  // Make sure we have two add-ons by the same name.
+  equal(filtered.length, 2);
+
+  firstAddon.uninstall();
+  firstAddonDir.remove(true);
+  secondAddon.uninstall();
+  secondAddonDir.remove(true);
+});
+
 // Test that we can get the ID from browser_specific_settings
 add_task(function* test_bss_id() {
   const ID = "webext_bss_id@tests.mozilla.org";
 
   let manifest = {
     name: "bss test",
     description: "test that ID may be in browser_specific_settings",
     manifest_version: 2,