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';
|
||||
{
|
||||
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}`;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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