Skip to content

Commit 937fab0

Browse files
committed
Let temporary permissions survive NoScript updates (shameless hack).
1 parent 5c99ed0 commit 937fab0

4 files changed

Lines changed: 247 additions & 12 deletions

File tree

src/bg/LifeCycle.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"use strict";
2+
3+
var LifeCycle = (() => {
4+
5+
const AES = "AES-GCM",
6+
keyUsages = ["encrypt", "decrypt"];
7+
8+
function toBase64(bytes) {
9+
return btoa(Array.from(bytes).map(b => String.fromCharCode(b)).join(''));
10+
}
11+
function fromBase64(string) {
12+
return Uint8Array.from((Array.from(atob(string)).map(c => c.charCodeAt(0))));
13+
}
14+
async function encrypt(clearText) {
15+
let key = await crypto.subtle.generateKey({
16+
name: AES,
17+
length: 256,
18+
},
19+
true,
20+
keyUsages,
21+
);
22+
let iv = crypto.getRandomValues(new Uint8Array(12));
23+
let encoded = new TextEncoder().encode(clearText);
24+
let cypherText = await crypto.subtle.encrypt({
25+
name: AES,
26+
iv
27+
}, key, encoded);
28+
return {cypherText, key: await crypto.subtle.exportKey("jwk", key), iv};
29+
}
30+
31+
var SurvivalTab = {
32+
url: "about:blank",
33+
async createAndStore() {
34+
let allSeen = {};
35+
await Promise.all((await browser.tabs.query({})).map(
36+
async t => {
37+
let seen = await ns.collectSeen(t.id);
38+
if (seen) allSeen[t.id] = seen;
39+
}
40+
));
41+
42+
let {url} = SurvivalTab;
43+
let tabInfo = {
44+
url,
45+
active: false,
46+
};
47+
if (browser.windows) { // it may be missing on mobile
48+
// check if an incognito windows exist and open our "survival" tab there
49+
for (let w of await browser.windows.getAll()) {
50+
if (w.incognito) {
51+
tabInfo.windowId = w.id;
52+
break;
53+
}
54+
}
55+
}
56+
let tab;
57+
for (;!tab;) {
58+
try {
59+
tab = await browser.tabs.create(tabInfo);
60+
} catch (e) {
61+
error(e);
62+
if (tabInfo.windowId) {
63+
// we might not have incognito permissions, let's try using any window
64+
delete tabInfo.windowId;
65+
} else {
66+
return; // bailout
67+
}
68+
}
69+
}
70+
let tabId = tab.id;
71+
72+
let {cypherText, key, iv} = await encrypt(JSON.stringify({
73+
policy: ns.policy.dry(true),
74+
allSeen,
75+
unrestrictedTabs: [...ns.unrestrictedTabs]
76+
}));
77+
78+
await new Promise((resolve, reject) => {
79+
let l = async (tabId, changeInfo) => {
80+
debug("Survival tab updating", changeInfo);
81+
if (changeInfo.status !== "complete") return;
82+
try {
83+
await Messages.send("store", {url, data: toBase64(new Uint8Array(cypherText))}, {tabId, frameId: 0});
84+
resolve();
85+
debug("Survival tab updated");
86+
browser.tabs.onUpdated.removeListener(l);
87+
} catch (e) {
88+
if (!Messages.isMissingEndpoint(e)) {
89+
error(e, "Survival tab failed");
90+
reject(e);
91+
} // otherwise we keep waiting for further updates from the tab until content script is ready to answer
92+
};
93+
}
94+
browser.tabs.onUpdated.addListener(l, {tabId});
95+
});
96+
await Storage.set("local", { "updateInfo": {key, iv: toBase64(iv), tabId}});
97+
debug("Ready to reload...", await Storage.get("local", "updateInfo"));
98+
},
99+
100+
async retrieveAndDestroy() {
101+
let {updateInfo} = await Storage.get("local", "updateInfo");
102+
if (!updateInfo) return;
103+
await Storage.remove("local", "updateInfo");
104+
let {key, iv, tabId} = updateInfo;
105+
key = await crypto.subtle.importKey("jwk", key, AES, true, keyUsages);
106+
iv = fromBase64(iv);
107+
let cypherText = fromBase64(await Messages.send("retrieve",
108+
{url: SurvivalTab.url},
109+
{tabId, frameId: 0}));
110+
let encoded = await crypto.subtle.decrypt({
111+
name: AES,
112+
iv
113+
}, key, cypherText
114+
);
115+
let {policy, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded));
116+
if (!policy) {
117+
error("Ephemeral policy not found!");
118+
return;
119+
}
120+
ns.unrestrictedTabs = new Set(unrestrictedTabs);
121+
browser.tabs.remove(tabId);
122+
await ns.initializing;
123+
ns.policy = new Policy(policy);
124+
await Promise.all(
125+
Object.entries(allSeen).map(
126+
async ([tabId, seen]) => {
127+
try {
128+
debug("Restoring seen %o to tab %s", seen, tabId);
129+
await Messages.send("allSeen", {seen}, {tabId, frameId: 0});
130+
} catch (e) {
131+
error(e, "Cannot send previously seen data to tab", tabId);
132+
}
133+
}
134+
)
135+
)
136+
}
137+
};
138+
139+
return {
140+
async onInstalled(details) {
141+
browser.runtime.onInstalled.removeListener(this.onInstalled);
142+
let {reason, previousVersion} = details;
143+
if (reason !== "update") return;
144+
145+
try {
146+
await SurvivalTab.retrieveAndDestroy();
147+
} catch (e) {
148+
error(e);
149+
}
150+
151+
await include("/lib/Ver.js");
152+
previousVersion = new Ver(previousVersion);
153+
let currentVersion = new Ver(browser.runtime.getManifest().version);
154+
let upgrading = Ver.is(previousVersion, "<=", currentVersion);
155+
if (!upgrading) return;
156+
157+
// put here any version specific upgrade adjustment in stored data
158+
159+
if (Ver.is(previousVersion, "<=", "11.0.10")) {
160+
log(`Upgrading from 11.0.10 or below (${previousVersion}): configure the "ping" capability.`);
161+
await ns.initializing;
162+
ns.policy.TRUSTED.capabilities.add("ping")
163+
await ns.savePolicy();
164+
}
165+
},
166+
167+
async onUpdateAvailable(details) {
168+
await include("/lib/Ver.js");
169+
if (Ver.is(details.version, "<", browser.runtime.getManifest().version)) {
170+
// downgrade: temporary survival might not be supported, and we don't care
171+
return;
172+
}
173+
try {
174+
await SurvivalTab.createAndStore();
175+
} catch (e) {
176+
console.error(e);
177+
} finally {
178+
browser.runtime.reload(); // apply update
179+
}
180+
}
181+
};
182+
})();

src/bg/main.js

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
{
22
'use strict';
33
{
4-
let onInstalled = async details => {
5-
browser.runtime.onInstalled.removeListener(onInstalled);
6-
let {reason, previousVersion} = details;
7-
if (reason !== "update") return;
8-
let v = previousVersion.split(".").map(n => parseInt(n));
9-
if (v[0] > 11 || v[1] > 0 || v[2] > 10) return;
10-
log(`Upgrading from 11.0.10 or below (${previousVersion}): configure the "ping" capability.`);
11-
await ns.initializing;
12-
ns.policy.TRUSTED.capabilities.add("ping")
13-
await ns.savePolicy();
14-
};
15-
browser.runtime.onInstalled.addListener(onInstalled);
4+
for (let event of ["onInstalled", "onUpdateAvailable"]) {
5+
browser.runtime[event].addListener(async details => {
6+
await include("/bg/LifeCycle.js");
7+
LifeCycle[event](details);
8+
});
9+
}
1610
}
1711
let popupURL = browser.extension.getURL("/ui/popup.html");
1812
let popupFor = tabId => `${popupURL}#tab${tabId}`;

src/content/content.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ var seen = {
2525
this._map.set(key, event);
2626
this._list = null;
2727
},
28+
recordAll(events) {
29+
for (let e of events) this.record(e);
30+
},
2831
get list() {
2932
return this._list || (this._list = [...this._map.values()]);
3033
}
@@ -55,10 +58,21 @@ Messages.addHandler({
5558
}
5659
}
5760
},
61+
allSeen(event) {
62+
seen.recordAll(event.seen);
63+
},
5864
collect(event) {
5965
let list = seen.list;
6066
debug("COLLECT", list);
6167
return list;
68+
},
69+
store(event) {
70+
if (document.URL !== event.url) return;
71+
document.documentElement.appendChild(document.createComment(event.data));
72+
},
73+
retrieve(event) {
74+
if (document.URL !== event.url) return;
75+
return document.documentElement.lastChild.textContent;
6276
}
6377
});
6478

src/lib/Ver.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use strict";
2+
class Ver {
3+
constructor(version) {
4+
if (version instanceof Ver) {
5+
this.versionString = version.versionString;
6+
this.parts = version.parts;
7+
} else {
8+
this.versionString = version.toString();
9+
this.parts = this.versionString.split(".");
10+
}
11+
}
12+
toString() {
13+
return this.versionString;
14+
}
15+
compare(other) {
16+
if (!(other instanceof Ver)) other = new Ver(other);
17+
let p1 = this.parts, p2 = other.parts;
18+
let maxParts = Math.max(p1.length, p2.length);
19+
for (let j = 0; j < maxParts; j++) {
20+
let s1 = p1[j] || "0";
21+
let s2 = p2[j] || "0";
22+
if (s1 === s2) continue;
23+
let n1 = parseInt(s1);
24+
let n2 = parseInt(s2);
25+
if (n1 > n2) return 1;
26+
if (n1 < n2) return -1;
27+
// if numeric part is the same, an alphabetic suffix decreases value
28+
// so a "pure number" wins
29+
if (!/\D/.test(s1)) return 1;
30+
if (!/\D/.test(s2)) return -1;
31+
// both have an alhpabetic suffix, let's compare lexicographycally
32+
if (s1 > s2) return 1;
33+
if (s1 < s2) return -1;
34+
}
35+
return 0;
36+
}
37+
static is(ver1, op, ver2) {
38+
let res = new Ver(ver1).compare(ver2);
39+
40+
return op.includes("!=") && res !== 0 ||
41+
op.includes("=") && res === 0 ||
42+
op.includes("<") && res === -1 ||
43+
op.includes(">") && res === 1;
44+
}
45+
}

0 commit comments

Comments
 (0)