diff options
author | Ruben Rodriguez <ruben@trisquel.info> | 2022-09-08 20:18:54 -0400 |
---|---|---|
committer | Ruben Rodriguez <ruben@trisquel.info> | 2022-09-08 20:18:54 -0400 |
commit | 5da28b0f8771834ae208d61431d632875e9f8e7d (patch) | |
tree | 688ecaff26197bad8abde617b4947b11d617309e /data/extensions/https-everywhere@eff.org/background-scripts | |
parent | 4a87716686104266a9cccc2d83cc249e312f3673 (diff) |
Updated extensions:
* Upgraded Privacy Redirect to 1.1.49 and configured to use the 10 most reliable invidious instances
* Removed ViewTube
* Added torproxy@icecat.gnu based on 'Proxy toggle' extension
* Added jShelter 0.11.1
* Upgraded LibreJS to 7.21.0
* Upgraded HTTPS Everywhere to 2021.7.13
* Upgraded SubmitMe to 1.9
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/background-scripts')
11 files changed, 429 insertions, 164 deletions
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/background.js b/data/extensions/https-everywhere@eff.org/background-scripts/background.js index 7d999f7..78a9aca 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/background.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/background.js @@ -9,10 +9,11 @@ const rules = require('./rules'), update = require('./update'), { update_channels } = require('./update_channels'), wasm = require('./wasm'), - ipUtils = require('./ip_utils'); - + ipUtils = require('./ip_utils'), + ssl_codes = require('./ssl_codes'); let all_rules = new rules.RuleSets(); +let blooms = []; async function initialize() { await wasm.initialize(); @@ -22,6 +23,7 @@ async function initialize() { await getUpgradeToSecureAvailable(); await update.initialize(store, initializeAllRules); await all_rules.loadFromBrowserStorage(store, update.applyStoredRulesets); + await update.applyStoredBlooms(blooms); await incognito.onIncognitoDestruction(destroy_caches); } initialize(); @@ -30,6 +32,8 @@ async function initializeAllRules() { const r = new rules.RuleSets(); await r.loadFromBrowserStorage(store, update.applyStoredRulesets); Object.assign(all_rules, r); + blooms.length = 0; + await update.applyStoredBlooms(blooms); } /** @@ -92,7 +96,8 @@ function initializeStoredGlobals() { }); } -let upgradeToSecureAvailable; +/** @type {boolean} */ +let upgradeToSecureAvailable = false; function getUpgradeToSecureAvailable() { if (typeof browser !== 'undefined') { @@ -181,14 +186,18 @@ function updateState () { title: 'HTTPS Everywhere' + ((iconState === 'active') ? '' : ' (' + iconState + ')') }); - chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { - if (!tabs || tabs.length === 0) { + const chromeUrl = 'chrome://'; + + chrome.tabs.query({ active: true, currentWindow: true, status: 'complete' }, function(tabs) { + if (!tabs || tabs.length === 0 || tabs[0].url.startsWith(chromeUrl) ) { return; } + + // tabUrl.host instead of hostname should be used to show the "disabled" status properly (#19293) const tabUrl = new URL(tabs[0].url); - const hostname = util.getNormalisedHostname(tabUrl.hostname); + const host = util.getNormalisedHostname(tabUrl.host); - if (isExtensionDisabledOnSite(hostname) || iconState == "disabled") { + if (isExtensionDisabledOnSite(host) || iconState == "disabled") { if ('setIcon' in chrome.browserAction) { chrome.browserAction.setIcon({ path: { @@ -268,7 +277,7 @@ BrowserSession.prototype = { // sort by ruleset names alphabetically, case-insensitive if (this.getTab(tabId, "applied_rulesets", null)) { - let rulesets = this.getTab(tabId, "applied_rulesets"); + let rulesets = this.getTab(tabId, "applied_rulesets", null); let insertIndex = 0; const ruleset_name = ruleset.name.toLowerCase(); @@ -313,7 +322,7 @@ BrowserSession.prototype = { this.requests.delete(requestId); } } -} +}; let browserSession = new BrowserSession(); @@ -360,7 +369,7 @@ function onBeforeRequest(details) { // Check if an user has disabled HTTPS Everywhere on this site. We should // ensure that all subresources are not run through HTTPS Everywhere as well. - browserSession.putTab(details.tabId, 'first_party_host', uri.hostname, true); + browserSession.putTab(details.tabId, 'first_party_host', uri.host, true); } if (isExtensionDisabledOnSite(browserSession.getTab(details.tabId, 'first_party_host', null))) { @@ -374,6 +383,7 @@ function onBeforeRequest(details) { (uri.protocol === 'http:' || uri.protocol === 'ftp:') && uri.hostname.slice(-6) !== '.onion' && uri.hostname !== 'localhost' && + !uri.hostname.endsWith('.localhost') && uri.hostname !== '[::1]' && !isLocalIp; @@ -398,7 +408,7 @@ function onBeforeRequest(details) { return redirectOnCancel(shouldCancel, details.url); } - if (browserSession.getRequest(details.requestId, "redirect_count") >= 8) { + if (browserSession.getRequest(details.requestId, "redirect_count", 0) >= 8) { util.log(util.NOTE, "Redirect counter hit for " + uri.href); urlBlacklist.add(uri.href); rules.settings.domainBlacklist.add(uri.hostname); @@ -421,6 +431,15 @@ function onBeforeRequest(details) { } } + if (newuristr == null && blooms.length > 0 && uri.protocol === 'http:') { + for(let bloom of blooms) { + if(bloom.check(uri.hostname)) { + newuristr = uri.href.replace(/^http:/, "https:"); + break; + } + } + } + // only use upgradeToSecure for trivial rewrites if (upgradeToSecureAvailable && newuristr) { // check rewritten URIs against the trivially upgraded URI @@ -560,28 +579,9 @@ function onErrorOccurred(details) { if (httpNowhereOn && details.type == "main_frame" && browserSession.getRequest(details.requestId, "simple_http_nowhere_redirect", false) && - ( // Enumerate a class of errors that are likely due to HTTPS misconfigurations - details.error.indexOf("net::ERR_SSL_") == 0 || - details.error.indexOf("net::ERR_CERT_") == 0 || - details.error.indexOf("net::ERR_CONNECTION_") == 0 || - details.error.indexOf("net::ERR_ABORTED") == 0 || - details.error.indexOf("net::ERR_SSL_PROTOCOL_ERROR") == 0 || - details.error.indexOf("NS_ERROR_CONNECTION_REFUSED") == 0 || - details.error.indexOf("NS_ERROR_NET_TIMEOUT") == 0 || - details.error.indexOf("NS_ERROR_NET_ON_TLS_HANDSHAKE_ENDED") == 0 || - details.error.indexOf("SSL received a record that exceeded the maximum permissible length.") == 0 || - details.error.indexOf("Peer’s Certificate has expired.") == 0 || - details.error.indexOf("Unable to communicate securely with peer: requested domain name does not match the server’s certificate.") == 0 || - details.error.indexOf("Peer’s Certificate issuer is not recognized.") == 0 || - details.error.indexOf("Peer’s Certificate has been revoked.") == 0 || - details.error.indexOf("Peer reports it experienced an internal error.") == 0 || - details.error.indexOf("The server uses key pinning (HPKP) but no trusted certificate chain could be constructed that matches the pinset. Key pinning violations cannot be overridden.") == 0 || - details.error.indexOf("SSL received a weak ephemeral Diffie-Hellman key in Server Key Exchange handshake message.") == 0 || - details.error.indexOf("The certificate was signed using a signature algorithm that is disabled because it is not secure.") == 0 || - details.error.indexOf("Unable to communicate securely with peer: requested domain name does not match the server’s certificate.") == 0 || - details.error.indexOf("Cannot communicate securely with peer: no common encryption algorithm(s).") == 0 || - details.error.indexOf("SSL peer has no certificate for the requested DNS name.") == 0 - )) { + // Enumerate errors that are likely due to HTTPS misconfigurations + ssl_codes.error_list.some(message => details.error.includes(message)) + ) { let url = new URL(details.url); if (url.protocol == "https:") { url.protocol = "http:"; @@ -646,7 +646,7 @@ function onHeadersReceived(details) { const upgradeInsecureRequests = { name: 'Content-Security-Policy', value: 'upgrade-insecure-requests' - } + }; details.responseHeaders.push(upgradeInsecureRequests); responseHeadersChanged = true; } @@ -669,7 +669,7 @@ chrome.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["https: chrome.webRequest.onCompleted.addListener(onCompleted, {urls: ["*://*/*"]}); // Cleanup redirectCounter if necessary -chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["*://*/*"]}) +chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["*://*/*"]}); // Insert upgrade-insecure-requests directive in httpNowhere mode chrome.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["https://*/*"]}, ["blocking", "responseHeaders"]); @@ -685,8 +685,8 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { let last_updated_promises = []; for(let update_channel of update_channels) { last_updated_promises.push(new Promise(resolve => { - store.local.get({['rulesets-timestamp: ' + update_channel.name]: 0}, item => { - resolve([update_channel.name, item['rulesets-timestamp: ' + update_channel.name]]); + store.local.get({['uc-timestamp: ' + update_channel.name]: 0}, item => { + resolve([update_channel.name, item['uc-timestamp: ' + update_channel.name]]); }); })); } @@ -792,11 +792,11 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { if (sendResponse !== null) { sendResponse(true); } - }) + }); return true; }, - get_ruleset_timestamps: () => { - update.getRulesetTimestamps().then(timestamps => sendResponse(timestamps)); + get_update_channel_timestamps: () => { + update.getUpdateChannelTimestamps().then(timestamps => sendResponse(timestamps)); return true; }, get_pinned_update_channels: () => { @@ -842,9 +842,16 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { return (update_channel.name != message.object); })}, () => { store.local.remove([ - 'rulesets-timestamp: ' + message.object, - 'rulesets-stored-timestamp: ' + message.object, - 'rulesets: ' + message.object + 'uc-timestamp: ' + message.object, + 'uc-stored-timestamp: ' + message.object, + 'rulesets: ' + message.object, + 'bloom: ' + message.object, + 'bloom_bitmap_bits: ' + message.object, + 'bloom_k_num: ' + message.object, + 'bloom_sip_keys_0_0: ' + message.object, + 'bloom_sip_keys_0_1: ' + message.object, + 'bloom_sip_keys_1_0: ' + message.object, + 'bloom_sip_keys_1_1: ' + message.object, ], () => { initializeAllRules(); sendResponse(true); @@ -868,10 +875,8 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { // Ensure that we check for new rulesets from the update channel immediately. // If the scope has changed, make sure that the rulesets are re-initialized. + update.removeStorageListener(); store.set({update_channels: item.update_channels}, () => { - // Since loadUpdateChannesKeys is already contained in chrome.storage.onChanged - // within update.js, the below call will make it run twice. This is - // necesssary to avoid a race condition, see #16673 update.loadUpdateChannelsKeys().then(() => { update.resetTimer(); if(scope_changed) { @@ -879,8 +884,8 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { } sendResponse(true); }); + update.addStorageListener(); }); - }); return true; }, @@ -934,21 +939,6 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { }); /** - * @description Upboarding event for visual changelog - */ -chrome.runtime.onInstalled.addListener(async ({reason, temporary}) => { - if (temporary) return; - switch (reason) { - case "update": - { - const url = chrome.runtime.getURL("pages/onboarding/updated.html"); - await chrome.tabs.create({ url }); - } - break; - } -}); - -/** * Clear any cache/ blacklist we have. */ function destroy_caches() { @@ -962,6 +952,7 @@ function destroy_caches() { Object.assign(exports, { all_rules, + blooms, urlBlacklist }); diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/incognito.js b/data/extensions/https-everywhere@eff.org/background-scripts/incognito.js index ca52177..7d4bc81 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/incognito.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/incognito.js @@ -43,7 +43,7 @@ Incognito.prototype = { } } }, -} +}; /** * Check if any incognito window still exists diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/ip_utils.js b/data/extensions/https-everywhere@eff.org/background-scripts/ip_utils.js index be7c1c8..90e0b1a 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/ip_utils.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/ip_utils.js @@ -2,11 +2,17 @@ (function (exports) { +/** + * Parse and convert literal IP address into numerical IP address. + * @param {string} ip + * @returns {number} + */ const parseIp = ip => { if (!/^[0-9.]+$/.test(ip)) { return -1; } + /** @type {string[]} */ const octets = ip.split('.'); if (octets.length !== 4) { @@ -26,14 +32,21 @@ const parseIp = ip => { return -1; } - ipN = (ipN << 8) | octet; + ipN = (ipN << 8) | octetN; } return ipN >>> 0; }; +/** + * Check if the numeric IP address is within a certain range. + * @param {number} ip + * @param {number[]} range + * @returns {boolean} + */ const isIpInRange = (ip, [rangeIp, mask]) => (ip & mask) >>> 0 === rangeIp; +// A list of local IP address ranges const localRanges = [ [/* 0.0.0.0 */ 0x00000000, /* 255.255.255.255 */ 0xffffffff], [/* 127.0.0.0 */ 0x7f000000, /* 255.0.0.0 */ 0xff000000], @@ -42,6 +55,11 @@ const localRanges = [ [/* 192.168.0.0 */ 0xc0a80000, /* 255.255.0.0 */ 0xffff0000], ]; +/** + * Check if the numeric IP address is inside the local IP address ranges. + * @param {number} ip + * @returns {boolean} + */ const isLocalIp = ip => localRanges.some(range => isIpInRange(ip, range)); Object.assign(exports, { diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/modules/on_before.js b/data/extensions/https-everywhere@eff.org/background-scripts/modules/on_before.js deleted file mode 100644 index e69de29..0000000 --- a/data/extensions/https-everywhere@eff.org/background-scripts/modules/on_before.js +++ /dev/null diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/modules/ssl_codes.js b/data/extensions/https-everywhere@eff.org/background-scripts/modules/ssl_codes.js new file mode 100644 index 0000000..ed955ec --- /dev/null +++ b/data/extensions/https-everywhere@eff.org/background-scripts/modules/ssl_codes.js @@ -0,0 +1,48 @@ +"use strict"; + +/** + * @exports error_list + * @type {array} + * @description A list of known SSL config errors to filter through and not try to upgrade the user + * @see + * Chrome SSL errors: https://github.com/chromium/chromium/blob/master/components/domain_reliability/util.cc + * Firefox SSL Errors: https://hg.mozilla.org/releases/mozilla-release/file/tip/security/manager/locales/en-US/chrome/pipnss/nsserrors.properties + */ + +(function (exports) { + +const error_list = [ + "net::ERR_SSL_PROTOCOL_ERROR", + "net::ERR_SSL_VERSION_OR_CIPHER_MISMATCH", + "net::ERR_SSL_UNRECOGNIZED_NAME_ALERT", + "net::ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN", + "net::ERR_CERT_COMMON_NAME_INVALID", + "net::ERR_CERT_DATE_INVALID", + "net::ERR_CERT_AUTHORITY_INVALID", + "net::ERR_CERT_REVOKED", + "net::ERR_CERT_INVALID", + "net::ERR_CONNECTION_CLOSED", + "net::ERR_CONNECTION_RESET", + "net::ERR_CONNECTION_REFUSED", + "net::ERR_CONNECTION_ABORTED", + "net::ERR_CONNECTION_FAILED", + "net::ERR_ABORTED", , + "NS_ERROR_CONNECTION_REFUSED", + "NS_ERROR_NET_ON_TLS_HANDSHAKE_ENDED", + "NS_BINDING_ABORTED", + "SSL received a record that exceeded the maximum permissible length.", + "Peer’s Certificate has expired.", + "Unable to communicate securely with peer: requested domain name does not match the server’s certificate.", + "Peer’s Certificate issuer is not recognized.", + "Peer’s Certificate has been revoked.", + "Peer reports it experienced an internal error.", + "The server uses key pinning (HPKP) but no trusted certificate chain could be constructed that matches the pinset. Key pinning violations cannot be overridden.", + "SSL received a weak ephemeral Diffie-Hellman key in Server Key Exchange handshake message.", + "The certificate was signed using a signature algorithm that is disabled because it is not secure.", + "Cannot communicate securely with peer: no common encryption algorithm(s).", + "SSL peer has no certificate for the requested DNS name." +]; + +Object.assign(exports, { error_list }); + +})(typeof exports !== 'undefined' ? exports : require.scopes.ssl_codes = {});
\ No newline at end of file diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/rules.js b/data/extensions/https-everywhere@eff.org/background-scripts/rules.js index 51da9b7..7f0a5b5 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/rules.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/rules.js @@ -16,7 +16,11 @@ const trivial_cookie_rule_c = /.+/; /* A map of all scope RegExp objects */ const scopes = new Map(); -/* Returns the scope object from the map for the given scope string */ +/** + * Returns the scope object from the map for the given scope string. + * @param {string} scope ruleset scope string + * @returns {RegExp} + */ function getScope(scope) { if (!scopes.has(scope)) { scopes.set(scope, new RegExp(scope)); @@ -75,9 +79,10 @@ function CookieRule(host, cookiename) { /** *A collection of rules - * @param set_name The name of this set - * @param default_state activity state - * @param note Note will be displayed in popup + * @param {string} set_name The name of this set + * @param {boolean} default_state activity state + * @param {string} scope ruleset scope string + * @param {string} note Note will be displayed in popup * @constructor */ function RuleSet(set_name, default_state, scope, note) { @@ -87,7 +92,7 @@ function RuleSet(set_name, default_state, scope, note) { this.cookierules = null; this.active = default_state; this.default_state = default_state; - this.scope = scope; + this.scope = getScope(scope); this.note = note; } @@ -124,7 +129,7 @@ RuleSet.prototype = { isEquivalentTo: function(ruleset) { if(this.name != ruleset.name || this.note != ruleset.note || - this.state != ruleset.state || + this.active != ruleset.active || this.default_state != ruleset.default_state) { return false; } @@ -176,7 +181,6 @@ RuleSet.prototype = { /** * Initialize Rule Sets - * @param ruleActiveStates default state for rules * @constructor */ function RuleSets() { @@ -189,7 +193,10 @@ function RuleSets() { // A cache for cookie hostnames. this.cookieHostCache = new Map(); - // A hash of rule name -> active status (true/false). + /** + * A hash of rule name -> active status (true/false). + * @type {Object<string, boolean>} + */ this.ruleActiveStates = {}; // The key to retrieve user rules from the storage api @@ -220,6 +227,8 @@ RuleSets.prototype = { /** * Convert XML to JS and load rulesets + * @param {Document} ruleXml + * @param {string} scope */ addFromXml: function(ruleXml, scope) { const rulesets_xml = ruleXml.getElementsByTagName("ruleset"); @@ -232,9 +241,11 @@ RuleSets.prototype = { this.addFromJson(rulesets, scope); }, + /** + * @param {*} ruleJson + * @param {string} scope + */ addFromJson: function(ruleJson, scope) { - const scope_obj = getScope(scope); - if (this.wasm_rs) { this.wasm_rs.add_all_from_js_array( ruleJson, @@ -244,7 +255,7 @@ RuleSets.prototype = { } else { for (let ruleset of ruleJson) { try { - this.parseOneJsonRuleset(ruleset, scope_obj); + this.parseOneJsonRuleset(ruleset, scope); } catch(e) { util.log(util.WARN, 'Error processing ruleset:' + e); } @@ -252,6 +263,11 @@ RuleSets.prototype = { } }, + /** + * Parse one JSON format ruleset element + * @param {*} ruletag + * @param {string} scope + */ parseOneJsonRuleset: function(ruletag, scope) { var default_state = true; var note = ""; @@ -266,7 +282,7 @@ RuleSets.prototype = { // If a ruleset declares a platform, and we don't match it, treat it as // off-by-default. In practice, this excludes "mixedcontent" rules. - var platform = ruletag["platform"] + var platform = ruletag["platform"]; if (platform) { default_state = false; if (platform == "mixedcontent" && settings.enableMixedRulesets) { @@ -320,6 +336,7 @@ RuleSets.prototype = { /** * Load a user rule * @param params + * @param {string} scope * @returns {boolean} */ addUserRule : function(params, scope) { @@ -365,7 +382,7 @@ RuleSets.prototype = { if (this.wasm_rs) { this.wasm_rs.remove_ruleset(ruleset); } else { - const tmp = this.targets.get(ruleset.name).filter(r => !r.isEquivalentTo(ruleset)) + const tmp = this.targets.get(ruleset.name).filter(r => !r.isEquivalentTo(ruleset)); this.targets.set(ruleset.name, tmp); if (this.targets.get(ruleset.name).length == 0) { @@ -398,7 +415,7 @@ RuleSets.prototype = { loadStoredUserRules: function() { return this.getStoredUserRules() .then(userRules => { - this.addFromJson(userRules, getScope()); + this.addFromJson(userRules, ''); util.log(util.INFO, `loaded ${userRules.length} stored user rules`); }); }, @@ -409,7 +426,7 @@ RuleSets.prototype = { * @param cb: Callback to call after success/fail * */ addNewRuleAndStore: async function(params) { - if (this.addUserRule(params, getScope())) { + if (this.addUserRule(params, '')) { // If we successfully added the user rule, save it in the storage // api so it's automatically applied when the extension is // reloaded. @@ -466,7 +483,7 @@ RuleSets.prototype = { }, loadCustomRuleset: function(ruleset_string) { - this.addFromXml((new DOMParser()).parseFromString(ruleset_string, 'text/xml')); + this.addFromXml((new DOMParser()).parseFromString(ruleset_string, 'text/xml'), ''); }, setRuleActiveState: async function(ruleset_name, active) { @@ -488,7 +505,7 @@ RuleSets.prototype = { let default_off = ruletag.getAttribute("default_off"); if (default_off) { - ruleset["default_off"] = platform; + ruleset["default_off"] = default_off; } let platform = ruletag.getAttribute("platform"); @@ -565,7 +582,7 @@ RuleSets.prototype = { if (this.wasm_rs) { let pa = this.wasm_rs.potentially_applicable(host); results = new Set([...pa].map(ruleset => { - let rs = new RuleSet(ruleset.name, ruleset.default_state, getScope(ruleset.scope), ruleset.note); + let rs = new RuleSet(ruleset.name, ruleset.default_state, ruleset.scope, ruleset.note); if (ruleset.cookierules) { let cookierules = ruleset.cookierules.map(cookierule => { @@ -586,6 +603,9 @@ RuleSets.prototype = { } else { rs.exclusions = null; } + + rs.active = ruleset.active; + return rs; })); } else { diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/store.js b/data/extensions/https-everywhere@eff.org/background-scripts/store.js index 3f32d03..e3af7cb 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/store.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/store.js @@ -86,13 +86,13 @@ async function performMigrations() { target: [userRule.host], rule: [{ from: userRule.urlMatcher, to: userRule.redirectTo }], default_off: "user rule" - } - }) + }; + }); return userRules; }) .then(userRules => { return set_promise(rules.RuleSets().USER_RULE_KEY, userRules); - }) + }); migration_version = 2; await set_promise('migration_version', migration_version); @@ -103,12 +103,12 @@ async function performMigrations() { .then(disabledList => { disabledList = disabledList.map(item => { return util.getNormalisedHostname(item); - }) + }); return disabledList; }) .then(disabledList => { return set_promise('disabledList', disabledList); - }) + }); migration_version = 3; await set_promise('migration_version', migration_version); @@ -132,6 +132,7 @@ function setStorage(store) { set_promise, local }); + chrome.runtime.sendMessage("store_initialized"); } Object.assign(exports, { diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/update.js b/data/extensions/https-everywhere@eff.org/background-scripts/update.js index 1363384..4767f38 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/update.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/update.js @@ -4,6 +4,7 @@ let combined_update_channels, extension_version; const { update_channels } = require('./update_channels'); +const wasm = require('./wasm'); // Determine if we're in the tests. If so, define some necessary components. if (typeof window === "undefined") { @@ -90,25 +91,25 @@ async function resetTimer() { await createTimer(); } -// Check for new rulesets. If found, return the timestamp. If not, return false -async function checkForNewRulesets(update_channel) { - let timestamp_result = await fetch(update_channel.update_path_prefix + "/latest-rulesets-timestamp"); +// Check for new updates. If found, return the timestamp. If not, return false +async function checkForNewUpdates(update_channel) { + let timestamp_result = await fetch(update_channel.update_path_prefix + (update_channel.format == "bloom" ? "/latest-bloom-timestamp" : "/latest-rulesets-timestamp")); if(timestamp_result.status == 200) { - let rulesets_timestamp = Number(await timestamp_result.text()); + let uc_timestamp = Number(await timestamp_result.text()); - if((await store.local.get_promise('rulesets-timestamp: ' + update_channel.name, 0)) < rulesets_timestamp) { - return rulesets_timestamp; + if((await store.local.get_promise('uc-timestamp: ' + update_channel.name, 0)) < uc_timestamp) { + return uc_timestamp; } } return false; } -// Retrieve the timestamp for when a stored ruleset bundle was published -async function getRulesetTimestamps() { +// Retrieve the timestamp for when an update channel was published +async function getUpdateChannelTimestamps() { let timestamp_promises = []; for(let update_channel of combined_update_channels) { timestamp_promises.push(new Promise(async resolve => { - let timestamp = await store.local.get_promise('rulesets-stored-timestamp: ' + update_channel.name, 0); + let timestamp = await store.local.get_promise('uc-stored-timestamp: ' + update_channel.name, 0); resolve([update_channel, timestamp]); })); } @@ -119,7 +120,7 @@ async function getRulesetTimestamps() { // Download and return new rulesets async function getNewRulesets(rulesets_timestamp, update_channel) { - store.local.set_promise('rulesets-timestamp: ' + update_channel.name, rulesets_timestamp); + store.local.set_promise('uc-timestamp: ' + update_channel.name, rulesets_timestamp); let signature_promise = fetch(update_channel.update_path_prefix + "/rulesets-signature." + rulesets_timestamp + ".sha256"); let rulesets_promise = fetch(update_channel.update_path_prefix + "/default.rulesets." + rulesets_timestamp + ".gz"); @@ -140,6 +141,34 @@ async function getNewRulesets(rulesets_timestamp, update_channel) { }; } +// Download and return new bloom +async function getNewBloom(bloom_timestamp, update_channel) { + store.local.set_promise('uc-timestamp: ' + update_channel.name, bloom_timestamp); + + let signature_promise = fetch(update_channel.update_path_prefix + "/bloom-signature." + bloom_timestamp + ".sha256"); + let bloom_metadata_promise = fetch(update_channel.update_path_prefix + "/bloom-metadata." + bloom_timestamp + ".json"); + let bloom_promise = fetch(update_channel.update_path_prefix + "/bloom." + bloom_timestamp + ".bin"); + + let responses = await Promise.all([ + signature_promise, + bloom_metadata_promise, + bloom_promise + ]); + + let resolutions = await Promise.all([ + responses[0].arrayBuffer(), + responses[1].arrayBuffer(), + responses[2].arrayBuffer() + ]); + + return { + signature_array_buffer: resolutions[0], + bloom_metadata_array_buffer: resolutions[1], + bloom_array_buffer: resolutions[2], + }; + +} + // Returns a promise which verifies that the rulesets have a valid EFF // signature, and if so, stores them and returns true. // Otherwise, it throws an exception. @@ -177,73 +206,179 @@ function verifyAndStoreNewRulesets(new_rulesets, rulesets_timestamp, update_chan }); } -// Unzip and apply the rulesets we have stored. -async function applyStoredRulesets(rulesets_obj) { - let rulesets_promises = []; - for(let update_channel of combined_update_channels) { - rulesets_promises.push(new Promise(resolve => { - const key = 'rulesets: ' + update_channel.name; - chrome.storage.local.get(key, root => { - if(root[key]) { - util.log(util.NOTE, update_channel.name + ': Applying stored rulesets.'); - - const rulesets_gz = window.atob(root[key]); - const rulesets_byte_array = pako.inflate(rulesets_gz); - const rulesets_string = new TextDecoder("utf-8").decode(rulesets_byte_array); - const rulesets_json = JSON.parse(rulesets_string); - - resolve({json: rulesets_json, scope: update_channel.scope, replaces: update_channel.replaces_default_rulesets}); +// Returns a promise which verifies that the bloom has a valid EFF +// signature, and if so, stores it and returns true. +// Otherwise, it throws an exception. +function verifyAndStoreNewBloom(new_bloom, bloom_timestamp, update_channel) { + return new Promise((resolve, reject) => { + window.crypto.subtle.verify( + { + name: "RSA-PSS", + saltLength: 32 + }, + imported_keys[update_channel.name], + new_bloom.signature_array_buffer, + new_bloom.bloom_metadata_array_buffer + ).then(async isvalid => { + if(isvalid) { + util.log(util.NOTE, update_channel.name + ': Bloom filter metadata signature checks out.'); + + const bloom_metadata = JSON.parse(util.ArrayBufferToString(new_bloom.bloom_metadata_array_buffer)); + const bloom_str = util.ArrayBufferToString(new_bloom.bloom_array_buffer); + + if(bloom_metadata.timestamp != bloom_timestamp) { + reject(update_channel.name + ': Downloaded bloom filter had an incorrect timestamp. This may be an attempted downgrade attack. Aborting.'); + } else if(await sha256sum(new_bloom.bloom_array_buffer) != bloom_metadata.sha256sum) { + reject(update_channel.name + ': sha256sum of the bloom filter is invalid. Aborting.'); } else { - resolve(); + await store.local.set_promise('bloom: ' + update_channel.name, window.btoa(bloom_str)); + await store.local.set_promise('bloom_bitmap_bits: ' + update_channel.name, bloom_metadata.bitmap_bits); + await store.local.set_promise('bloom_k_num: ' + update_channel.name, bloom_metadata.k_num); + await store.local.set_promise('bloom_sip_keys_0_0: ' + update_channel.name, bloom_metadata.sip_keys[0][0]); + await store.local.set_promise('bloom_sip_keys_0_1: ' + update_channel.name, bloom_metadata.sip_keys[0][1]); + await store.local.set_promise('bloom_sip_keys_1_0: ' + update_channel.name, bloom_metadata.sip_keys[1][0]); + await store.local.set_promise('bloom_sip_keys_1_1: ' + update_channel.name, bloom_metadata.sip_keys[1][1]); + resolve(true); } - }); - })); - } + } else { + reject(update_channel.name + ': Downloaded bloom filter metadata signature is invalid. Aborting.'); + } + }).catch(() => { + reject(update_channel.name + ': Downloaded bloom signature could not be verified. Aborting.'); + }); + }); +} + +async function sha256sum(buffer) { + const hashBuffer = await window.crypto.subtle.digest('SHA-256', buffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join(''); + return hashHex; +} - function isNotUndefined(subject) { - return (typeof subject != 'undefined'); +function isNotUndefined(subject) { + return (typeof subject != 'undefined'); +} + +// Apply the rulesets we have stored. +async function applyStoredRulesets(rulesets_obj) { + let rulesets_promises = []; + for(let update_channel of combined_update_channels) { + if(update_channel.format == "rulesets" || !update_channel.format) { + rulesets_promises.push(new Promise(resolve => { + const key = 'rulesets: ' + update_channel.name; + chrome.storage.local.get(key, root => { + if(root[key]) { + util.log(util.NOTE, update_channel.name + ': Applying stored rulesets.'); + + const rulesets_gz = window.atob(root[key]); + const rulesets_byte_array = pako.inflate(rulesets_gz); + const rulesets_string = new TextDecoder("utf-8").decode(rulesets_byte_array); + const rulesets_json = JSON.parse(rulesets_string); + + resolve({json: rulesets_json, scope: update_channel.scope, replaces: update_channel.replaces_default_rulesets}); + } else { + resolve(); + } + }); + })); + } } - const channel_results = (await Promise.all(rulesets_promises)).filter(isNotUndefined); + const rulesets_results = (await Promise.all(rulesets_promises)).filter(isNotUndefined); let replaces = false; - for(const channel_result of channel_results) { - if(channel_result.replaces === true) { + for(const rulesets_result of rulesets_results) { + if(rulesets_result.replaces === true) { replaces = true; } - rulesets_obj.addFromJson(channel_result.json.rulesets, channel_result.scope); + rulesets_obj.addFromJson(rulesets_result.json.rulesets, rulesets_result.scope); } if(!replaces) { - rulesets_obj.addFromJson(util.loadExtensionFile('rules/default.rulesets', 'json')); + rulesets_obj.addFromJson(util.loadExtensionFile('rules/default.rulesets', 'json'), ''); + } +} + +// Apply the blooms we have stored. +async function applyStoredBlooms(bloom_arr) { + let bloom_promises = []; + for(let update_channel of combined_update_channels) { + if(update_channel.format == "bloom") { + bloom_promises.push(new Promise(resolve => { + const key = 'bloom: ' + update_channel.name; + chrome.storage.local.get(key, async root => { + if(root[key]) { + util.log(util.NOTE, update_channel.name + ': Applying stored bloom filter.'); + const bloom = util.StringToArrayBuffer(window.atob(root[key])); + const bloom_bitmap_bits = await store.local.get_promise('bloom_bitmap_bits: ' + update_channel.name, ""); + const bloom_k_num = await store.local.get_promise('bloom_k_num: ' + update_channel.name, ""); + const bloom_sip_keys_0_0 = await store.local.get_promise('bloom_sip_keys_0_0: ' + update_channel.name, ""); + const bloom_sip_keys_0_1 = await store.local.get_promise('bloom_sip_keys_0_1: ' + update_channel.name, ""); + const bloom_sip_keys_1_0 = await store.local.get_promise('bloom_sip_keys_1_0: ' + update_channel.name, ""); + const bloom_sip_keys_1_1 = await store.local.get_promise('bloom_sip_keys_1_1: ' + update_channel.name, ""); + + try{ + resolve(wasm.Bloom.from_existing(bloom, bloom_bitmap_bits, bloom_k_num, [[bloom_sip_keys_0_0, bloom_sip_keys_0_1], [bloom_sip_keys_1_0, bloom_sip_keys_1_1]])); + } catch(_) { + resolve(); + } + } else { + resolve(); + } + }); + })); + } + } + + bloom_arr.length = 0; + const bloom_results = (await Promise.all(bloom_promises)).filter(isNotUndefined); + for(const bloom_result of bloom_results) { + bloom_arr.push(bloom_result); } } + // basic workflow for periodic checks async function performCheck() { - util.log(util.NOTE, 'Checking for new rulesets.'); + util.log(util.NOTE, 'Checking for new updates.'); const current_timestamp = Date.now() / 1000; store.local.set_promise('last-checked', current_timestamp); let num_updates = 0; for(let update_channel of combined_update_channels) { - let new_rulesets_timestamp = await checkForNewRulesets(update_channel); - if(new_rulesets_timestamp) { - - if(update_channel.replaces_default_rulesets && extension_timestamp > new_rulesets_timestamp) { - util.log(util.NOTE, update_channel.name + ': A new ruleset bundle has been released, but it is older than the extension-bundled rulesets it replaces. Skipping.'); - continue; + if(update_channel.format == "bloom") { + let new_bloom_timestamp = await checkForNewUpdates(update_channel); + if(new_bloom_timestamp) { + util.log(util.NOTE, update_channel.name + ': A new bloom filter has been released. Downloading now.'); + let new_bloom = await getNewBloom(new_bloom_timestamp, update_channel); + try{ + await verifyAndStoreNewBloom(new_bloom, new_bloom_timestamp, update_channel); + store.local.set_promise('uc-stored-timestamp: ' + update_channel.name, new_bloom_timestamp); + num_updates++; + } catch(err) { + util.log(util.WARN, update_channel.name + ': ' + err); + } } + } else { + let new_rulesets_timestamp = await checkForNewUpdates(update_channel); + if(new_rulesets_timestamp) { + + if(update_channel.replaces_default_rulesets && extension_timestamp > new_rulesets_timestamp) { + util.log(util.NOTE, update_channel.name + ': A new ruleset bundle has been released, but it is older than the extension-bundled rulesets it replaces. Skipping.'); + continue; + } - util.log(util.NOTE, update_channel.name + ': A new ruleset bundle has been released. Downloading now.'); - let new_rulesets = await getNewRulesets(new_rulesets_timestamp, update_channel); - try{ - await verifyAndStoreNewRulesets(new_rulesets, new_rulesets_timestamp, update_channel); - store.local.set_promise('rulesets-stored-timestamp: ' + update_channel.name, new_rulesets_timestamp); - num_updates++; - } catch(err) { - util.log(util.WARN, update_channel.name + ': ' + err); + util.log(util.NOTE, update_channel.name + ': A new ruleset bundle has been released. Downloading now.'); + let new_rulesets = await getNewRulesets(new_rulesets_timestamp, update_channel); + try{ + await verifyAndStoreNewRulesets(new_rulesets, new_rulesets_timestamp, update_channel); + store.local.set_promise('uc-stored-timestamp: ' + update_channel.name, new_rulesets_timestamp); + num_updates++; + } catch(err) { + util.log(util.WARN, update_channel.name + ': ' + err); + } } } } @@ -252,7 +387,7 @@ async function performCheck() { } }; -chrome.storage.onChanged.addListener(async function(changes, areaName) { +async function storageListener(changes, areaName) { if (areaName === 'sync' || areaName === 'local') { if ('autoUpdateRulesets' in changes) { if (changes.autoUpdateRulesets.newValue) { @@ -266,7 +401,17 @@ chrome.storage.onChanged.addListener(async function(changes, areaName) { if ('update_channels' in changes) { await loadUpdateChannelsKeys(); } -}); +}; + +function addStorageListener() { + chrome.storage.onChanged.addListener(storageListener); +} + +function removeStorageListener() { + chrome.storage.onChanged.removeListener(storageListener); +} + +addStorageListener(); let initialCheck, subsequentChecks; @@ -294,8 +439,8 @@ function clear_replacement_update_channels() { for (const update_channel of combined_update_channels) { if(update_channel.replaces_default_rulesets) { util.log(util.NOTE, update_channel.name + ': You have a new version of the extension. Clearing any stored rulesets, which replace the new extension-bundled ones.'); - keys.push('rulesets-timestamp: ' + update_channel.name); - keys.push('rulesets-stored-timestamp: ' + update_channel.name); + keys.push('uc-timestamp: ' + update_channel.name); + keys.push('uc-stored-timestamp: ' + update_channel.name); keys.push('rulesets: ' + update_channel.name); } } @@ -323,10 +468,13 @@ async function initialize(store_param, cb) { Object.assign(exports, { applyStoredRulesets, + applyStoredBlooms, initialize, - getRulesetTimestamps, + getUpdateChannelTimestamps, resetTimer, - loadUpdateChannelsKeys + loadUpdateChannelsKeys, + addStorageListener, + removeStorageListener, }); })(typeof exports == 'undefined' ? require.scopes.update = {} : exports); diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js b/data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js index f21b332..80494ef 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js @@ -4,22 +4,41 @@ (function (exports) { -exports.update_channels = [{ - name: 'EFF (Full)', - jwk: { - kty: 'RSA', - e: 'AQAB', - n: '1cwvFQu3Kw-Pz8bcEFuV5zx0ZheDsc4Tva7Qv6BL90_sDLqCW79Y543nDkPtNVfFH_89pt2kSPp_IcS5XnYiw6zBQeFuILFw5JpvZt14K0s4' + - 'e025Q9CXfhYKIBKT9PnqihwAacjMa6rQb7RTu7XxVvqxRb3b0vx2CR40LSlYZ8H_KpeaUwq2oz-fyrI6LFTeYvbO3ZuLKeK5xV1a32xeTVMF' + - 'kIj3LxnQalxq-DRHfj7LRRoTnbRDW4uoDc8aVpLFliuO79jUKbobz4slpiWJ4wjKR_O6OK13HbZUiOSxi8Bms-UqBPOyzbMVpmA7lv_zWdaL' + - 'u1IVlVXQyLVbbrqI6llRqfHdcJoEl-eC48AofuB-relQtjTEK_hyBf7sPwrbqAarjRjlyEx6Qy5gTXyxM9attfNAeupYR6jm8LKm6TFpfWky' + - 'DxUmj_f5pJMBWNTomV74f8iQ2M18_KWMUDCOf80tR0t21Q1iCWdvA3K_KJn05tTLyumlwwlQijMqRkYuao-CX9L3DJIaB3VPYPTSIPUr7oi1' + - '6agsuamOyiOtlZiRpEvoNg2ksJMZtwnj5xhBQydkdhMW2ZpHDzcLuZlhJYZL_l3_7wuzRM7vpyA9obP92CpZRFJErGZmFxJC93I4U9-0B0wg' + - '-sbyMKGJ5j1BWTnibCklDXtWzXtuiz18EgE' +exports.update_channels = [ + { + name: 'EFF (Full)', + jwk: { + kty: 'RSA', + e: 'AQAB', + n: '1cwvFQu3Kw-Pz8bcEFuV5zx0ZheDsc4Tva7Qv6BL90_sDLqCW79Y543nDkPtNVfFH_89pt2kSPp_IcS5XnYiw6zBQeFuILFw5JpvZt14K0s4' + + 'e025Q9CXfhYKIBKT9PnqihwAacjMa6rQb7RTu7XxVvqxRb3b0vx2CR40LSlYZ8H_KpeaUwq2oz-fyrI6LFTeYvbO3ZuLKeK5xV1a32xeTVMF' + + 'kIj3LxnQalxq-DRHfj7LRRoTnbRDW4uoDc8aVpLFliuO79jUKbobz4slpiWJ4wjKR_O6OK13HbZUiOSxi8Bms-UqBPOyzbMVpmA7lv_zWdaL' + + 'u1IVlVXQyLVbbrqI6llRqfHdcJoEl-eC48AofuB-relQtjTEK_hyBf7sPwrbqAarjRjlyEx6Qy5gTXyxM9attfNAeupYR6jm8LKm6TFpfWky' + + 'DxUmj_f5pJMBWNTomV74f8iQ2M18_KWMUDCOf80tR0t21Q1iCWdvA3K_KJn05tTLyumlwwlQijMqRkYuao-CX9L3DJIaB3VPYPTSIPUr7oi1' + + '6agsuamOyiOtlZiRpEvoNg2ksJMZtwnj5xhBQydkdhMW2ZpHDzcLuZlhJYZL_l3_7wuzRM7vpyA9obP92CpZRFJErGZmFxJC93I4U9-0B0wg' + + '-sbyMKGJ5j1BWTnibCklDXtWzXtuiz18EgE' + }, + update_path_prefix: 'https://www.https-rulesets.org/v1/', + scope: '', + replaces_default_rulesets: true }, - update_path_prefix: 'https://www.https-rulesets.org/v1/', - scope: '', - replaces_default_rulesets: true -}]; + { + name: 'DuckDuckGo Smarter Encryption', + format: 'bloom', + jwk: { + kty: 'RSA', + e: 'AQAB', + n: '1cwvFQu3Kw-Pz8bcEFuV5zx0ZheDsc4Tva7Qv6BL90_sDLqCW79Y543nDkPtNVfFH_89pt2kSPp_IcS5XnYiw6zBQeFuILFw5JpvZt14K0s4' + + 'e025Q9CXfhYKIBKT9PnqihwAacjMa6rQb7RTu7XxVvqxRb3b0vx2CR40LSlYZ8H_KpeaUwq2oz-fyrI6LFTeYvbO3ZuLKeK5xV1a32xeTVMF' + + 'kIj3LxnQalxq-DRHfj7LRRoTnbRDW4uoDc8aVpLFliuO79jUKbobz4slpiWJ4wjKR_O6OK13HbZUiOSxi8Bms-UqBPOyzbMVpmA7lv_zWdaL' + + 'u1IVlVXQyLVbbrqI6llRqfHdcJoEl-eC48AofuB-relQtjTEK_hyBf7sPwrbqAarjRjlyEx6Qy5gTXyxM9attfNAeupYR6jm8LKm6TFpfWky' + + 'DxUmj_f5pJMBWNTomV74f8iQ2M18_KWMUDCOf80tR0t21Q1iCWdvA3K_KJn05tTLyumlwwlQijMqRkYuao-CX9L3DJIaB3VPYPTSIPUr7oi1' + + '6agsuamOyiOtlZiRpEvoNg2ksJMZtwnj5xhBQydkdhMW2ZpHDzcLuZlhJYZL_l3_7wuzRM7vpyA9obP92CpZRFJErGZmFxJC93I4U9-0B0wg' + + '-sbyMKGJ5j1BWTnibCklDXtWzXtuiz18EgE' + }, + update_path_prefix: 'https://www.https-rulesets.org/ddg/', + scope: '', + } +]; })(typeof exports === 'undefined' ? require.scopes.update_channels = {} : exports); diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/util.js b/data/extensions/https-everywhere@eff.org/background-scripts/util.js index 5a4097c..e2b069b 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/util.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/util.js @@ -62,11 +62,16 @@ function loadExtensionFile(url, returnType) { /** * Remove tailing dots from hostname, e.g. "www.example.com." + * Preserve port numbers if they are used */ -function getNormalisedHostname(hostname) { +function getNormalisedHostname(host) { + let [ hostname, port ] = host.split(":"); while (hostname && hostname[hostname.length - 1] === '.' && hostname !== '.') { hostname = hostname.slice(0, -1); } + if (port) { + return `${hostname}:${port}`; + } return hostname; } @@ -143,6 +148,19 @@ function ArrayBufferToString(ab) { return string; } +/** + * Convert a string to an ArrayBuffer + * + * @param string: a string to convert + */ +function StringToArrayBuffer(str) { + var byteArray = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) { + byteArray[i] = str.charCodeAt(i); + } + return byteArray; +} + Object.assign(exports, { VERB, @@ -158,7 +176,8 @@ Object.assign(exports, { setDefaultLogLevel, getDefaultLogLevel, loadExtensionFile, - ArrayBufferToString + ArrayBufferToString, + StringToArrayBuffer }); })(typeof exports == 'undefined' ? require.scopes.util = {} : exports); diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/wasm.js b/data/extensions/https-everywhere@eff.org/background-scripts/wasm.js index 3385bb7..551b1d3 100644 --- a/data/extensions/https-everywhere@eff.org/background-scripts/wasm.js +++ b/data/extensions/https-everywhere@eff.org/background-scripts/wasm.js @@ -3,7 +3,7 @@ (function(exports) { const util = require('./util'), - { RuleSets } = wasm_bindgen; + { RuleSets, Bloom } = wasm_bindgen; async function initialize() { try { @@ -20,6 +20,7 @@ function is_enabled() { Object.assign(exports, { initialize, RuleSets, + Bloom, is_enabled, }); |