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 }