Let temporary permissions survive NoScript updates (shameless hack).
This commit is contained in:
parent
5c99ed053b
commit
937fab04d2
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
|
@ -1,18 +1,12 @@
|
||||||
{
|
{
|
||||||
'use strict';
|
'use strict';
|
||||||
{
|
{
|
||||||
let onInstalled = async details => {
|
for (let event of ["onInstalled", "onUpdateAvailable"]) {
|
||||||
browser.runtime.onInstalled.removeListener(onInstalled);
|
browser.runtime[event].addListener(async details => {
|
||||||
let {reason, previousVersion} = details;
|
await include("/bg/LifeCycle.js");
|
||||||
if (reason !== "update") return;
|
LifeCycle[event](details);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
let popupURL = browser.extension.getURL("/ui/popup.html");
|
let popupURL = browser.extension.getURL("/ui/popup.html");
|
||||||
let popupFor = tabId => `${popupURL}#tab${tabId}`;
|
let popupFor = tabId => `${popupURL}#tab${tabId}`;
|
||||||
|
|
|
@ -25,6 +25,9 @@ var seen = {
|
||||||
this._map.set(key, event);
|
this._map.set(key, event);
|
||||||
this._list = null;
|
this._list = null;
|
||||||
},
|
},
|
||||||
|
recordAll(events) {
|
||||||
|
for (let e of events) this.record(e);
|
||||||
|
},
|
||||||
get list() {
|
get list() {
|
||||||
return this._list || (this._list = [...this._map.values()]);
|
return this._list || (this._list = [...this._map.values()]);
|
||||||
}
|
}
|
||||||
|
@ -55,10 +58,21 @@ Messages.addHandler({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
allSeen(event) {
|
||||||
|
seen.recordAll(event.seen);
|
||||||
|
},
|
||||||
collect(event) {
|
collect(event) {
|
||||||
let list = seen.list;
|
let list = seen.list;
|
||||||
debug("COLLECT", list);
|
debug("COLLECT", list);
|
||||||
return 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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue