Skip to content
This repository has been archived by the owner on Nov 6, 2023. It is now read-only.

Commit

Permalink
Update channels ux (#16306)
Browse files Browse the repository at this point in the history
Adding a UX with the ability to add, delete, and edit update channels.
  • Loading branch information
Hainish authored Aug 16, 2018
1 parent 6d1c05d commit 53c18b6
Show file tree
Hide file tree
Showing 7 changed files with 537 additions and 71 deletions.
96 changes: 95 additions & 1 deletion chromium/background-scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const rules = require('./rules'),
store = require('./store'),
incognito = require('./incognito'),
util = require('./util'),
update = require('./update');
update = require('./update'),
{ update_channels } = require('./update_channels');


let all_rules = new rules.RuleSets();
Expand Down Expand Up @@ -721,6 +722,25 @@ chrome.runtime.onConnect.addListener(function (port) {
// This is necessary for communication with the popup in Firefox Private
// Browsing Mode, see https://bugzilla.mozilla.org/show_bug.cgi?id=1329304
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse){

function get_update_channels_generic(update_channels){
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]]);
});
}));
}
Promise.all(last_updated_promises).then(results => {
const last_updated = results.reduce((obj, item) => {
obj[item[0]] = item[1];
return obj;
}, {});
sendResponse({update_channels, last_updated});
});
}

const responses = {
get_option: () => {
store.get(message.object, sendResponse);
Expand Down Expand Up @@ -782,6 +802,80 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse){
get_ruleset_timestamps: () => {
update.getRulesetTimestamps().then(timestamps => sendResponse(timestamps));
return true;
},
get_pinned_update_channels: () => {
get_update_channels_generic(update_channels);
return true;
},
get_stored_update_channels: () => {
store.get({update_channels: []}, item => {
get_update_channels_generic(item.update_channels);
});
return true;
},
create_update_channel: () => {

store.get({update_channels: []}, item => {

const update_channel_names = update_channels.concat(item.update_channels).reduce((obj, item) => {
obj.add(item.name);
return obj;
}, new Set());

if(update_channel_names.has(message.object)){
return sendResponse(false);
}

item.update_channels.push({
name: message.object,
jwk: {},
update_path_prefix: ''
});

store.set({update_channels: item.update_channels}, () => {
sendResponse(true);
});

});
return true;
},
delete_update_channel: () => {
store.get({update_channels: []}, item => {
store.set({update_channels: item.update_channels.filter(update_channel => {
return (update_channel.name != message.object);
})}, () => {
store.local.remove([
'rulesets-timestamp: ' + message.object,
'rulesets-stored-timestamp: ' + message.object,
'rulesets: ' + message.object
], () => {
initializeAllRules();
sendResponse(true);
});
});
});
return true;
},
update_update_channel: () => {
store.get({update_channels: []}, item => {
for(let i = 0; i < item.update_channels.length; i++){
if(item.update_channels[i].name == message.object.name){
item.update_channels[i] = message.object;
}
}

store.set({update_channels: item.update_channels}, () => {
sendResponse(true);
});

});
return true;
},
get_last_checked: () => {
store.local.get({'last-checked': false}, item => {
sendResponse(item['last-checked']);
});
return true;
}
};
if (message.type in responses) {
Expand Down
2 changes: 2 additions & 0 deletions chromium/background-scripts/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ async function performMigrations() {
const local = {
get: chrome.storage.local.get,
set: chrome.storage.local.set,
remove: chrome.storage.local.remove,
get_promise: local_get_promise,
set_promise: local_set_promise
};
Expand All @@ -87,6 +88,7 @@ function setStorage(store) {
Object.assign(exports, {
get: store.get.bind(store),
set: store.set.bind(store),
remove: store.remove.bind(store),
get_promise,
set_promise,
local
Expand Down
121 changes: 73 additions & 48 deletions chromium/background-scripts/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

"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"),
Expand All @@ -12,11 +15,12 @@ if (typeof window === "undefined") {
{ TextDecoder } = require('text-encoding'),
chrome = require("sinon-chrome"),
window = { atob, btoa, chrome, crypto, pako, TextDecoder };

combined_update_channels = update_channels;
}

(function(exports) {

const { update_channels } = require('./update_channels');
const util = require('./util');

let store,
Expand All @@ -25,19 +29,36 @@ let store,
// how often we should check for new rulesets
const periodicity = 86400;

// jwk key loaded from keys.js
let imported_keys = {};
for(let update_channel of update_channels){
imported_keys[update_channel.name] = window.crypto.subtle.importKey(
"jwk",
update_channel.jwk,
{
name: "RSA-PSS",
hash: {name: "SHA-256"},
},
false,
["verify"]
);
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.');
}
}
}


Expand Down Expand Up @@ -69,7 +90,7 @@ async function checkForNewRulesets(update_channel) {
// Retrieve the timestamp for when a stored ruleset bundle was published
async function getRulesetTimestamps(){
let timestamp_promises = [];
for(let update_channel of update_channels){
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]);
Expand Down Expand Up @@ -108,48 +129,44 @@ async function getNewRulesets(rulesets_timestamp, update_channel) {
// Otherwise, it throws an exception.
function verifyAndStoreNewRulesets(new_rulesets, rulesets_timestamp, update_channel){
return new Promise((resolve, reject) => {
imported_keys[update_channel.name].then(publicKey => {
window.crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32
},
publicKey,
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);
}
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 {
reject(update_channel.name + ': Downloaded ruleset signature is invalid. Aborting.');
await store.local.set_promise('rulesets: ' + update_channel.name, window.btoa(rulesets_gz));
resolve(true);
}
}).catch(() => {
reject(update_channel.name + ': Downloaded ruleset signature could not be verified. Aborting.');
});
} else {
reject(update_channel.name + ': Downloaded ruleset signature is invalid. Aborting.');
}
}).catch(() => {
reject(update_channel.name + ': Could not import key. Aborting.');
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 update_channels){
for(let update_channel of combined_update_channels){
rulesets_promises.push(new Promise(resolve => {
const key = 'rulesets: ' + update_channel.name
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.');
Expand All @@ -170,7 +187,9 @@ async function applyStoredRulesets(rulesets_obj){
const rulesets_jsons = await Promise.all(rulesets_promises);
if(rulesets_jsons.join("").length > 0){
for(let rulesets_json of rulesets_jsons){
rulesets_obj.addFromJson(rulesets_json.rulesets);
if(typeof(rulesets_json) != 'undefined'){
rulesets_obj.addFromJson(rulesets_json.rulesets);
}
}
} else {
rulesets_obj.addFromJson(util.loadExtensionFile('rules/default.rulesets', 'json'));
Expand All @@ -185,7 +204,7 @@ async function performCheck() {
store.local.set_promise('last-checked', current_timestamp);

let num_updates = 0;
for(let update_channel of update_channels){
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.');
Expand Down Expand Up @@ -214,6 +233,10 @@ chrome.storage.onChanged.addListener(async function(changes, areaName) {
}
}
}

if ('update_channels' in changes) {
await loadUpdateChannelsKeys();
}
});

let initialCheck,
Expand Down Expand Up @@ -241,6 +264,8 @@ async function initialize(store_param, cb){
store = store_param;
background_callback = cb;

await loadUpdateChannelsKeys();

if (await store.get_promise('autoUpdateRulesets', true)) {
await createTimer();
}
Expand Down
51 changes: 36 additions & 15 deletions chromium/pages/options/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,45 @@
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="section-header"><span class="section-header-span" data-i18n="options_generalSettings"></span></div>
<div id="counter-wrapper" class="general-settings-wrapper">
<input type="checkbox" id="showCounter">
<label for="showCounter" data-i18n="menu_showCounter"></label>
</div>
<div id="update-wrapper" class="general-settings-wrapper">
<input type="checkbox" id="autoUpdateRulesets">
<label for="autoUpdateRulesets" data-i18n="options_autoUpdateRulesets"></label>
<span class="section-header-span active" data-show="general-settings-wrapper" data-i18n="options_generalSettings"></span>
<span class="section-header-span inactive" data-show="advanced-settings-wrapper" data-i18n="options_advancedSettings"></span>
<span class="section-header-span inactive" data-show="update-channels-wrapper" data-i18n="options_updateChannels"></span>

<div class="section-wrapper" id="general-settings-wrapper">
<div id="counter-wrapper" class="settings-wrapper">
<input type="checkbox" id="showCounter">
<label for="showCounter" data-i18n="menu_showCounter"></label>
</div>
<div id="update-wrapper" class="settings-wrapper">
<input type="checkbox" id="autoUpdateRulesets">
<label for="autoUpdateRulesets" data-i18n="options_autoUpdateRulesets"></label>
</div>
</div>

<div class="section-header"><span class="section-header-span" data-i18n="options_advancedSettings"></span></div>
<div id="mixed-rulesets-wrapper" class="general-settings-wrapper">
<input type="checkbox" id="enableMixedRulesets">
<label for="enableMixedRulesets" data-i18n="options_enableMixedRulesets"></label>
<div class="section-wrapper" id="advanced-settings-wrapper">
<div id="mixed-rulesets-wrapper" class="settings-wrapper">
<input type="checkbox" id="enableMixedRulesets">
<label for="enableMixedRulesets" data-i18n="options_enableMixedRulesets"></label>
</div>
<div id="show-devtools-tab-wrapper" class="settings-wrapper">
<input type="checkbox" id="showDevtoolsTab">
<label for="showDevtoolsTab" data-i18n="options_showDevtoolsTab"></label>
</div>
</div>
<div id="show-devtools-tab-wrapper" class="general-settings-wrapper">
<input type="checkbox" id="showDevtoolsTab">
<label for="showDevtoolsTab" data-i18n="options_showDevtoolsTab"></label>

<div class="section-wrapper" id="update-channels-wrapper">
<div id="update-channels-error">
<span id="update-channels-error-text"></span>
<img id="update-channels-error-hide" src="/images/remove.png">
</div>
<div id="update-channels-warning" data-i18n="options_updateChannelsWarning"></div>
<div id="update-channels-last-checked"></div>
<div class="clearer"></div>
<div id="update-channels-list"></div>
<div id="add-update-channel-wrapper">
<button id="add-update-channel" data-i18n="options_addUpdateChannel"></button>
<input type="text" id="update-channel-name" />
</div>
</div>

<script src="ux.js"></script>
Expand Down
Loading

0 comments on commit 53c18b6

Please sign in to comment.