2023-06-06 15:33:17 -07:00
|
|
|
/**
|
|
|
|
* @file worker.js
|
|
|
|
* A background script for the CrowdTLS project.
|
|
|
|
* This script is used to process web requests and retrieve security information
|
|
|
|
* for each HTTPS response.
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2023-06-07 14:36:03 -07:00
|
|
|
// const API_BASE = "https://crowdtls.mips.uk/api/v1";
|
|
|
|
const API_BASE = "http://127.0.0.1:8000/api/v1";
|
2023-06-06 15:33:17 -07:00
|
|
|
|
2023-06-16 13:25:55 -07:00
|
|
|
let send_buffer = {};
|
|
|
|
|
2023-06-06 15:33:17 -07:00
|
|
|
/**
|
|
|
|
* Processes the given request details to extract and log security information.
|
|
|
|
*
|
|
|
|
* @async
|
|
|
|
* @function
|
|
|
|
* @param {Object} details - The details of the request.
|
|
|
|
* @param {string} details.requestId - The unique identifier for the request.
|
|
|
|
* @throws Will throw an error if the request fails or if the security state is insecure.
|
|
|
|
* @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/getSecurityInfo}
|
|
|
|
*/
|
|
|
|
async function process_request(details) {
|
|
|
|
try {
|
2023-06-07 14:36:03 -07:00
|
|
|
let hostname = (new URL(details.url)).hostname;
|
2023-06-06 15:33:17 -07:00
|
|
|
let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, { certificateChain: true, rawDER: true });
|
|
|
|
if (securityInfo.state !== "insecure") {
|
|
|
|
const fingerprint = securityInfo.certificates[0].fingerprint.sha256;
|
2023-06-16 13:25:55 -07:00
|
|
|
|
2023-06-06 15:33:17 -07:00
|
|
|
let fpData = localStorage.getItem(fingerprint);
|
|
|
|
|
|
|
|
const currentTime = Date.now();
|
|
|
|
if (fpData) {
|
|
|
|
fpData = JSON.parse(fpData);
|
|
|
|
// If the last request was less than 10 minutes ago, don't spam the API
|
|
|
|
if (fpData.lastCheck && currentTime - fpData.lastCheck < 10 * 60 * 1000) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// If the last response was less than 12 hours ago, don't check again
|
|
|
|
if (fpData.lastResponse && currentTime - fpData.lastResponse < 12 * 60 * 60 * 1000) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fpData = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
fpData.lastCheck = currentTime;
|
|
|
|
localStorage.setItem(fingerprint, JSON.stringify(fpData));
|
|
|
|
|
2023-06-16 13:25:55 -07:00
|
|
|
const tab = details.tabId;
|
|
|
|
if (!send_buffer[tab]) {
|
|
|
|
send_buffer[tab] = {};
|
|
|
|
}
|
|
|
|
if (!send_buffer[tab][hostname]) {
|
|
|
|
send_buffer[tab][hostname] = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
securityInfo.certificates.forEach(cert => {
|
|
|
|
send_buffer[tab][hostname][cert.fingerprint.sha256] = cert.rawDER;
|
|
|
|
});
|
2023-06-06 15:33:17 -07:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks the fingerprint and sends a REST GET request to the API.
|
|
|
|
*
|
|
|
|
* @async
|
|
|
|
* @function
|
2023-06-16 13:25:55 -07:00
|
|
|
* @param {Object} buffer - The buffer containing info for a specific tab
|
2023-06-06 15:33:17 -07:00
|
|
|
*/
|
2023-06-16 13:25:55 -07:00
|
|
|
async function do_checks(buffer) {
|
|
|
|
let fingerprint_buffer = {};
|
|
|
|
Object.keys(buffer).forEach(hostname => {
|
|
|
|
fingerprint_buffer[hostname] = Object.keys(buffer[hostname]);
|
|
|
|
});
|
2023-06-06 15:33:17 -07:00
|
|
|
|
2023-06-16 13:25:55 -07:00
|
|
|
try {
|
2023-06-06 15:33:17 -07:00
|
|
|
const response = await fetch(`${API_BASE}/check`, {
|
|
|
|
method: 'POST',
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
2023-06-16 13:25:55 -07:00
|
|
|
body: JSON.stringify(fingerprint_buffer),
|
2023-06-06 15:33:17 -07:00
|
|
|
timeout: 5000
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
console.error(`Error: ${response.status}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
2023-06-16 13:25:55 -07:00
|
|
|
Object.keys(buffer).forEach(hostname => {
|
|
|
|
if (!data[hostname]) {
|
|
|
|
delete buffer[hostname];
|
2023-06-06 15:33:17 -07:00
|
|
|
}
|
2023-06-16 13:25:55 -07:00
|
|
|
});
|
|
|
|
send_certificate_chain(buffer);
|
2023-06-06 15:33:17 -07:00
|
|
|
}
|
|
|
|
catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends the certificate chain to the CrowdTLS service.
|
|
|
|
*
|
|
|
|
* @async
|
|
|
|
* @function
|
2023-06-16 13:25:55 -07:00
|
|
|
* @param {Object} chain - The buffer containing info for a specific tab
|
2023-06-06 15:33:17 -07:00
|
|
|
* @throws Will log an error to the console if the request fails.
|
|
|
|
*/
|
2023-06-16 13:25:55 -07:00
|
|
|
async function send_certificate_chain(chain) {
|
2023-06-06 15:33:17 -07:00
|
|
|
try {
|
2023-06-07 14:36:03 -07:00
|
|
|
const response = await fetch(`${API_BASE}/new`, {
|
2023-06-06 15:33:17 -07:00
|
|
|
method: 'POST',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
2023-06-16 13:25:55 -07:00
|
|
|
body: JSON.stringify(chain),
|
2023-06-06 15:33:17 -07:00
|
|
|
timeout: 10000
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
console.error(`Request returned with status ${response.status}`);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-06-16 13:25:55 -07:00
|
|
|
* Performs garbage collection on localStorage by removing objects older than 12 hours based on their "lastCheck" property,
|
|
|
|
* and removes entries from send_buffer if that tab ID is no longer open.
|
|
|
|
*
|
2023-06-06 15:33:17 -07:00
|
|
|
* @function
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2023-06-16 13:25:55 -07:00
|
|
|
async function garbage_collection() {
|
2023-06-06 15:33:17 -07:00
|
|
|
const now = new Date().getTime();
|
|
|
|
const twelveHoursInMillis = 12 * 60 * 60 * 1000;
|
|
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
|
|
const key = localStorage.key(i);
|
|
|
|
const value = localStorage.getItem(key);
|
|
|
|
if (value) {
|
|
|
|
const entry = JSON.parse(value);
|
|
|
|
if (entry.lastCheck && now - entry.lastCheck > twelveHoursInMillis) {
|
|
|
|
localStorage.removeItem(key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-06-16 13:25:55 -07:00
|
|
|
|
|
|
|
const tabs = await browser.tabs.query({});
|
|
|
|
const open_tabs = tabs.map(tab => tab.id);
|
|
|
|
|
|
|
|
for (const tabId in send_buffer) {
|
|
|
|
if (!open_tabs.includes(Number(tabId))) {
|
|
|
|
delete send_buffer[tabId];
|
|
|
|
}
|
|
|
|
}
|
2023-06-06 15:33:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
setInterval(garbage_collection, 3600000);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The listener for HTTPS responses. Schedules the process_request() function for every response.
|
|
|
|
* Blocking is necessary to ensure that the response is not delivered before the function is completed.
|
|
|
|
*
|
|
|
|
* @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onHeadersReceived}
|
|
|
|
*/
|
|
|
|
browser.webRequest.onHeadersReceived.addListener(
|
|
|
|
process_request,
|
|
|
|
{ urls: ["https://*/*"] },
|
|
|
|
["blocking"]
|
2023-06-16 13:25:55 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
// When a tab update happens (i.e., page load complete), send the buffered data
|
|
|
|
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|
|
|
if (changeInfo.status === "complete" && send_buffer[tabId]) {
|
|
|
|
await do_checks(send_buffer[tabId]);
|
|
|
|
delete send_buffer[tabId];
|
|
|
|
}
|
|
|
|
});
|