Let temporary permissions survive NoScript updates (shameless hack).

This commit is contained in:
hackademix 2020-08-18 00:13:21 +02:00
parent 5c99ed053b
commit 937fab04d2
4 changed files with 247 additions and 12 deletions

182
src/bg/LifeCycle.js Normal file
View File

@ -0,0 +1,182 @@
"use strict";
var LifeCycle = (() => {
const AES = "AES-GCM",
keyUsages = ["encrypt", "decrypt"];
function toBase64(bytes) {
return btoa(Array.from(bytes).map(b => String.fromCharCode(b)).join(''));
}
function fromBase64(string) {
return Uint8Array.from((Array.from(atob(string)).map(c => c.charCodeAt(0))));
}
async function encrypt(clearText) {
let key = await crypto.subtle.generateKey({
name: AES,
length: 256,
},
true,
keyUsages,
);
let iv = crypto.getRandomValues(new Uint8Array(12));
let encoded = new TextEncoder().encode(clearText);
let cypherText = await crypto.subtle.encrypt({
name: AES,
iv
}, key, encoded);
return {cypherText, key: await crypto.subtle.exportKey("jwk", key), iv};
}
var SurvivalTab = {
url: "about:blank",
async createAndStore() {
let allSeen = {};
await Promise.all((await browser.tabs.query({})).map(
async t => {
let seen = await ns.collectSeen(t.id);
if (seen) allSeen[t.id] = seen;
}
));
let {url} = SurvivalTab;
let tabInfo = {
url,
active: false,
};
if (browser.windows) { // it may be missing on mobile
// check if an incognito windows exist and open our "survival" tab there
for (let w of await browser.windows.getAll()) {
if (w.incognito) {
tabInfo.windowId = w.id;
break;
}
}
}
let tab;
for (;!tab;) {
try {
tab = await browser.tabs.create(tabInfo);
} catch (e) {
error(e);
if (tabInfo.windowId) {
// we might not have incognito permissions, let's try using any window
delete tabInfo.windowId;
} else {
return; // bailout
}
}
}
let tabId = tab.id;
let {cypherText, key, iv} = await encrypt(JSON.stringify({
policy: ns.policy.dry(true),
allSeen,
unrestrictedTabs: [...ns.unrestrictedTabs]
}));
await new Promise((resolve, reject) => {
let l = async (tabId, changeInfo) => {
debug("Survival tab updating", changeInfo);
if (changeInfo.status !== "complete") return;
try {
await Messages.send("store", {url, data: toBase64(new Uint8Array(cypherText))}, {tabId, frameId: 0});
resolve();
debug("Survival tab updated");
browser.tabs.onUpdated.removeListener(l);
} catch (e) {
if (!Messages.isMissingEndpoint(e)) {
error(e, "Survival tab failed");
reject(e);
} // otherwise we keep waiting for further updates from the tab until content script is ready to answer
};
}
browser.tabs.onUpdated.addListener(l, {tabId});
});
await Storage.set("local", { "updateInfo": {key, iv: toBase64(iv), tabId}});
debug("Ready to reload...", await Storage.get("local", "updateInfo"));
},
async retrieveAndDestroy() {
let {updateInfo} = await Storage.get("local", "updateInfo");
if (!updateInfo) return;
await Storage.remove("local", "updateInfo");
let {key, iv, tabId} = updateInfo;
key = await crypto.subtle.importKey("jwk", key, AES, true, keyUsages);
iv = fromBase64(iv);
let cypherText = fromBase64(await Messages.send("retrieve",
{url: SurvivalTab.url},
{tabId, frameId: 0}));
let encoded = await crypto.subtle.decrypt({
name: AES,
iv
}, key, cypherText
);
let {policy, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded));
if (!policy) {
error("Ephemeral policy not found!");
return;
}
ns.unrestrictedTabs = new Set(unrestrictedTabs);
browser.tabs.remove(tabId);
await ns.initializing;
ns.policy = new Policy(policy);
await Promise.all(
Object.entries(allSeen).map(
async ([tabId, seen]) => {
try {
debug("Restoring seen %o to tab %s", seen, tabId);
await Messages.send("allSeen", {seen}, {tabId, frameId: 0});
} catch (e) {
error(e, "Cannot send previously seen data to tab", tabId);
}
}
)
)
}
};
return {
async onInstalled(details) {
browser.runtime.onInstalled.removeListener(this.onInstalled);
let {reason, previousVersion} = details;
if (reason !== "update") return;
try {
await SurvivalTab.retrieveAndDestroy();
} catch (e) {
error(e);
}
await include("/lib/Ver.js");
previousVersion = new Ver(previousVersion);
let currentVersion = new Ver(browser.runtime.getManifest().version);
let upgrading = Ver.is(previousVersion, "<=", currentVersion);
if (!upgrading) return;
// put here any version specific upgrade adjustment in stored data
if (Ver.is(previousVersion, "<=", "11.0.10")) {
log(`Upgrading from 11.0.10 or below (${previousVersion}): configure the "ping" capability.`);
await ns.initializing;
ns.policy.TRUSTED.capabilities.add("ping")
await ns.savePolicy();
}
},
async onUpdateAvailable(details) {
await include("/lib/Ver.js");
if (Ver.is(details.version, "<", browser.runtime.getManifest().version)) {
// downgrade: temporary survival might not be supported, and we don't care
return;
}
try {
await SurvivalTab.createAndStore();
} catch (e) {
console.error(e);
} finally {
browser.runtime.reload(); // apply update
}
}
};
})();

View File

@ -1,18 +1,12 @@
{
'use strict';
{
let onInstalled = async details => {
browser.runtime.onInstalled.removeListener(onInstalled);
let {reason, previousVersion} = details;
if (reason !== "update") return;
let v = previousVersion.split(".").map(n => parseInt(n));
if (v[0] > 11 || v[1] > 0 || v[2] > 10) return;
log(`Upgrading from 11.0.10 or below (${previousVersion}): configure the "ping" capability.`);
await ns.initializing;
ns.policy.TRUSTED.capabilities.add("ping")
await ns.savePolicy();
};
browser.runtime.onInstalled.addListener(onInstalled);
for (let event of ["onInstalled", "onUpdateAvailable"]) {
browser.runtime[event].addListener(async details => {
await include("/bg/LifeCycle.js");
LifeCycle[event](details);
});
}
}
let popupURL = browser.extension.getURL("/ui/popup.html");
let popupFor = tabId => `${popupURL}#tab${tabId}`;

View File

@ -25,6 +25,9 @@ var seen = {
this._map.set(key, event);
this._list = null;
},
recordAll(events) {
for (let e of events) this.record(e);
},
get list() {
return this._list || (this._list = [...this._map.values()]);
}
@ -55,10 +58,21 @@ Messages.addHandler({
}
}
},
allSeen(event) {
seen.recordAll(event.seen);
},
collect(event) {
let list = seen.list;
debug("COLLECT", list);
return list;
},
store(event) {
if (document.URL !== event.url) return;
document.documentElement.appendChild(document.createComment(event.data));
},
retrieve(event) {
if (document.URL !== event.url) return;
return document.documentElement.lastChild.textContent;
}
});

45
src/lib/Ver.js Normal file
View File

@ -0,0 +1,45 @@
"use strict";
class Ver {
constructor(version) {
if (version instanceof Ver) {
this.versionString = version.versionString;
this.parts = version.parts;
} else {
this.versionString = version.toString();
this.parts = this.versionString.split(".");
}
}
toString() {
return this.versionString;
}
compare(other) {
if (!(other instanceof Ver)) other = new Ver(other);
let p1 = this.parts, p2 = other.parts;
let maxParts = Math.max(p1.length, p2.length);
for (let j = 0; j < maxParts; j++) {
let s1 = p1[j] || "0";
let s2 = p2[j] || "0";
if (s1 === s2) continue;
let n1 = parseInt(s1);
let n2 = parseInt(s2);
if (n1 > n2) return 1;
if (n1 < n2) return -1;
// if numeric part is the same, an alphabetic suffix decreases value
// so a "pure number" wins
if (!/\D/.test(s1)) return 1;
if (!/\D/.test(s2)) return -1;
// both have an alhpabetic suffix, let's compare lexicographycally
if (s1 > s2) return 1;
if (s1 < s2) return -1;
}
return 0;
}
static is(ver1, op, ver2) {
let res = new Ver(ver1).compare(ver2);
return op.includes("!=") && res !== 0 ||
op.includes("=") && res === 0 ||
op.includes("<") && res === -1 ||
op.includes(">") && res === 1;
}
}