diff options
Diffstat (limited to 'services/common/blocklist-clients.js')
-rw-r--r-- | services/common/blocklist-clients.js | 310 |
1 files changed, 0 insertions, 310 deletions
diff --git a/services/common/blocklist-clients.js b/services/common/blocklist-clients.js deleted file mode 100644 index fc51aaca4..000000000 --- a/services/common/blocklist-clients.js +++ /dev/null @@ -1,310 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["AddonBlocklistClient", - "GfxBlocklistClient", - "OneCRLBlocklistClient", - "PluginBlocklistClient", - "FILENAME_ADDONS_JSON", - "FILENAME_GFX_JSON", - "FILENAME_PLUGINS_JSON"]; - -const { classes: Cc, interfaces: Ci, utils: Cu } = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -const { Task } = Cu.import("resource://gre/modules/Task.jsm"); -const { OS } = Cu.import("resource://gre/modules/osfile.jsm"); -Cu.importGlobalProperties(["fetch"]); - -const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js"); -const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js"); -const { CanonicalJSON } = Components.utils.import("resource://gre/modules/CanonicalJSON.jsm"); - -const PREF_SETTINGS_SERVER = "services.settings.server"; -const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket"; -const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection"; -const PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS = "services.blocklist.onecrl.checked"; -const PREF_BLOCKLIST_ADDONS_COLLECTION = "services.blocklist.addons.collection"; -const PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS = "services.blocklist.addons.checked"; -const PREF_BLOCKLIST_PLUGINS_COLLECTION = "services.blocklist.plugins.collection"; -const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS = "services.blocklist.plugins.checked"; -const PREF_BLOCKLIST_GFX_COLLECTION = "services.blocklist.gfx.collection"; -const PREF_BLOCKLIST_GFX_CHECKED_SECONDS = "services.blocklist.gfx.checked"; -const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced"; - -const INVALID_SIGNATURE = "Invalid content/signature"; - -this.FILENAME_ADDONS_JSON = "blocklist-addons.json"; -this.FILENAME_GFX_JSON = "blocklist-gfx.json"; -this.FILENAME_PLUGINS_JSON = "blocklist-plugins.json"; - -function mergeChanges(localRecords, changes) { - // Kinto.js adds attributes to local records that aren't present on server. - // (e.g. _status) - const stripPrivateProps = (obj) => { - return Object.keys(obj).reduce((current, key) => { - if (!key.startsWith("_")) { - current[key] = obj[key]; - } - return current; - }, {}); - }; - - const records = {}; - // Local records by id. - localRecords.forEach((record) => records[record.id] = stripPrivateProps(record)); - // All existing records are replaced by the version from the server. - changes.forEach((record) => records[record.id] = record); - - return Object.values(records) - // Filter out deleted records. - .filter((record) => record.deleted != true) - // Sort list by record id. - .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0); -} - - -function fetchCollectionMetadata(collection) { - const client = new KintoHttpClient(collection.api.remote); - return client.bucket(collection.bucket).collection(collection.name).getData() - .then(result => { - return result.signature; - }); -} - -function fetchRemoteCollection(collection) { - const client = new KintoHttpClient(collection.api.remote); - return client.bucket(collection.bucket) - .collection(collection.name) - .listRecords({sort: "id"}); -} - -/** - * Helper to instantiate a Kinto client based on preferences for remote server - * URL and bucket name. It uses the `FirefoxAdapter` which relies on SQLite to - * persist the local DB. - */ -function kintoClient() { - let base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER); - let bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET); - - let Kinto = loadKinto(); - - let FirefoxAdapter = Kinto.adapters.FirefoxAdapter; - - let config = { - remote: base, - bucket: bucket, - adapter: FirefoxAdapter, - }; - - return new Kinto(config); -} - - -class BlocklistClient { - - constructor(collectionName, lastCheckTimePref, processCallback, signerName) { - this.collectionName = collectionName; - this.lastCheckTimePref = lastCheckTimePref; - this.processCallback = processCallback; - this.signerName = signerName; - } - - validateCollectionSignature(payload, collection, ignoreLocal) { - return Task.spawn((function* () { - // this is a content-signature field from an autograph response. - const {x5u, signature} = yield fetchCollectionMetadata(collection); - const certChain = yield fetch(x5u).then((res) => res.text()); - - const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"] - .createInstance(Ci.nsIContentSignatureVerifier); - - let toSerialize; - if (ignoreLocal) { - toSerialize = { - last_modified: `${payload.last_modified}`, - data: payload.data - }; - } else { - const localRecords = (yield collection.list()).data; - const records = mergeChanges(localRecords, payload.changes); - toSerialize = { - last_modified: `${payload.lastModified}`, - data: records - }; - } - - const serialized = CanonicalJSON.stringify(toSerialize); - - if (verifier.verifyContentSignature(serialized, "p384ecdsa=" + signature, - certChain, - this.signerName)) { - // In case the hash is valid, apply the changes locally. - return payload; - } - throw new Error(INVALID_SIGNATURE); - }).bind(this)); - } - - /** - * Synchronize from Kinto server, if necessary. - * - * @param {int} lastModified the lastModified date (on the server) for - the remote collection. - * @param {Date} serverTime the current date return by the server. - * @return {Promise} which rejects on sync or process failure. - */ - maybeSync(lastModified, serverTime) { - let db = kintoClient(); - let opts = {}; - let enforceCollectionSigning = - Services.prefs.getBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING); - - // if there is a signerName and collection signing is enforced, add a - // hook for incoming changes that validates the signature - if (this.signerName && enforceCollectionSigning) { - opts.hooks = { - "incoming-changes": [this.validateCollectionSignature.bind(this)] - } - } - - let collection = db.collection(this.collectionName, opts); - - return Task.spawn((function* syncCollection() { - try { - yield collection.db.open(); - - let collectionLastModified = yield collection.db.getLastModified(); - // If the data is up to date, there's no need to sync. We still need - // to record the fact that a check happened. - if (lastModified <= collectionLastModified) { - this.updateLastCheck(serverTime); - return; - } - // Fetch changes from server. - try { - let syncResult = yield collection.sync(); - if (!syncResult.ok) { - throw new Error("Sync failed"); - } - } catch (e) { - if (e.message == INVALID_SIGNATURE) { - // if sync fails with a signature error, it's likely that our - // local data has been modified in some way. - // We will attempt to fix this by retrieving the whole - // remote collection. - let payload = yield fetchRemoteCollection(collection); - yield this.validateCollectionSignature(payload, collection, true); - // if the signature is good (we haven't thrown), and the remote - // last_modified is newer than the local last_modified, replace the - // local data - const localLastModified = yield collection.db.getLastModified(); - if (payload.last_modified >= localLastModified) { - yield collection.clear(); - yield collection.loadDump(payload.data); - } - } else { - throw e; - } - } - // Read local collection of records. - let list = yield collection.list(); - - yield this.processCallback(list.data); - - // Track last update. - this.updateLastCheck(serverTime); - } finally { - collection.db.close(); - } - }).bind(this)); - } - - /** - * Save last time server was checked in users prefs. - * - * @param {Date} serverTime the current date return by server. - */ - updateLastCheck(serverTime) { - let checkedServerTimeInSeconds = Math.round(serverTime / 1000); - Services.prefs.setIntPref(this.lastCheckTimePref, checkedServerTimeInSeconds); - } -} - -/** - * Revoke the appropriate certificates based on the records from the blocklist. - * - * @param {Object} records current records in the local db. - */ -function* updateCertBlocklist(records) { - let certList = Cc["@mozilla.org/security/certblocklist;1"] - .getService(Ci.nsICertBlocklist); - for (let item of records) { - try { - if (item.issuerName && item.serialNumber) { - certList.revokeCertByIssuerAndSerial(item.issuerName, - item.serialNumber); - } else if (item.subject && item.pubKeyHash) { - certList.revokeCertBySubjectAndPubKey(item.subject, - item.pubKeyHash); - } - } catch (e) { - // prevent errors relating to individual blocklist entries from - // causing sync to fail. At some point in the future, we may want to - // accumulate telemetry on these failures. - Cu.reportError(e); - } - } - certList.saveEntries(); -} - -/** - * Write list of records into JSON file, and notify nsBlocklistService. - * - * @param {String} filename path relative to profile dir. - * @param {Object} records current records in the local db. - */ -function* updateJSONBlocklist(filename, records) { - // Write JSON dump for synchronous load at startup. - const path = OS.Path.join(OS.Constants.Path.profileDir, filename); - const serialized = JSON.stringify({data: records}, null, 2); - try { - yield OS.File.writeAtomic(path, serialized, {tmpPath: path + ".tmp"}); - - // Notify change to `nsBlocklistService` - const eventData = {filename: filename}; - Services.cpmm.sendAsyncMessage("Blocklist:reload-from-disk", eventData); - } catch(e) { - Cu.reportError(e); - } -} - - -this.OneCRLBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION), - PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS, - updateCertBlocklist, - "onecrl.content-signature.mozilla.org" -); - -this.AddonBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION), - PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS, - updateJSONBlocklist.bind(undefined, FILENAME_ADDONS_JSON) -); - -this.GfxBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION), - PREF_BLOCKLIST_GFX_CHECKED_SECONDS, - updateJSONBlocklist.bind(undefined, FILENAME_GFX_JSON) -); - -this.PluginBlocklistClient = new BlocklistClient( - Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION), - PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS, - updateJSONBlocklist.bind(undefined, FILENAME_PLUGINS_JSON) -); |