Commit c708afd0 authored by Rob Wu's avatar Rob Wu
Browse files

Bug 1752979 - Check damaged files. r=mixedpuppy, a=RyanVM

parent 78c9fb22
Loading
Loading
Loading
Loading
+78 −35
Original line number Diff line number Diff line
@@ -802,6 +802,17 @@ function computeSha256HashAsString(input) {
  return getHashStringForCrypto(crypto);
}

function getHashForFile(file, algorithm) {
  let crypto = CryptoHash(algorithm);
  let fis = new FileInputStream(file, -1, -1, false);
  try {
    crypto.updateFromStream(fis, file.fileSize);
  } finally {
    fis.close();
  }
  return getHashStringForCrypto(crypto);
}

/**
 * Returns the signedState for a given return code and certificate by verifying
 * it against the expected ID.
@@ -1230,6 +1241,9 @@ function getHashStringForCrypto(aCrypto) {
  return hash.join("").toLowerCase();
}

// A hash algorithm if the caller of AddonInstall did not specify one.
const DEFAULT_HASH_ALGO = "sha256";

/**
 * Base class for objects that manage the installation of an addon.
 * This class isn't instantiated directly, see the derived classes below.
@@ -1280,6 +1294,7 @@ class AddonInstall {
      };
    }
    this.hash = this.originalHash;
    this.fileHash = null;
    this.existingAddon = options.existingAddon || null;
    this.promptHandler = options.promptHandler || (() => Promise.resolve());
    this.releaseNotesURI = options.releaseNotesURI || null;
@@ -1506,6 +1521,18 @@ class AddonInstall {
    }
  }

  _setFileHash(calculatedHash) {
    this.fileHash = {
      algorithm: this.hash ? this.hash.algorithm : DEFAULT_HASH_ALGO,
      data: calculatedHash,
    };

    if (this.hash && calculatedHash != this.hash.data) {
      return false;
    }
    return true;
  }

  /**
   * Updates the addon metadata that has to be propagated across restarts.
   */
@@ -1864,6 +1891,7 @@ class AddonInstall {
      }
      this.state = AddonManager.STATE_INSTALL_FAILED;
      this.error = AddonManager.ERROR_FILE_ACCESS;
      this._cleanup();
      AddonManagerPrivate.callAddonListeners(
        "onOperationCancelled",
        this.addon.wrapper
@@ -1881,18 +1909,32 @@ class AddonInstall {
   * @param {boolean} restartRequired
   *        If true, the final installation will be deferred until the
   *        next app startup.
   * @param {AddonInternal} stagedAddon
   *        The AddonInternal object for the staged install.
   * @param {nsIFile} stagedAddon
   *        The file where the add-on should be staged.
   * @param {boolean} isSameLocation
   *        True if this installation is an upgrade for an existing
   *        add-on in the same location.
   * @throws if the file cannot be staged.
   */
  async stageInstall(restartRequired, stagedAddon, isSameLocation) {
    logger.debug(`Addon ${this.addon.id} will be installed as a packed xpi`);
    stagedAddon.leafName = `${this.addon.id}.xpi`;

    try {
      await OS.File.copy(this.file.path, stagedAddon.path);

      let calculatedHash = getHashForFile(stagedAddon, this.fileHash.algorithm);
      if (calculatedHash != this.fileHash.data) {
        logger.warn(
          `Staged file hash (${calculatedHash}) did not match initial hash (${this.fileHash.data})`
        );
        throw new Error("Refusing to stage add-on because it has been damaged");
      }
    } catch (e) {
      await OS.File.remove(stagedAddon.path, { ignoreAbsent: true });
      throw e;
    }

    if (restartRequired) {
      // Point the add-on to its extracted files as the xpi may get deleted
      this.addon.sourceBundle = stagedAddon;
@@ -1935,12 +1977,23 @@ class AddonInstall {

    let stagingDir = this.location.installer.getStagingDir();

    try {
      await this.location.installer.requestStagingDir();
      await this.unstageInstall(stagingDir);

      let stagedAddon = getFile(`${this.addon.id}.xpi`, stagingDir);

      await this.stageInstall(true, stagedAddon, true);
    } catch (e) {
      logger.warn(`Failed to postpone install of ${this.addon.id}`, e);
      this.state = AddonManager.STATE_INSTALL_FAILED;
      this.error = AddonManager.ERROR_FILE_ACCESS;
      this._cleanup();
      this.removeTemporaryFile();
      this.location.installer.releaseStagingDir();
      this._callInstallListeners("onInstallFailed");
      return;
    }

    this._callInstallListeners("onInstallPostponed");

@@ -2016,10 +2069,10 @@ var LocalAddonInstall = class extends AddonInstall {
    this.progress = this.file.fileSize;
    this.maxProgress = this.file.fileSize;

    let algorithm = this.hash ? this.hash.algorithm : DEFAULT_HASH_ALGO;
    if (this.hash) {
      let crypto;
      try {
        crypto = CryptoHash(this.hash.algorithm);
        CryptoHash(this.hash.algorithm);
      } catch (e) {
        logger.warn(
          "Unknown hash algorithm '" +
@@ -2033,24 +2086,17 @@ var LocalAddonInstall = class extends AddonInstall {
        this._cleanup();
        return;
      }
    }

      let fis = new FileInputStream(this.file, -1, -1, false);
      crypto.updateFromStream(fis, this.file.fileSize);
      let calculatedHash = getHashStringForCrypto(crypto);
      if (calculatedHash != this.hash.data) {
    if (!this._setFileHash(getHashForFile(this.file, algorithm))) {
      logger.warn(
          "File hash (" +
            calculatedHash +
            ") did not match provided hash (" +
            this.hash.data +
            ")"
        `File hash (${this.fileHash.data}) did not match provided hash (${this.hash.data})`
      );
      this.state = AddonManager.STATE_DOWNLOAD_FAILED;
      this.error = AddonManager.ERROR_INCORRECT_HASH;
      this._cleanup();
      return;
    }
    }

    try {
      await this.loadManifest(this.file);
@@ -2182,6 +2228,7 @@ var DownloadAddonInstall = class extends AddonInstall {
        this.progress = 0;
        this.maxProgress = -1;
        this.hash = this.originalHash;
        this.fileHash = null;
        this.startDownload();
        break;
      default:
@@ -2382,7 +2429,7 @@ var DownloadAddonInstall = class extends AddonInstall {
    } else {
      // We always need something to consume data from the inputstream passed
      // to onDataAvailable so just create a dummy cryptohasher to do that.
      this.crypto = CryptoHash("sha1");
      this.crypto = CryptoHash(DEFAULT_HASH_ALGO);
    }

    this.progress = 0;
@@ -2410,6 +2457,9 @@ var DownloadAddonInstall = class extends AddonInstall {
    this.badCerthandler = null;
    Services.obs.removeObserver(this, "network:offline-about-to-go-offline");

    let crypto = this.crypto;
    this.crypto = null;

    // If the download was cancelled then update the state and send events
    if (aStatus == Cr.NS_BINDING_ABORTED) {
      if (this.state == AddonManager.STATE_DOWNLOADING) {
@@ -2450,17 +2500,10 @@ var DownloadAddonInstall = class extends AddonInstall {
          }
        }

        // convert the binary hash data to a hex string.
        let calculatedHash = getHashStringForCrypto(this.crypto);
        this.crypto = null;
        if (this.hash && calculatedHash != this.hash.data) {
        if (!this._setFileHash(getHashStringForCrypto(crypto))) {
          this.downloadFailed(
            AddonManager.ERROR_INCORRECT_HASH,
            "Downloaded file hash (" +
              calculatedHash +
              ") did not match provided hash (" +
              this.hash.data +
              ")"
            `Downloaded file hash (${this.fileHash.data}) did not match provided hash (${this.hash.data})`
          );
          return;
        }