summaryrefslogtreecommitdiff
path: root/data/extensions/https-everywhere@eff.org/background-scripts/update.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/background-scripts/update.js')
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/update.js280
1 files changed, 280 insertions, 0 deletions
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/update.js b/data/extensions/https-everywhere@eff.org/background-scripts/update.js
new file mode 100644
index 0000000..795f968
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/update.js
@@ -0,0 +1,280 @@
+/* global pako */
+
+"use strict";
+
+let combined_update_channels;
+const { update_channels } = require('./update_channels');
+
+// Determine if we're in the tests. If so, define some necessary components.
+if (typeof window === "undefined") {
+ var WebCrypto = require("node-webcrypto-ossl"),
+ crypto = new WebCrypto(),
+ atob = require("atob"),
+ btoa = require("btoa"),
+ pako = require('../external/pako-1.0.5/pako_inflate.min.js'),
+ { TextDecoder } = require('text-encoding'),
+ chrome = require("sinon-chrome"),
+ window = { atob, btoa, chrome, crypto, pako, TextDecoder };
+
+ combined_update_channels = update_channels;
+}
+
+(function(exports) {
+
+const util = require('./util');
+
+let store,
+ background_callback;
+
+// how often we should check for new rulesets
+const periodicity = 86400;
+
+let imported_keys;
+
+// update channels are loaded from `background-scripts/update_channels.js` as well as the storage api
+async function loadUpdateChannelsKeys() {
+ util.log(util.NOTE, 'Loading update channels and importing associated public keys.');
+
+ const stored_update_channels = await store.get_promise('update_channels', []);
+ const combined_update_channels_preflight = update_channels.concat(stored_update_channels);
+
+ imported_keys = {};
+ combined_update_channels = [];
+
+ for(let update_channel of combined_update_channels_preflight){
+
+ try{
+ imported_keys[update_channel.name] = await window.crypto.subtle.importKey(
+ "jwk",
+ update_channel.jwk,
+ {
+ name: "RSA-PSS",
+ hash: {name: "SHA-256"},
+ },
+ false,
+ ["verify"]
+ );
+ combined_update_channels.push(update_channel);
+ } catch(err) {
+ util.log(util.WARN, update_channel.name + ': Could not import key. Aborting.');
+ }
+ }
+}
+
+
+// Determine the time until we should check for new rulesets
+async function timeToNextCheck() {
+ const last_checked = await store.local.get_promise('last-checked', false);
+ if(last_checked) {
+ const current_timestamp = Date.now() / 1000;
+ const secs_since_last_checked = current_timestamp - last_checked;
+ return Math.max(0, periodicity - secs_since_last_checked);
+ } else {
+ return 0;
+ }
+}
+
+// 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");
+ if(timestamp_result.status == 200) {
+ let rulesets_timestamp = Number(await timestamp_result.text());
+
+ if((await store.local.get_promise('rulesets-timestamp: ' + update_channel.name, 0)) < rulesets_timestamp){
+ return rulesets_timestamp;
+ }
+ }
+ return false;
+}
+
+// Retrieve the timestamp for when a stored ruleset bundle was published
+async function getRulesetTimestamps(){
+ 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);
+ resolve([update_channel.name, timestamp]);
+ }));
+ }
+ let timestamps = await Promise.all(timestamp_promises);
+ return timestamps;
+}
+
+// Download and return new rulesets
+async function getNewRulesets(rulesets_timestamp, update_channel) {
+
+ store.local.set_promise('rulesets-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");
+
+ let responses = await Promise.all([
+ signature_promise,
+ rulesets_promise
+ ]);
+
+ let resolutions = await Promise.all([
+ responses[0].arrayBuffer(),
+ responses[1].arrayBuffer()
+ ]);
+
+ return {
+ signature_array_buffer: resolutions[0],
+ rulesets_array_buffer: resolutions[1]
+ };
+}
+
+// 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.
+function verifyAndStoreNewRulesets(new_rulesets, rulesets_timestamp, update_channel){
+ return new Promise((resolve, reject) => {
+ window.crypto.subtle.verify(
+ {
+ name: "RSA-PSS",
+ saltLength: 32
+ },
+ imported_keys[update_channel.name],
+ new_rulesets.signature_array_buffer,
+ new_rulesets.rulesets_array_buffer
+ ).then(async isvalid => {
+ if(isvalid) {
+ util.log(util.NOTE, update_channel.name + ': Downloaded ruleset signature checks out. Storing rulesets.');
+
+ const rulesets_gz = util.ArrayBufferToString(new_rulesets.rulesets_array_buffer);
+ const rulesets_byte_array = pako.inflate(rulesets_gz);
+ const rulesets = new TextDecoder("utf-8").decode(rulesets_byte_array);
+ const rulesets_json = JSON.parse(rulesets);
+
+ if(rulesets_json.timestamp != rulesets_timestamp){
+ reject(update_channel.name + ': Downloaded ruleset had an incorrect timestamp. This may be an attempted downgrade attack. Aborting.');
+ } else {
+ await store.local.set_promise('rulesets: ' + update_channel.name, window.btoa(rulesets_gz));
+ resolve(true);
+ }
+ } else {
+ reject(update_channel.name + ': Downloaded ruleset signature is invalid. Aborting.');
+ }
+ }).catch(() => {
+ reject(update_channel.name + ': Downloaded ruleset signature could not be verified. Aborting.');
+ });
+ });
+}
+
+// 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 = new TextDecoder("utf-8").decode(rulesets_byte_array);
+ const rulesets_json = JSON.parse(rulesets);
+
+ resolve(rulesets_json);
+ } else {
+ resolve();
+ }
+ });
+ }));
+ }
+
+ const rulesets_jsons = await Promise.all(rulesets_promises);
+ if(rulesets_jsons.join("").length > 0){
+ for(let rulesets_json of rulesets_jsons){
+ if(typeof(rulesets_json) != 'undefined'){
+ rulesets_obj.addFromJson(rulesets_json.rulesets);
+ }
+ }
+ } else {
+ rulesets_obj.addFromJson(util.loadExtensionFile('rules/default.rulesets', 'json'));
+ }
+}
+
+// basic workflow for periodic checks
+async function performCheck() {
+ util.log(util.NOTE, 'Checking for new rulesets.');
+
+ 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){
+ 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);
+ }
+ }
+ }
+ if(num_updates > 0){
+ background_callback();
+ }
+};
+
+chrome.storage.onChanged.addListener(async function(changes, areaName) {
+ if (areaName === 'sync' || areaName === 'local') {
+ if ('autoUpdateRulesets' in changes) {
+ if (changes.autoUpdateRulesets.newValue) {
+ await createTimer();
+ } else {
+ destroyTimer();
+ }
+ }
+ }
+
+ if ('update_channels' in changes) {
+ await loadUpdateChannelsKeys();
+ }
+});
+
+let initialCheck,
+ subsequentChecks;
+
+async function createTimer(){
+ const time_to_next_check = await timeToNextCheck();
+
+ initialCheck = setTimeout(() => {
+ performCheck();
+ subsequentChecks = setInterval(performCheck, periodicity * 1000);
+ }, time_to_next_check * 1000);
+}
+
+function destroyTimer(){
+ if (initialCheck) {
+ clearTimeout(initialCheck);
+ }
+ if (subsequentChecks) {
+ clearInterval(subsequentChecks);
+ }
+}
+
+async function initialize(store_param, cb){
+ store = store_param;
+ background_callback = cb;
+
+ await loadUpdateChannelsKeys();
+
+ if (await store.get_promise('autoUpdateRulesets', false)) {
+ await createTimer();
+ }
+}
+
+Object.assign(exports, {
+ applyStoredRulesets,
+ initialize,
+ getRulesetTimestamps
+});
+
+})(typeof exports == 'undefined' ? require.scopes.update = {} : exports);