tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

RemoteSecuritySettings.sys.mjs (23264B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { RemoteSettings } from "resource://services-settings/remote-settings.sys.mjs";
      6 
      7 import { X509 } from "resource://gre/modules/psm/X509.sys.mjs";
      8 
      9 const SECURITY_STATE_BUCKET = "security-state";
     10 const SECURITY_STATE_SIGNER = "onecrl.content-signature.mozilla.org";
     11 
     12 const INTERMEDIATES_DL_PER_POLL_PREF =
     13  "security.remote_settings.intermediates.downloads_per_poll";
     14 const INTERMEDIATES_DL_PARALLEL_REQUESTS =
     15  "security.remote_settings.intermediates.parallel_downloads";
     16 const INTERMEDIATES_ENABLED_PREF =
     17  "security.remote_settings.intermediates.enabled";
     18 const LOGLEVEL_PREF = "browser.policies.loglevel";
     19 
     20 const CRLITE_FILTERS_ENABLED_PREF =
     21  "security.remote_settings.crlite_filters.enabled";
     22 
     23 const CRLITE_FILTER_CHANNEL_PREF = "security.pki.crlite_channel";
     24 
     25 const lazy = {};
     26 
     27 ChromeUtils.defineLazyGetter(lazy, "gTextDecoder", () => new TextDecoder());
     28 
     29 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     30  let { ConsoleAPI } = ChromeUtils.importESModule(
     31    "resource://gre/modules/Console.sys.mjs"
     32  );
     33  return new ConsoleAPI({
     34    prefix: "RemoteSecuritySettings",
     35    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
     36    // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
     37    maxLogLevel: "error",
     38    maxLogLevelPref: LOGLEVEL_PREF,
     39  });
     40 });
     41 ChromeUtils.defineESModuleGetters(lazy, {
     42  RemoteSettingsClient:
     43    "resource://services-settings/RemoteSettingsClient.sys.mjs",
     44 });
     45 
     46 // Converts a JS string to an array of bytes consisting of the char code at each
     47 // index in the string.
     48 function stringToBytes(s) {
     49  let b = [];
     50  for (let i = 0; i < s.length; i++) {
     51    b.push(s.charCodeAt(i));
     52  }
     53  return b;
     54 }
     55 
     56 // Converts an array of bytes to a JS string using fromCharCode on each byte.
     57 function bytesToString(bytes) {
     58  if (bytes.length > 65535) {
     59    throw new Error("input too long for bytesToString");
     60  }
     61  return String.fromCharCode.apply(null, bytes);
     62 }
     63 
     64 class CertInfo {
     65  constructor(cert, subject) {
     66    this.cert = cert;
     67    this.subject = subject;
     68    this.trust = Ci.nsICertStorage.TRUST_INHERIT;
     69  }
     70 }
     71 CertInfo.prototype.QueryInterface = ChromeUtils.generateQI(["nsICertInfo"]);
     72 
     73 class RevocationState {
     74  constructor(state) {
     75    this.state = state;
     76  }
     77 }
     78 
     79 class IssuerAndSerialRevocationState extends RevocationState {
     80  constructor(issuer, serial, state) {
     81    super(state);
     82    this.issuer = issuer;
     83    this.serial = serial;
     84  }
     85 }
     86 IssuerAndSerialRevocationState.prototype.QueryInterface =
     87  ChromeUtils.generateQI(["nsIIssuerAndSerialRevocationState"]);
     88 
     89 class SubjectAndPubKeyRevocationState extends RevocationState {
     90  constructor(subject, pubKey, state) {
     91    super(state);
     92    this.subject = subject;
     93    this.pubKey = pubKey;
     94  }
     95 }
     96 SubjectAndPubKeyRevocationState.prototype.QueryInterface =
     97  ChromeUtils.generateQI(["nsISubjectAndPubKeyRevocationState"]);
     98 
     99 function setRevocations(certStorage, revocations) {
    100  return new Promise(resolve =>
    101    certStorage.setRevocations(revocations, resolve)
    102  );
    103 }
    104 
    105 /**
    106 * Helper function that returns a promise that will resolve with whether or not
    107 * the nsICertStorage implementation has prior data of the given type.
    108 *
    109 * @param {Integer} dataType a Ci.nsICertStorage.DATA_TYPE_* constant
    110 *                           indicating the type of data
    111 
    112 * @returns {Promise} a promise that will resolve with true if the data type is
    113 *                   present
    114 */
    115 function hasPriorData(dataType) {
    116  let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
    117    Ci.nsICertStorage
    118  );
    119  return new Promise(resolve => {
    120    certStorage.hasPriorData(dataType, (rv, out) => {
    121      if (rv == Cr.NS_OK) {
    122        resolve(out);
    123      } else {
    124        // If calling hasPriorData failed, assume we need to reload everything
    125        // (even though it's unlikely doing so will succeed).
    126        resolve(false);
    127      }
    128    });
    129  });
    130 }
    131 
    132 /**
    133 * Revoke the appropriate certificates based on the records from the blocklist.
    134 *
    135 * @param {object} options
    136 * @param {object} options.data Current records in the local db.
    137 * @param {Array} options.data.current
    138 * @param {Array} options.data.created
    139 * @param {Array} options.data.updated
    140 * @param {Array} options.data.deleted
    141 */
    142 const updateCertBlocklist = async function ({
    143  data: { current, created, updated, deleted },
    144 }) {
    145  let items = [];
    146 
    147  // See if we have prior revocation data (this can happen when we can't open
    148  // the database and we have to re-create it (see bug 1546361)).
    149  let hasPriorRevocationData = await hasPriorData(
    150    Ci.nsICertStorage.DATA_TYPE_REVOCATION
    151  );
    152 
    153  // If we don't have prior data, make it so we re-load everything.
    154  if (!hasPriorRevocationData) {
    155    deleted = [];
    156    updated = [];
    157    created = current;
    158  }
    159 
    160  let toDelete = deleted.concat(updated.map(u => u.old));
    161  for (let item of toDelete) {
    162    if (item.issuerName && item.serialNumber) {
    163      items.push(
    164        new IssuerAndSerialRevocationState(
    165          item.issuerName,
    166          item.serialNumber,
    167          Ci.nsICertStorage.STATE_UNSET
    168        )
    169      );
    170    } else if (item.subject && item.pubKeyHash) {
    171      items.push(
    172        new SubjectAndPubKeyRevocationState(
    173          item.subject,
    174          item.pubKeyHash,
    175          Ci.nsICertStorage.STATE_UNSET
    176        )
    177      );
    178    }
    179  }
    180 
    181  const toAdd = created.concat(updated.map(u => u.new));
    182 
    183  for (let item of toAdd) {
    184    if (item.issuerName && item.serialNumber) {
    185      items.push(
    186        new IssuerAndSerialRevocationState(
    187          item.issuerName,
    188          item.serialNumber,
    189          Ci.nsICertStorage.STATE_ENFORCE
    190        )
    191      );
    192    } else if (item.subject && item.pubKeyHash) {
    193      items.push(
    194        new SubjectAndPubKeyRevocationState(
    195          item.subject,
    196          item.pubKeyHash,
    197          Ci.nsICertStorage.STATE_ENFORCE
    198        )
    199      );
    200    }
    201  }
    202 
    203  try {
    204    const certList = Cc["@mozilla.org/security/certstorage;1"].getService(
    205      Ci.nsICertStorage
    206    );
    207    await setRevocations(certList, items);
    208  } catch (e) {
    209    lazy.log.error(e);
    210  }
    211 };
    212 
    213 export var RemoteSecuritySettings = {
    214  _initialized: false,
    215  OneCRLBlocklistClient: null,
    216  IntermediatePreloadsClient: null,
    217  CRLiteFiltersClient: null,
    218 
    219  /**
    220   * Initialize the clients (cheap instantiation) and setup their sync event.
    221   * This static method is called from BrowserGlue.sys.mjs soon after startup.
    222   *
    223   * @returns {object} instantiated clients for security remote settings.
    224   */
    225  init() {
    226    // Avoid repeated initialization (work-around for bug 1730026).
    227    if (this._initialized) {
    228      return this;
    229    }
    230    this._initialized = true;
    231 
    232    this.OneCRLBlocklistClient = RemoteSettings("onecrl", {
    233      bucketName: SECURITY_STATE_BUCKET,
    234      signerName: SECURITY_STATE_SIGNER,
    235    });
    236    this.OneCRLBlocklistClient.on("sync", updateCertBlocklist);
    237 
    238    this.IntermediatePreloadsClient = new IntermediatePreloads();
    239 
    240    this.CRLiteFiltersClient = new CRLiteFilters();
    241 
    242    return this;
    243  },
    244 };
    245 
    246 class IntermediatePreloads {
    247  constructor() {
    248    this.maybeInit();
    249  }
    250 
    251  maybeInit() {
    252    if (
    253      this.client ||
    254      !Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)
    255    ) {
    256      return;
    257    }
    258    this.client = RemoteSettings("intermediates", {
    259      bucketName: SECURITY_STATE_BUCKET,
    260      signerName: SECURITY_STATE_SIGNER,
    261      localFields: ["cert_import_complete"],
    262    });
    263 
    264    this.client.on("sync", this.onSync.bind(this));
    265    Services.obs.addObserver(
    266      this.onObservePollEnd.bind(this),
    267      "remote-settings:changes-poll-end"
    268    );
    269 
    270    lazy.log.debug("Intermediate Preloading: constructor");
    271  }
    272 
    273  async updatePreloadedIntermediates() {
    274    if (!Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)) {
    275      lazy.log.debug("Intermediate Preloading is disabled");
    276      Services.obs.notifyObservers(
    277        null,
    278        "remote-security-settings:intermediates-updated",
    279        "disabled"
    280      );
    281      return;
    282    }
    283    this.maybeInit();
    284 
    285    // Download attachments that are awaiting download, up to a max.
    286    const maxDownloadsPerRun = Services.prefs.getIntPref(
    287      INTERMEDIATES_DL_PER_POLL_PREF,
    288      100
    289    );
    290    const parallelDownloads = Services.prefs.getIntPref(
    291      INTERMEDIATES_DL_PARALLEL_REQUESTS,
    292      8
    293    );
    294 
    295    // Bug 1519256: Move this to a separate method that's on a separate timer
    296    // with a higher frequency (so we can attempt to download outstanding
    297    // certs more than once daily)
    298 
    299    // See if we have prior cert data (this can happen when we can't open the database and we
    300    // have to re-create it (see bug 1546361)).
    301    let hasPriorCertData = await hasPriorData(
    302      Ci.nsICertStorage.DATA_TYPE_CERTIFICATE
    303    );
    304    // If we don't have prior data, make it so we re-load everything.
    305    if (!hasPriorCertData) {
    306      let current = [];
    307      try {
    308        current = await this.client.db.list();
    309      } catch (err) {
    310        if (!(err instanceof lazy.RemoteSettingsClient.EmptyDatabaseError)) {
    311          lazy.log.warn(
    312            `Unable to list intermediate preloading collection: ${err}`
    313          );
    314          return;
    315        }
    316      }
    317      const toReset = current.filter(record => record.cert_import_complete);
    318      try {
    319        await this.client.db.importChanges(
    320          undefined, // do not touch metadata.
    321          undefined, // do not touch collection timestamp.
    322          toReset.map(r => ({ ...r, cert_import_complete: false }))
    323        );
    324      } catch (err) {
    325        lazy.log.warn(
    326          `Unable to update intermediate preloading collection: ${err}`
    327        );
    328        return;
    329      }
    330    }
    331 
    332    try {
    333      // fetches a bundle containing all attachments, download() is called further down to force a re-sync on hash mismatches for old data or if the bundle fails to download
    334      await this.client.attachments.cacheAll();
    335    } catch (err) {
    336      lazy.log.warn(
    337        `Error fetching/caching attachment bundle in intermediate preloading: ${err}`
    338      );
    339    }
    340 
    341    let current = [];
    342    try {
    343      current = await this.client.db.list();
    344    } catch (err) {
    345      if (!(err instanceof lazy.RemoteSettingsClient.EmptyDatabaseError)) {
    346        lazy.log.warn(
    347          `Unable to list intermediate preloading collection: ${err}`
    348        );
    349        return;
    350      }
    351    }
    352    const waiting = current.filter(record => !record.cert_import_complete);
    353 
    354    lazy.log.debug(
    355      `There are ${waiting.length} intermediates awaiting download.`
    356    );
    357    if (!waiting.length) {
    358      // Nothing to do.
    359      Services.obs.notifyObservers(
    360        null,
    361        "remote-security-settings:intermediates-updated",
    362        "success"
    363      );
    364      return;
    365    }
    366 
    367    let toDownload = waiting.slice(0, maxDownloadsPerRun);
    368    let recordsCertsAndSubjects = [];
    369    for (let i = 0; i < toDownload.length; i += parallelDownloads) {
    370      const chunk = toDownload.slice(i, i + parallelDownloads);
    371      const downloaded = await Promise.all(
    372        chunk.map(record => this.maybeDownloadAttachment(record))
    373      );
    374      recordsCertsAndSubjects = recordsCertsAndSubjects.concat(downloaded);
    375    }
    376 
    377    let certInfos = [];
    378    let recordsToUpdate = [];
    379    for (let { record, cert, subject } of recordsCertsAndSubjects) {
    380      if (cert && subject) {
    381        certInfos.push(new CertInfo(cert, subject));
    382        recordsToUpdate.push(record);
    383      }
    384    }
    385    const certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
    386      Ci.nsICertStorage
    387    );
    388    let result = await new Promise(resolve => {
    389      certStorage.addCerts(certInfos, resolve);
    390    }).catch(err => err);
    391    if (result != Cr.NS_OK) {
    392      lazy.log.error(`certStorage.addCerts failed: ${result}`);
    393      return;
    394    }
    395    try {
    396      await this.client.db.importChanges(
    397        undefined, // do not touch metadata.
    398        undefined, // do not touch collection timestamp.
    399        recordsToUpdate.map(r => ({ ...r, cert_import_complete: true }))
    400      );
    401    } catch (err) {
    402      lazy.log.warn(
    403        `Unable to update intermediate preloading collection: ${err}`
    404      );
    405      return;
    406    }
    407 
    408    // attachment cache is no longer needed
    409    await this.client.attachments.deleteAll();
    410 
    411    Services.obs.notifyObservers(
    412      null,
    413      "remote-security-settings:intermediates-updated",
    414      "success"
    415    );
    416  }
    417 
    418  async onObservePollEnd(subject, topic) {
    419    lazy.log.debug(`onObservePollEnd ${subject} ${topic}`);
    420 
    421    try {
    422      await this.updatePreloadedIntermediates();
    423    } catch (err) {
    424      lazy.log.warn(`Unable to update intermediate preloads: ${err}`);
    425    }
    426  }
    427 
    428  // This method returns a promise to RemoteSettingsClient.maybeSync method.
    429  async onSync({ data: { deleted } }) {
    430    if (!Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)) {
    431      lazy.log.debug("Intermediate Preloading is disabled");
    432      return;
    433    }
    434 
    435    lazy.log.debug(`Removing ${deleted.length} Intermediate certificates`);
    436    await this.removeCerts(deleted);
    437  }
    438 
    439  /**
    440   * Attempts to download the attachment, assuming it's not been processed
    441   * already. Does not retry, and always resolves (e.g., does not reject upon
    442   * failure.) Errors are reported via console.error.
    443   *
    444   * @param  {AttachmentRecord} record defines which data to obtain
    445   * @returns {Promise}          a Promise that will resolve to an object with the properties
    446   *                            record, cert, and subject. record is the original record.
    447   *                            cert is the base64-encoded bytes of the downloaded certificate (if
    448   *                            downloading was successful), and null otherwise.
    449   *                            subject is the base64-encoded bytes of the subject distinguished
    450   *                            name of the same.
    451   */
    452  async maybeDownloadAttachment(record) {
    453    let result = { record, cert: null, subject: null };
    454 
    455    let dataAsString = null;
    456    try {
    457      let { buffer } = await this.client.attachments.download(record, {
    458        retries: 0,
    459        checkHash: true,
    460        cacheResult: false,
    461      });
    462      dataAsString = lazy.gTextDecoder.decode(new Uint8Array(buffer));
    463    } catch (err) {
    464      if (err.name == "BadContentError") {
    465        lazy.log.debug(`Bad attachment content.`);
    466      } else {
    467        lazy.log.error(`Failed to download attachment: ${err}`);
    468      }
    469      return result;
    470    }
    471 
    472    let certBase64;
    473    let subjectBase64;
    474    try {
    475      // split off the header and footer
    476      certBase64 = dataAsString.split("-----")[2].replace(/\s/g, "");
    477      // get an array of bytes so we can use X509.sys.mjs
    478      let certBytes = stringToBytes(atob(certBase64));
    479      let cert = new X509.Certificate();
    480      cert.parse(certBytes);
    481      // get the DER-encoded subject and get a base64-encoded string from it
    482      // TODO(bug 1542028): add getters for _der and _bytes
    483      subjectBase64 = btoa(
    484        bytesToString(cert.tbsCertificate.subject._der._bytes)
    485      );
    486    } catch (err) {
    487      lazy.log.error(`Failed to decode cert: ${err}`);
    488      return result;
    489    }
    490    result.cert = certBase64;
    491    result.subject = subjectBase64;
    492    return result;
    493  }
    494 
    495  async maybeSync(expectedTimestamp, options) {
    496    return this.client.maybeSync(expectedTimestamp, options);
    497  }
    498 
    499  async removeCerts(recordsToRemove) {
    500    let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
    501      Ci.nsICertStorage
    502    );
    503    let hashes = recordsToRemove.map(record => record.derHash);
    504    let result = await new Promise(resolve => {
    505      certStorage.removeCertsByHashes(hashes, resolve);
    506    }).catch(err => err);
    507    if (result != Cr.NS_OK) {
    508      lazy.log.error(`Failed to remove some intermediate certificates`);
    509    }
    510  }
    511 }
    512 
    513 // Helper function to compare filters. One filter is "less than" another filter (i.e. it sorts
    514 // earlier) if its timestamp is farther in the past than the other.
    515 function compareFilters(filterA, filterB) {
    516  return filterA.effectiveTimestamp - filterB.effectiveTimestamp;
    517 }
    518 
    519 class CRLiteFilters {
    520  constructor() {
    521    this.maybeInit();
    522  }
    523 
    524  maybeInit() {
    525    if (
    526      this.client ||
    527      !Services.prefs.getBoolPref(CRLITE_FILTERS_ENABLED_PREF, true)
    528    ) {
    529      return;
    530    }
    531    this.client = RemoteSettings("cert-revocations", {
    532      bucketName: SECURITY_STATE_BUCKET,
    533      signerName: SECURITY_STATE_SIGNER,
    534      localFields: ["loaded_into_cert_storage"],
    535    });
    536 
    537    Services.obs.addObserver(
    538      this.onObservePollEnd.bind(this),
    539      "remote-settings:changes-poll-end"
    540    );
    541    Services.prefs.addObserver(CRLITE_FILTER_CHANNEL_PREF, this);
    542  }
    543 
    544  async observe(subject, topic, prefName) {
    545    if (topic == "nsPref:changed" && prefName == CRLITE_FILTER_CHANNEL_PREF) {
    546      // When the user changes from channel A to channel B, mark the records
    547      // for channel A (and all other channels) with loaded_into_cert_storage =
    548      // false. If we don't do this, then the user will fail to reinstall the
    549      // channel A artifacts if they switch back to channel A.
    550      let records;
    551      try {
    552        records = await this.client.db.list();
    553      } catch (err) {
    554        if (err instanceof lazy.RemoteSettingsClient.EmptyDatabaseError) {
    555          // Likely during tests, less likely in production.
    556          return;
    557        }
    558        throw err;
    559      }
    560      let newChannel = Services.prefs.getStringPref(
    561        CRLITE_FILTER_CHANNEL_PREF,
    562        "none"
    563      );
    564      let toReset = records.filter(record => record.channel != newChannel);
    565      await this.client.db.importChanges(
    566        undefined, // do not touch metadata.
    567        undefined, // do not touch collection timestamp.
    568        toReset.map(r => ({ ...r, loaded_into_cert_storage: false }))
    569      );
    570    }
    571  }
    572 
    573  async getFilteredRecords() {
    574    let records = [];
    575    try {
    576      records = await this.client.db.list();
    577    } catch (err) {
    578      if (!(err instanceof lazy.RemoteSettingsClient.EmptyDatabaseError)) {
    579        throw err;
    580      }
    581    }
    582    records = await this.client._filterEntries(records);
    583    return records;
    584  }
    585 
    586  async onObservePollEnd() {
    587    if (!Services.prefs.getBoolPref(CRLITE_FILTERS_ENABLED_PREF, true)) {
    588      lazy.log.debug("CRLite filter downloading is disabled");
    589      Services.obs.notifyObservers(
    590        null,
    591        "remote-security-settings:crlite-filters-downloaded",
    592        "disabled"
    593      );
    594      return;
    595    }
    596 
    597    this.maybeInit();
    598 
    599    let hasPriorFilter = await hasPriorData(
    600      Ci.nsICertStorage.DATA_TYPE_CRLITE_FILTER_FULL
    601    );
    602    if (!hasPriorFilter) {
    603      let current = await this.getFilteredRecords();
    604      let toReset = current.filter(
    605        record => !record.incremental && record.loaded_into_cert_storage
    606      );
    607      await this.client.db.importChanges(
    608        undefined, // do not touch metadata.
    609        undefined, // do not touch collection timestamp.
    610        toReset.map(r => ({ ...r, loaded_into_cert_storage: false }))
    611      );
    612    }
    613    let hasPriorDelta = await hasPriorData(
    614      Ci.nsICertStorage.DATA_TYPE_CRLITE_FILTER_INCREMENTAL
    615    );
    616    if (!hasPriorDelta) {
    617      let current = await this.getFilteredRecords();
    618      let toReset = current.filter(
    619        record => record.incremental && record.loaded_into_cert_storage
    620      );
    621      await this.client.db.importChanges(
    622        undefined, // do not touch metadata.
    623        undefined, // do not touch collection timestamp.
    624        toReset.map(r => ({ ...r, loaded_into_cert_storage: false }))
    625      );
    626    }
    627 
    628    let current = await this.getFilteredRecords();
    629    let fullFilters = current.filter(filter => !filter.incremental);
    630    if (fullFilters.length < 1) {
    631      lazy.log.debug("no full CRLite filters to download?");
    632      Services.obs.notifyObservers(
    633        null,
    634        "remote-security-settings:crlite-filters-downloaded",
    635        "unavailable"
    636      );
    637      return;
    638    }
    639    fullFilters.sort(compareFilters);
    640    lazy.log.debug("fullFilters:", fullFilters);
    641    let fullFilter = fullFilters.pop(); // the most recent filter sorts last
    642    let incrementalFilters = current.filter(
    643      filter =>
    644        // Return incremental filters that are more recent than (i.e. sort later than) the full
    645        // filter.
    646        filter.incremental && compareFilters(filter, fullFilter) > 0
    647    );
    648    incrementalFilters.sort(compareFilters);
    649    // Map of id to filter where that filter's parent has the given id.
    650    let parentIdMap = {};
    651    for (let filter of incrementalFilters) {
    652      if (filter.parent in parentIdMap) {
    653        lazy.log.debug(`filter with parent id ${filter.parent} already seen?`);
    654      } else {
    655        parentIdMap[filter.parent] = filter;
    656      }
    657    }
    658    let filtersToDownload = [];
    659    let nextFilter = fullFilter;
    660    while (nextFilter) {
    661      filtersToDownload.push(nextFilter);
    662      nextFilter = parentIdMap[nextFilter.id];
    663    }
    664    const certList = Cc["@mozilla.org/security/certstorage;1"].getService(
    665      Ci.nsICertStorage
    666    );
    667    filtersToDownload = filtersToDownload.filter(
    668      filter => !filter.loaded_into_cert_storage
    669    );
    670    lazy.log.debug("filtersToDownload:", filtersToDownload);
    671    let filtersDownloaded = [];
    672    for (let filter of filtersToDownload) {
    673      try {
    674        let attachment = await this.client.attachments.downloadAsBytes(filter);
    675        let bytes = new Uint8Array(attachment);
    676        lazy.log.debug(
    677          `Downloaded ${filter.details.name}: ${bytes.length} bytes`
    678        );
    679        filter.bytes = bytes;
    680        filtersDownloaded.push(filter);
    681      } catch (e) {
    682        lazy.log.error("failed to download CRLite filter", e);
    683      }
    684    }
    685    let fullFiltersDownloaded = filtersDownloaded.filter(
    686      filter => !filter.incremental
    687    );
    688    if (fullFiltersDownloaded.length) {
    689      if (fullFiltersDownloaded.length > 1) {
    690        lazy.log.warn("trying to install more than one full CRLite filter?");
    691      }
    692      let filter = fullFiltersDownloaded[0];
    693 
    694      await new Promise(resolve => {
    695        certList.setFullCRLiteFilter(filter.bytes, rv => {
    696          lazy.log.debug(`setFullCRLiteFilter: ${rv}`);
    697          resolve();
    698        });
    699      });
    700    }
    701    let deltas = filtersDownloaded.filter(filter => filter.incremental);
    702    for (let filter of deltas) {
    703      lazy.log.debug(`adding delta update of size ${filter.bytes.length}`);
    704      await new Promise(resolve => {
    705        certList.addCRLiteDelta(
    706          filter.bytes,
    707          filter.attachment.filename,
    708          rv => {
    709            lazy.log.debug(`addCRLiteDelta: ${rv}`);
    710            resolve();
    711          }
    712        );
    713      });
    714    }
    715 
    716    for (let filter of filtersDownloaded) {
    717      delete filter.bytes;
    718    }
    719 
    720    await this.client.db.importChanges(
    721      undefined, // do not touch metadata.
    722      undefined, // do not touch collection timestamp.
    723      filtersDownloaded.map(r => ({ ...r, loaded_into_cert_storage: true }))
    724    );
    725 
    726    Services.obs.notifyObservers(
    727      null,
    728      "remote-security-settings:crlite-filters-downloaded",
    729      `finished;${filtersDownloaded
    730        .map(filter => filter.details.name)
    731        .join(",")}`
    732    );
    733  }
    734 }