/** * @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"; // const API_BASE = "https://crowdtls.mips.uk/api/v1"; const API_BASE = "http://127.0.0.1:8000/api/v1"; let send_buffer = {}; /** * 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 { let hostname = (new URL(details.url)).hostname; let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, { certificateChain: true, rawDER: true }); if (securityInfo.certificates.length > 0) { const currentTime = Date.now(); securityInfo.certificates.forEach(cert => { const fingerprint = cert.fingerprint.sha256; let fpData = localStorage.getItem(fingerprint); 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)); const tab = details.tabId; if (!send_buffer[tab]) { send_buffer[tab] = {}; } if (!send_buffer[tab][hostname]) { send_buffer[tab][hostname] = {}; } send_buffer[tab][hostname][fingerprint] = cert.rawDER; }); } } catch (error) { console.error(error); } } function generate_alerts(fingerprints) { fingerprints.forEach(fingerprint => { let fpData = localStorage.getItem(fingerprint); fpData = JSON.parse(fpData); if (fpData.alert === true) { browser.notifications.create({ "type": "basic", "iconUrl": browser.runtime.getURL("icons/crowdtls-48.png"), "title": "CrowdTLS Alert", "message": fpData.alertMsg }); } }); } /** * Checks the fingerprint and sends a REST GET request to the API. * * @async * @function * @param {Object} buffer - The buffer containing info for a specific tab */ async function do_checks(buffer) { let fingerprint_buffer = {}; Object.keys(buffer).forEach(hostname => { fingerprint_buffer[hostname] = Object.keys(buffer[hostname]); }); try { const response = await fetch(`${API_BASE}/check`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(fingerprint_buffer), timeout: 5000 }); if (!response.ok) { console.error(`Error: ${response.status}`); return; } const data = await response.json(); if (data["anomalies"]) { const currentTime = Date.now(); Object.keys(data["anomalies"]).forEach(fingerprint => { let fpData = localStorage.getItem(fingerprint); fpData = JSON.parse(fpData); fpData.lastCheck = currentTime; fpData.lastResponse = currentTime; fpData.alert = true; fpData.alertTime = currentTime; fpData.alertMsg = data["anomalies"][fingerprint]; localStorage.setItem(fingerprint, JSON.stringify(fpData)); }); generate_alerts(Object.keys(data["anomalies"])); } Object.keys(buffer).forEach(hostname => { if (!data[hostname]) { delete buffer[hostname]; } }); await send_certificate_chain(buffer); } catch (error) { console.error(error); } } /** * Sends the certificate chain to the CrowdTLS service. * * @async * @function * @param {Object} chain - The buffer containing info for a specific tab * @throws Will log an error to the console if the request fails. */ async function send_certificate_chain(chain) { try { const response = await fetch(`${API_BASE}/new`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(chain), timeout: 10000 }); if (!response.ok) { console.error(`Request returned with status ${response.status}`); } } catch (error) { console.error(error); } } /** * 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. * * @function * @returns {void} */ async function garbage_collection() { 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); } } } 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]; } } } 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"] ); // 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 || tab.status) == 'complete' && send_buffer[tabId]) { await do_checks(send_buffer[tabId]); delete send_buffer[tabId]; } });