-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
371 lines (319 loc) · 12.8 KB
/
Copy pathbackground.js
File metadata and controls
371 lines (319 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
let waitingForRequestPage = true;
let requestPopupWindow;
let currentAccount;
let portToOffscreen;
chrome.runtime.onConnect.addListener(async function (port) {
}); // just accept connection
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.fromOffscreen) {
return handleOffscreenMessages(request, sender, sendResponse);
}
else if (request.message === 'requestPageLoaded') {
waitingForRequestPage = false;
sendResponse({ success: true });
return false; // Synchronous response
}
else if (request.message === 'onNovioTxRequest') {
//You need to be authenticated to send requests.
getTabWallet('' + sender.tab?.id).then((assignedWallet) => {
if (assignedWallet == null) {
sendResponse({
error: `Not authenticated`
})
return false;
}
if (requestPopupWindow) {
chrome.windows.remove(requestPopupWindow.id);
requestPopupWindow = null;
}
if (chrome.runtime.lastError) {
sendResponse({ error: chrome.runtime.lastError.message });
return;
}
openTransactionRequest(request, sendResponse, sender, assignedWallet);
return true;
})
return true;
}
else if (request.message === 'onNovioSignRequest') {
//first open the wallet if the extension is locked.
const readyForRequest = new Promise((resolve) => {
if (currentAccount == null) {
chrome.action.openPopup().catch((err) => {
console.error("Failed to open popup:", err);
});
chrome.runtime.onMessage.addListener((innerRequest, sender, sendResponse) => {
if (innerRequest.message === 'NovioAccountOpened') {
resolve();
}
});
}
else {
resolve();
}
})
readyForRequest.then(() => {
if (requestPopupWindow) {
chrome.windows.remove(requestPopupWindow.id);
requestPopupWindow = null;
}
if (chrome.runtime.lastError) {
sendResponse({ error: chrome.runtime.lastError.message });
return;
}
openSignRequest(request, sendResponse, sender);
})
return true; // We're sending an async response
}
else if (request.message === 'onNovioClientSend') {
chrome.runtime.sendMessage({
target: 'offscreen',
message: 'clientSend',
data: request.data,
tabId: sender.tab?.id,
});
sendResponse({ success: true });
return false; // Synchronous response
}
else if (request.message === 'NovioAccountOpened') {
currentAccount = request.account;
sendResponse({ success: true });
return false; // Synchronous response
}
// For any unhandled messages
sendResponse({ error: 'Unknown message type' });
return false;
});
async function openSignRequest(request, sendResponse, sender) {
chrome.windows.get(sender.tab.windowId, async (tabWindow) => {
const width = 440;
let height = 310;
const url = 'request/sign.html';
if (request.allowClient) {
height += 20;
}
const left = Math.round((tabWindow.width - width) * 0.5 + tabWindow.left)
const top = Math.round((tabWindow.height - height) * 0.5 + tabWindow.top)
waitingForRequestPage = true;
requestPopupWindow = await chrome.windows.create({
width: width,
height: height,
top: Math.round(top),
left: Math.round(left),
focused: true,
type: 'popup',
url: url,
});
setTimeout(async () => {
while (waitingForRequestPage) {
await sleep(50);
}
if (request.message === 'onNovioSignRequest') {
chrome.runtime.sendMessage({
message: "signRequestData",
data: request.data,
allowClient: request.allowClient,
account: currentAccount
}, async (result) => {
if (chrome.runtime.lastError) {
sendResponse({
error: 'cancelled'
});
} else {
if (requestPopupWindow) {
chrome.windows.remove(requestPopupWindow.id);
requestPopupWindow = null;
}
setTabWallet('' + sender.tab?.id, currentAccount.Name);
if (request.allowClient) {
try {
await chrome.offscreen.closeDocument();
} catch (error) {
//We clean up first, but if there is no offscreen document we dont want any errors to show, thats fine.
}
await setupOffscreenSandbox(sender);
}
sendResponse(result);
}
requestPopupWindow = null;
});
}
}, 50);
})
}
async function openTransactionRequest(request, sendResponse, sender, assignedWallet) {
chrome.windows.get(sender.tab.windowId, async (tabWindow) => {
const width = 420;
const height = 280;
const url = 'request/request.html';
const left = Math.round((tabWindow.width - width) * 0.5 + tabWindow.left)
const top = Math.round((tabWindow.height - height) * 0.5 + tabWindow.top)
waitingForRequestPage = true;
requestPopupWindow = await chrome.windows.create({
width: width,
height: height,
top: Math.round(top),
left: Math.round(left),
focused: true,
type: 'popup',
url: url,
});
setTimeout(async () => {
while (waitingForRequestPage) {
await sleep(50);
}
chrome.runtime.sendMessage({
message: "transactionRequestData",
data: request.data,
assignedWalletName: assignedWallet
}, (result) => {
if (chrome.runtime.lastError) {
sendResponse({
error: 'cancelled'
});
} else {
sendResponse(result);
}
requestPopupWindow = null;
});
}, 50);
})
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
let creating; // A global promise to avoid concurrency issues
async function setupOffscreenSandbox(sender) {
// Check all windows controlled by the service worker to see if one
// of them is the offscreen document with the given path
const offscreenUrl = chrome.runtime.getURL('offscreen.html');
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [offscreenUrl]
});
if (existingContexts.length > 0) {
return;
}
// create offscreen document
if (creating) {
await creating;
} else {
creating = chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: [chrome.offscreen.Reason.IFRAME_SCRIPTING],
justification: 'secure sandbox nkn client',
});
let created = await creating;
creating = null;
await authenticateOffscreenSecurely(sender);
return created;
}
}
async function authenticateOffscreenSecurely(sender) {
if (!portToOffscreen) {
portToOffscreen = chrome.runtime.connect({ name: "secure-auth-channel" });
portToOffscreen.onDisconnect.addListener(function () {
console.log('we disconnected with offscreen.');
chrome.tabs.sendMessage(
portToOffscreen.clientTabId,
{ message: 'NovioDisconnectedClient', target: 'content' }
);
portToOffscreen = undefined;
});
}
let lastUsedName = (await chrome.storage.local.get(["lastUsedAccountName"])).lastUsedAccountName;
const storedSession = (await chrome.storage.session.get(["session"])).session;
if (!storedSession) return;
let decrypted = await aesGcmDecrypt(storedSession.a, storedSession.b);
let accounts = await chrome.storage.local.get(["accountStore"]);
let walletJSON = accounts.accountStore[lastUsedName];
portToOffscreen.clientTabId = sender.tab?.id;
portToOffscreen.postMessage({
message: "clientAuthenticate",
walletJson: walletJSON,
walletPassword: decrypted,
clientTabId: sender.tab?.id,
});
}
async function handleOffscreenMessages(request, sender, sendResponse) {
if (request.clientTabId == null) {
throw new Error("Offscreen message received which has no target tab.");
}
//This is how we close the offscreen document.
//TODO: identify when the website disconnects, closes/refreshes tab, so we can close offscreen.
//chrome.offscreen.closeDocument();
if (request.message === 'onNovioClientPing') {
chrome.tabs.sendMessage(
request.clientTabId,
{ message: 'onNovioClientPing', target: 'content', data: request.data }
).then(response => {
sendResponse({ success: true, response });
}).catch((onError) => {
console.warn('client connection to tab lost, closing offscreen document...', onError);
try {
chrome.offscreen.closeDocument();
} catch (error) {
}
sendResponse({ error: onError.message });
});
return true; // We're sending an async response
}
else if (request.message === 'NovioConnectedClient') {
chrome.tabs.sendMessage(
request.clientTabId,
{ message: 'NovioConnectedClient', target: 'content', data: request.data }
);
sendResponse({ success: true });
return false; // We're sending an async response
}
else if (request.message === 'NovioDisconnectedClient') {
chrome.tabs.sendMessage(
request.clientTabId,
{ message: 'NovioDisconnectedClient', target: 'content', data: request.data }
);
sendResponse({ success: true });
return false; // We're sending an async response
}
else if (request.message === 'onNovioClientMessage') {
chrome.tabs.sendMessage(
request.clientTabId,
{ message: 'onNovioClientMessage', target: 'content', data: request.data }
).then(() => {
sendResponse({ success: true });
}).catch((error) => {
sendResponse({ error: error.message });
});
return true; // We're sending an async response
}
}
async function aesGcmDecrypt(ciphertext, password) {
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
const ivStr = atob(ciphertext).slice(0, 12); // decode base64 iv
const iv = new Uint8Array(Array.from(ivStr).map(ch => ch.charCodeAt(0))); // iv as Uint8Array
const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // generate key from pw
const ctStr = atob(ciphertext).slice(12); // decode base64 ciphertext
const ctUint8 = new Uint8Array(Array.from(ctStr).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array
// note: why doesn't ctUint8 = new TextEncoder().encode(ctStr) work?
try {
const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key
const plaintext = new TextDecoder().decode(plainBuffer); // plaintext from ArrayBuffer
return plaintext; // return the plaintext
} catch (e) {
throw new Error('Decrypt failed');
}
}
async function setTabWallet(tabId, name) {
await chrome.storage.session.set({
[tabId]: name
});
}
async function getTabWallet(tabId) {
try {
const result = await chrome.storage.session.get([tabId]);
return result[tabId];
} catch (error) {
}
return null;
}