From ba6449c5b015ac9f2581e8899039a6d45f94947a Mon Sep 17 00:00:00 2001 From: sal Date: Sat, 28 Feb 2026 23:43:12 -0600 Subject: [PATCH] Add options page with vector controls, whitelist, and container management - Options page (open_in_tab) with 4 sections: fingerprint vector toggles, domain whitelist, container table with delete, and bulk actions - Per-vector spoofing control: 12 independent toggles (canvas, WebGL, audio, navigator, screen, timezone, WebRTC, fonts, clientRects, plugins, battery, connection) - Domain whitelist bypasses containerization entirely - Delete individual containers from options UI - Remove dead code (tabOrigins, getContainerDomain) - Refactor profile registration into buildProfileAndRegister helper --- background.js | 104 ++++++++----- inject.js | 359 ++++++++++++++++++++++--------------------- manifest.json | 4 + options/options.css | 71 +++++++++ options/options.html | 64 ++++++++ options/options.js | 226 +++++++++++++++++++++++++++ 6 files changed, 617 insertions(+), 211 deletions(-) create mode 100644 options/options.css create mode 100644 options/options.html create mode 100644 options/options.js diff --git a/background.js b/background.js index cbdbfbf..9a4b989 100644 --- a/background.js +++ b/background.js @@ -5,7 +5,7 @@ const registeredScripts = {}; // cookieStoreId -> RegisteredContentScript let injectSourceCache = null; let domainMap = {}; // baseDomain -> cookieStoreId let pendingTabs = {}; // tabId -> true (tabs being redirected) -let tabOrigins = {}; // tabId -> cookieStoreId (tracks which container a tab was assigned to) +let cachedWhitelist = []; // domains that bypass containerization const CONTAINER_COLORS = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple"]; const CONTAINER_ICONS = ["fingerprint", "fence", "briefcase", "cart", "circle", "gift", "tree", "chill"]; @@ -82,6 +82,13 @@ async function registerForContainer(cookieStoreId, profile) { }); } +async function buildProfileAndRegister(cookieStoreId, seed) { + const profile = generateFingerprintProfile(seed); + const vsStored = await browser.storage.local.get("vectorSettings"); + profile.vectors = vsStored.vectorSettings || {}; + await registerForContainer(cookieStoreId, profile); +} + async function registerAllKnownContainers() { const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]); const seeds = stored.containerSeeds || {}; @@ -97,8 +104,7 @@ async function registerAllKnownContainers() { for (const [cookieStoreId, seed] of Object.entries(seeds)) { const cfg = settings[cookieStoreId] || { enabled: true }; if (!cfg.enabled) continue; - const profile = generateFingerprintProfile(seed); - await registerForContainer(cookieStoreId, profile); + await buildProfileAndRegister(cookieStoreId, seed); } } @@ -139,8 +145,7 @@ async function getOrCreateContainerForDomain(baseDomain) { seeds[cookieStoreId] = generateSeed(); await browser.storage.local.set({ containerSeeds: seeds }); - const profile = generateFingerprintProfile(seeds[cookieStoreId]); - await registerForContainer(cookieStoreId, profile); + await buildProfileAndRegister(cookieStoreId, seeds[cookieStoreId]); return cookieStoreId; } @@ -148,18 +153,12 @@ async function getOrCreateContainerForDomain(baseDomain) { // tabId -> baseDomain — tabs we just created, skip only for the same domain const createdByUs = {}; -// Reverse lookup: find what domain a container was created for -function getContainerDomain(cookieStoreId) { - for (const [domain, cid] of Object.entries(domainMap)) { - if (cid === cookieStoreId) return domain; - } - return null; -} - // Handle a tab that needs to be in a container for a given domain async function assignTabToContainer(tabId, url, baseDomain) { // Skip tabs we just created — but only for the domain we created them for if (createdByUs[tabId] === baseDomain) return; + // Skip whitelisted domains + if (cachedWhitelist.includes(baseDomain)) return; if (pendingTabs[tabId]) return; pendingTabs[tabId] = true; @@ -232,30 +231,22 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { // Clean up tab tracking when tabs close browser.tabs.onRemoved.addListener((tabId) => { delete pendingTabs[tabId]; - delete tabOrigins[tabId]; }); -// --- Message Handling (from popup) --- +// --- Message Handling (from popup and options page) --- browser.runtime.onMessage.addListener((message, sender) => { - if (message.type === "getContainerList") { - return handleGetContainerList(); - } - if (message.type === "toggleContainer") { - return handleToggle(message.cookieStoreId, message.enabled); - } - if (message.type === "regenerateFingerprint") { - return handleRegenerate(message.cookieStoreId); - } - if (message.type === "regenerateAll") { - return handleRegenerateAll(); - } - if (message.type === "resetAll") { - return handleResetAll(); - } - if (message.type === "pruneContainers") { - return handlePruneContainers(); - } + if (message.type === "getContainerList") return handleGetContainerList(); + if (message.type === "toggleContainer") return handleToggle(message.cookieStoreId, message.enabled); + if (message.type === "regenerateFingerprint") return handleRegenerate(message.cookieStoreId); + if (message.type === "regenerateAll") return handleRegenerateAll(); + if (message.type === "resetAll") return handleResetAll(); + if (message.type === "pruneContainers") return handlePruneContainers(); + if (message.type === "deleteContainer") return handleDeleteContainer(message.cookieStoreId); + if (message.type === "getWhitelist") return handleGetWhitelist(); + if (message.type === "setWhitelist") return handleSetWhitelist(message.whitelist); + if (message.type === "getVectorSettings") return handleGetVectorSettings(); + if (message.type === "setVectorSettings") return handleSetVectorSettings(message.vectorSettings); }); async function handleGetContainerList() { @@ -295,8 +286,7 @@ async function handleToggle(cookieStoreId, enabled) { const seedStored = await browser.storage.local.get("containerSeeds"); const seeds = seedStored.containerSeeds || {}; if (seeds[cookieStoreId]) { - const profile = generateFingerprintProfile(seeds[cookieStoreId]); - await registerForContainer(cookieStoreId, profile); + await buildProfileAndRegister(cookieStoreId, seeds[cookieStoreId]); } } return { ok: true }; @@ -312,8 +302,7 @@ async function handleRegenerate(cookieStoreId) { const cfg = settings[cookieStoreId] || { enabled: true }; if (cfg.enabled) { - const profile = generateFingerprintProfile(seeds[cookieStoreId]); - await registerForContainer(cookieStoreId, profile); + await buildProfileAndRegister(cookieStoreId, seeds[cookieStoreId]); } return { ok: true }; } @@ -351,7 +340,7 @@ async function handleResetAll() { // Clear all storage domainMap = {}; pendingTabs = {}; - tabOrigins = {}; + cachedWhitelist = []; managedContainerIds.clear(); await browser.storage.local.clear(); @@ -379,6 +368,41 @@ async function handlePruneContainers() { return { pruned }; } +async function handleDeleteContainer(cookieStoreId) { + try { + await browser.contextualIdentities.remove(cookieStoreId); + } catch(e) {} + const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]); + const seeds = stored.containerSeeds || {}; + const settings = stored.containerSettings || {}; + delete seeds[cookieStoreId]; + delete settings[cookieStoreId]; + await browser.storage.local.set({ containerSeeds: seeds, containerSettings: settings }); + return { ok: true }; +} + +async function handleGetWhitelist() { + const stored = await browser.storage.local.get("whitelist"); + return stored.whitelist || []; +} + +async function handleSetWhitelist(whitelist) { + cachedWhitelist = whitelist; + await browser.storage.local.set({ whitelist }); + return { ok: true }; +} + +async function handleGetVectorSettings() { + const stored = await browser.storage.local.get("vectorSettings"); + return stored.vectorSettings || {}; +} + +async function handleSetVectorSettings(vectorSettings) { + await browser.storage.local.set({ vectorSettings }); + await registerAllKnownContainers(); + return { ok: true }; +} + // --- Container Lifecycle --- browser.contextualIdentities.onRemoved.addListener(async ({ contextualIdentity }) => { @@ -400,12 +424,12 @@ browser.contextualIdentities.onRemoved.addListener(async ({ contextualIdentity } async function init() { await loadDomainMap(); - // Populate managedContainerIds from stored seeds - const stored = await browser.storage.local.get("containerSeeds"); + const stored = await browser.storage.local.get(["containerSeeds", "whitelist"]); const seeds = stored.containerSeeds || {}; for (const cid of Object.keys(seeds)) { managedContainerIds.add(cid); } + cachedWhitelist = stored.whitelist || []; await registerAllKnownContainers(); } diff --git a/inject.js b/inject.js index 3e1749f..88f444e 100644 --- a/inject.js +++ b/inject.js @@ -10,6 +10,10 @@ const pageWindow = window.wrappedJSObject; + // --- Vector Toggle --- + const V = CONFIG.vectors || {}; + function vectorEnabled(name) { return V[name] !== false; } + // --- PRNG (Mulberry32) --- function mulberry32(seed) { return function() { @@ -25,14 +29,48 @@ // CANVAS SPOOFING // ========================================================================= - const origGetImageData = window.CanvasRenderingContext2D.prototype.getImageData; - const origPutImageData = window.CanvasRenderingContext2D.prototype.putImageData; + if (vectorEnabled("canvas")) { + const origGetImageData = window.CanvasRenderingContext2D.prototype.getImageData; + const origPutImageData = window.CanvasRenderingContext2D.prototype.putImageData; - function addCanvasNoise(ctx, canvas) { - try { - const w = canvas.width, h = canvas.height; - if (w <= 0 || h <= 0) return; - const imgData = origGetImageData.call(ctx, 0, 0, w, h); + function addCanvasNoise(ctx, canvas) { + try { + const w = canvas.width, h = canvas.height; + if (w <= 0 || h <= 0) return; + const imgData = origGetImageData.call(ctx, 0, 0, w, h); + const data = imgData.data; + const rng = mulberry32(CONFIG.canvasSeed); + for (let i = 0; i < data.length; i += 4) { + if (rng() < 0.1) { + const ch = (rng() * 3) | 0; + const delta = rng() < 0.5 ? -1 : 1; + data[i + ch] = Math.max(0, Math.min(255, data[i + ch] + delta)); + } + } + origPutImageData.call(ctx, imgData, 0, 0); + } catch(e) {} + } + + const origToDataURL = window.HTMLCanvasElement.prototype.toDataURL; + exportFunction(function(...args) { + try { + const ctx = this.getContext("2d"); + if (ctx) addCanvasNoise(ctx, this); + } catch(e) {} + return origToDataURL.apply(this, args); + }, pageWindow.HTMLCanvasElement.prototype, { defineAs: "toDataURL" }); + + const origToBlob = window.HTMLCanvasElement.prototype.toBlob; + exportFunction(function(callback, ...args) { + try { + const ctx = this.getContext("2d"); + if (ctx) addCanvasNoise(ctx, this); + } catch(e) {} + return origToBlob.call(this, callback, ...args); + }, pageWindow.HTMLCanvasElement.prototype, { defineAs: "toBlob" }); + + exportFunction(function(...args) { + const imgData = origGetImageData.apply(this, args); const data = imgData.data; const rng = mulberry32(CONFIG.canvasSeed); for (let i = 0; i < data.length; i += 4) { @@ -42,155 +80,137 @@ data[i + ch] = Math.max(0, Math.min(255, data[i + ch] + delta)); } } - origPutImageData.call(ctx, imgData, 0, 0); - } catch(e) {} + return imgData; + }, pageWindow.CanvasRenderingContext2D.prototype, { defineAs: "getImageData" }); } - const origToDataURL = window.HTMLCanvasElement.prototype.toDataURL; - exportFunction(function(...args) { - try { - const ctx = this.getContext("2d"); - if (ctx) addCanvasNoise(ctx, this); - } catch(e) {} - return origToDataURL.apply(this, args); - }, pageWindow.HTMLCanvasElement.prototype, { defineAs: "toDataURL" }); - - const origToBlob = window.HTMLCanvasElement.prototype.toBlob; - exportFunction(function(callback, ...args) { - try { - const ctx = this.getContext("2d"); - if (ctx) addCanvasNoise(ctx, this); - } catch(e) {} - return origToBlob.call(this, callback, ...args); - }, pageWindow.HTMLCanvasElement.prototype, { defineAs: "toBlob" }); - - exportFunction(function(...args) { - const imgData = origGetImageData.apply(this, args); - const data = imgData.data; - const rng = mulberry32(CONFIG.canvasSeed); - for (let i = 0; i < data.length; i += 4) { - if (rng() < 0.1) { - const ch = (rng() * 3) | 0; - const delta = rng() < 0.5 ? -1 : 1; - data[i + ch] = Math.max(0, Math.min(255, data[i + ch] + delta)); - } - } - return imgData; - }, pageWindow.CanvasRenderingContext2D.prototype, { defineAs: "getImageData" }); - // ========================================================================= // WEBGL SPOOFING // ========================================================================= - const UNMASKED_VENDOR = 0x9245; - const UNMASKED_RENDERER = 0x9246; + if (vectorEnabled("webgl")) { + const UNMASKED_VENDOR = 0x9245; + const UNMASKED_RENDERER = 0x9246; - function patchWebGL(protoName) { - const pageProto = pageWindow[protoName]; - if (!pageProto) return; - const origProto = window[protoName]; - if (!origProto) return; + function patchWebGL(protoName) { + const pageProto = pageWindow[protoName]; + if (!pageProto) return; + const origProto = window[protoName]; + if (!origProto) return; - const origGetParam = origProto.prototype.getParameter; - exportFunction(function(pname) { - if (pname === UNMASKED_VENDOR) return CONFIG.webgl.vendor; - if (pname === UNMASKED_RENDERER) return CONFIG.webgl.renderer; - return origGetParam.call(this, pname); - }, pageProto.prototype, { defineAs: "getParameter" }); + const origGetParam = origProto.prototype.getParameter; + exportFunction(function(pname) { + if (pname === UNMASKED_VENDOR) return CONFIG.webgl.vendor; + if (pname === UNMASKED_RENDERER) return CONFIG.webgl.renderer; + return origGetParam.call(this, pname); + }, pageProto.prototype, { defineAs: "getParameter" }); + } + + patchWebGL("WebGLRenderingContext"); + patchWebGL("WebGL2RenderingContext"); } - patchWebGL("WebGLRenderingContext"); - patchWebGL("WebGL2RenderingContext"); - // ========================================================================= // AUDIO SPOOFING // ========================================================================= - const origGetFloatFreq = window.AnalyserNode.prototype.getFloatFrequencyData; - exportFunction(function(array) { - origGetFloatFreq.call(this, array); - const rng = mulberry32(CONFIG.audioSeed); - for (let i = 0; i < array.length; i++) { - if (array[i] !== 0) array[i] += (rng() - 0.5) * 0.0001; - } - }, pageWindow.AnalyserNode.prototype, { defineAs: "getFloatFrequencyData" }); - - const origGetByteFreq = window.AnalyserNode.prototype.getByteFrequencyData; - exportFunction(function(array) { - origGetByteFreq.call(this, array); - const rng = mulberry32(CONFIG.audioSeed); - for (let i = 0; i < array.length; i++) { - if (array[i] !== 0 && rng() < 0.05) { - array[i] = Math.max(0, Math.min(255, array[i] + (rng() < 0.5 ? -1 : 1))); + if (vectorEnabled("audio")) { + const origGetFloatFreq = window.AnalyserNode.prototype.getFloatFrequencyData; + exportFunction(function(array) { + origGetFloatFreq.call(this, array); + const rng = mulberry32(CONFIG.audioSeed); + for (let i = 0; i < array.length; i++) { + if (array[i] !== 0) array[i] += (rng() - 0.5) * 0.0001; } - } - }, pageWindow.AnalyserNode.prototype, { defineAs: "getByteFrequencyData" }); + }, pageWindow.AnalyserNode.prototype, { defineAs: "getFloatFrequencyData" }); - const origGetChannelData = window.AudioBuffer.prototype.getChannelData; - exportFunction(function(channel) { - const data = origGetChannelData.call(this, channel); - const rng = mulberry32(CONFIG.audioSeed); - for (let i = 0; i < data.length; i++) { - if (data[i] !== 0) data[i] += (rng() - 0.5) * 0.0001; - } - return data; - }, pageWindow.AudioBuffer.prototype, { defineAs: "getChannelData" }); + const origGetByteFreq = window.AnalyserNode.prototype.getByteFrequencyData; + exportFunction(function(array) { + origGetByteFreq.call(this, array); + const rng = mulberry32(CONFIG.audioSeed); + for (let i = 0; i < array.length; i++) { + if (array[i] !== 0 && rng() < 0.05) { + array[i] = Math.max(0, Math.min(255, array[i] + (rng() < 0.5 ? -1 : 1))); + } + } + }, pageWindow.AnalyserNode.prototype, { defineAs: "getByteFrequencyData" }); + + const origGetChannelData = window.AudioBuffer.prototype.getChannelData; + exportFunction(function(channel) { + const data = origGetChannelData.call(this, channel); + const rng = mulberry32(CONFIG.audioSeed); + for (let i = 0; i < data.length; i++) { + if (data[i] !== 0) data[i] += (rng() - 0.5) * 0.0001; + } + return data; + }, pageWindow.AudioBuffer.prototype, { defineAs: "getChannelData" }); + } // ========================================================================= // NAVIGATOR SPOOFING // ========================================================================= - const navOverrides = { - hardwareConcurrency: CONFIG.nav.hardwareConcurrency, - platform: CONFIG.nav.platform, - deviceMemory: CONFIG.nav.deviceMemory, - maxTouchPoints: CONFIG.nav.maxTouchPoints - }; + if (vectorEnabled("navigator")) { + const navOverrides = { + hardwareConcurrency: CONFIG.nav.hardwareConcurrency, + platform: CONFIG.nav.platform, + deviceMemory: CONFIG.nav.deviceMemory, + maxTouchPoints: CONFIG.nav.maxTouchPoints + }; - for (const [prop, value] of Object.entries(navOverrides)) { - if (value !== undefined) { - Object.defineProperty(pageWindow.Navigator.prototype, prop, { - get: exportFunction(function() { return value; }, pageWindow), - configurable: true, - enumerable: true - }); + for (const [prop, value] of Object.entries(navOverrides)) { + if (value !== undefined) { + Object.defineProperty(pageWindow.Navigator.prototype, prop, { + get: exportFunction(function() { return value; }, pageWindow), + configurable: true, + enumerable: true + }); + } } + + const frozenLangs = CONFIG.nav.languages; + Object.defineProperty(pageWindow.Navigator.prototype, "languages", { + get: exportFunction(function() { + return cloneInto(frozenLangs, pageWindow, { freeze: true }); + }, pageWindow), + configurable: true, + enumerable: true + }); + + Object.defineProperty(pageWindow.Navigator.prototype, "language", { + get: exportFunction(function() { return frozenLangs[0]; }, pageWindow), + configurable: true, + enumerable: true + }); } - const frozenLangs = CONFIG.nav.languages; - Object.defineProperty(pageWindow.Navigator.prototype, "languages", { - get: exportFunction(function() { - return cloneInto(frozenLangs, pageWindow, { freeze: true }); - }, pageWindow), - configurable: true, - enumerable: true - }); + // ========================================================================= + // PLUGINS SPOOFING + // ========================================================================= - Object.defineProperty(pageWindow.Navigator.prototype, "language", { - get: exportFunction(function() { return frozenLangs[0]; }, pageWindow), - configurable: true, - enumerable: true - }); + if (vectorEnabled("plugins")) { + Object.defineProperty(pageWindow.Navigator.prototype, "plugins", { + get: exportFunction(function() { + return cloneInto([], pageWindow); + }, pageWindow), + configurable: true, + enumerable: true + }); - // Spoof plugins and mimeTypes as empty - Object.defineProperty(pageWindow.Navigator.prototype, "plugins", { - get: exportFunction(function() { - return cloneInto([], pageWindow); - }, pageWindow), - configurable: true, - enumerable: true - }); + Object.defineProperty(pageWindow.Navigator.prototype, "mimeTypes", { + get: exportFunction(function() { + return cloneInto([], pageWindow); + }, pageWindow), + configurable: true, + enumerable: true + }); + } - Object.defineProperty(pageWindow.Navigator.prototype, "mimeTypes", { - get: exportFunction(function() { - return cloneInto([], pageWindow); - }, pageWindow), - configurable: true, - enumerable: true - }); + // ========================================================================= + // CONNECTION SPOOFING + // ========================================================================= - // Spoof connection info - if (pageWindow.navigator.connection) { + if (vectorEnabled("connection") && pageWindow.navigator.connection) { try { Object.defineProperty(pageWindow.Navigator.prototype, "connection", { get: exportFunction(function() { @@ -207,8 +227,11 @@ } catch(e) {} } - // Block Battery API - if (pageWindow.navigator.getBattery) { + // ========================================================================= + // BATTERY SPOOFING + // ========================================================================= + + if (vectorEnabled("battery") && pageWindow.navigator.getBattery) { exportFunction(function() { return new pageWindow.Promise(exportFunction(function(resolve) { resolve(cloneInto({ @@ -227,55 +250,55 @@ // SCREEN SPOOFING // ========================================================================= - const screenOverrides = { - width: CONFIG.screen.width, - height: CONFIG.screen.height, - availWidth: CONFIG.screen.width, - availHeight: CONFIG.screen.height - 40, - colorDepth: CONFIG.screen.colorDepth, - pixelDepth: CONFIG.screen.colorDepth - }; + if (vectorEnabled("screen")) { + const screenOverrides = { + width: CONFIG.screen.width, + height: CONFIG.screen.height, + availWidth: CONFIG.screen.width, + availHeight: CONFIG.screen.height - 40, + colorDepth: CONFIG.screen.colorDepth, + pixelDepth: CONFIG.screen.colorDepth + }; - for (const [prop, value] of Object.entries(screenOverrides)) { - Object.defineProperty(pageWindow.Screen.prototype, prop, { - get: exportFunction(function() { return value; }, pageWindow), - configurable: true, - enumerable: true + for (const [prop, value] of Object.entries(screenOverrides)) { + Object.defineProperty(pageWindow.Screen.prototype, prop, { + get: exportFunction(function() { return value; }, pageWindow), + configurable: true, + enumerable: true + }); + } + + Object.defineProperty(pageWindow, "outerWidth", { + get: exportFunction(function() { return CONFIG.screen.width; }, pageWindow), + configurable: true + }); + Object.defineProperty(pageWindow, "outerHeight", { + get: exportFunction(function() { return CONFIG.screen.height; }, pageWindow), + configurable: true + }); + Object.defineProperty(pageWindow, "innerWidth", { + get: exportFunction(function() { return CONFIG.screen.width; }, pageWindow), + configurable: true + }); + Object.defineProperty(pageWindow, "innerHeight", { + get: exportFunction(function() { return CONFIG.screen.height - 80; }, pageWindow), + configurable: true }); } - Object.defineProperty(pageWindow, "outerWidth", { - get: exportFunction(function() { return CONFIG.screen.width; }, pageWindow), - configurable: true - }); - Object.defineProperty(pageWindow, "outerHeight", { - get: exportFunction(function() { return CONFIG.screen.height; }, pageWindow), - configurable: true - }); - Object.defineProperty(pageWindow, "innerWidth", { - get: exportFunction(function() { return CONFIG.screen.width; }, pageWindow), - configurable: true - }); - Object.defineProperty(pageWindow, "innerHeight", { - get: exportFunction(function() { return CONFIG.screen.height - 80; }, pageWindow), - configurable: true - }); - // ========================================================================= // TIMEZONE SPOOFING // ========================================================================= - if (CONFIG.timezone) { + if (vectorEnabled("timezone") && CONFIG.timezone) { const tzName = CONFIG.timezone.name; const tzOffset = CONFIG.timezone.offset; - // Override getTimezoneOffset const origGetTimezoneOffset = window.Date.prototype.getTimezoneOffset; exportFunction(function() { return tzOffset; }, pageWindow.Date.prototype, { defineAs: "getTimezoneOffset" }); - // Override Intl.DateTimeFormat.prototype.resolvedOptions to report spoofed timezone const OrigDateTimeFormat = window.Intl.DateTimeFormat; const origResolvedOptions = OrigDateTimeFormat.prototype.resolvedOptions; exportFunction(function() { @@ -284,12 +307,10 @@ return opts; }, pageWindow.Intl.DateTimeFormat.prototype, { defineAs: "resolvedOptions" }); - // Override Date.prototype.toString and toTimeString to reflect spoofed timezone const origToString = window.Date.prototype.toString; const origToTimeString = window.Date.prototype.toTimeString; function formatTzAbbrev(tzName) { - // Generate a plausible timezone abbreviation const abbrevMap = { "America/New_York": "EST", "America/Chicago": "CST", "America/Denver": "MST", "America/Los_Angeles": "PST", @@ -347,12 +368,10 @@ // WEBRTC LEAK PROTECTION // ========================================================================= - if (CONFIG.webrtc && CONFIG.webrtc.blockLocal) { - // Wrap RTCPeerConnection to prevent local IP leaks + if (vectorEnabled("webrtc") && CONFIG.webrtc && CONFIG.webrtc.blockLocal) { if (pageWindow.RTCPeerConnection) { const OrigRTC = window.RTCPeerConnection; const wrappedRTC = exportFunction(function(config, constraints) { - // Force TURN-only to prevent local candidate leaks if (config && config.iceServers) { config.iceTransportPolicy = "relay"; } @@ -377,15 +396,13 @@ // FONT FINGERPRINT PROTECTION // ========================================================================= - if (CONFIG.fontSeed) { + if (vectorEnabled("fonts") && CONFIG.fontSeed) { const fontRng = mulberry32(CONFIG.fontSeed); - // Add subtle noise to measureText to prevent font enumeration const origMeasureText = window.CanvasRenderingContext2D.prototype.measureText; exportFunction(function(text) { const metrics = origMeasureText.call(this, text); - // Add deterministic sub-pixel noise to width const noise = (fontRng() - 0.5) * 0.3; const origWidth = metrics.width; @@ -404,7 +421,7 @@ // CLIENTRECTS FINGERPRINT PROTECTION // ========================================================================= - if (CONFIG.rectSeed) { + if (vectorEnabled("clientRects") && CONFIG.rectSeed) { const rectRng = mulberry32(CONFIG.rectSeed); function addRectNoise(rect) { diff --git a/manifest.json b/manifest.json index 397b0c0..5122ca1 100644 --- a/manifest.json +++ b/manifest.json @@ -15,6 +15,10 @@ "background": { "scripts": ["lib/prng.js", "lib/fingerprint-gen.js", "background.js"] }, + "options_ui": { + "page": "options/options.html", + "open_in_tab": true + }, "browser_action": { "default_popup": "popup/popup.html", "default_icon": { diff --git a/options/options.css b/options/options.css new file mode 100644 index 0000000..3eb9fae --- /dev/null +++ b/options/options.css @@ -0,0 +1,71 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font: 13px/1.4 system-ui, sans-serif; color: #e0e0e0; background: #1e1e2e; } +.wrap { max-width: 640px; margin: 0 auto; padding: 24px 20px; } + +header { display: flex; align-items: baseline; gap: 10px; padding-bottom: 12px; border-bottom: 1px solid #333; margin-bottom: 24px; } +h1 { font-size: 18px; font-weight: 600; } +#version { font-size: 11px; color: #888; } + +section { margin-bottom: 28px; } +h2 { font-size: 14px; font-weight: 600; padding-bottom: 6px; border-bottom: 1px solid #2a2a3a; margin-bottom: 8px; } +.desc { font-size: 11px; color: #888; margin-bottom: 10px; } + +/* Vector grid */ +#vector-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; } +.vector-item { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; background: #2a2a3a; border-radius: 4px; } +.vector-label { font-size: 12px; } + +/* Toggle switch (matches popup) */ +.toggle { appearance: none; width: 32px; height: 18px; background: #444; border-radius: 9px; position: relative; cursor: pointer; flex-shrink: 0; } +.toggle::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; background: #888; border-radius: 50%; transition: .15s; } +.toggle:checked { background: #4a9eff; } +.toggle:checked::after { left: 16px; background: #fff; } + +/* Whitelist */ +.whitelist-input { display: flex; gap: 6px; margin-bottom: 8px; } +.whitelist-input input { flex: 1; padding: 6px 8px; background: #2a2a3a; border: 1px solid #444; border-radius: 4px; color: #e0e0e0; font-size: 12px; outline: none; } +.whitelist-input input:focus { border-color: #4a9eff; } +#wl-list { display: flex; flex-wrap: wrap; gap: 6px; } +.wl-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; background: #2a2a3a; border: 1px solid #444; border-radius: 12px; font-size: 11px; } +.wl-chip button { background: none; border: none; color: #888; cursor: pointer; font-size: 13px; line-height: 1; padding: 0 2px; } +.wl-chip button:hover { color: #ff613d; } + +/* Container table */ +table { width: 100%; border-collapse: collapse; } +th { text-align: left; font-size: 11px; color: #888; font-weight: 400; padding: 4px 8px; border-bottom: 1px solid #333; } +td { padding: 6px 8px; border-bottom: 1px solid #2a2a3a; } +tr:hover td { background: #2a2a3a; } +.empty { font-size: 12px; color: #888; padding: 12px 0; } + +/* Color dots (matches popup) */ +.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; } +.dot-blue { background: #37adff; } +.dot-turquoise { background: #00c79a; } +.dot-green { background: #51cd00; } +.dot-yellow { background: #ffcb00; } +.dot-orange { background: #ff9f00; } +.dot-red { background: #ff613d; } +.dot-pink { background: #ff4bda; } +.dot-purple { background: #af51f5; } +.dot-toolbar { background: #888; } + +/* Buttons */ +.btn { padding: 6px 12px; background: #333; border: 1px solid #555; color: #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; } +.btn:hover { background: #444; color: #fff; } +.secondary { background: #2a2a3a; color: #aaa; } +.secondary:hover { background: #333; color: #ddd; } +.danger { background: #3a1a1a; border-color: #663333; color: #ff613d; } +.danger:hover { background: #4a2020; color: #ff8866; } +.btn-sm { padding: 2px 6px; font-size: 11px; } +.btn-icon { background: none; border: 1px solid #555; color: #aaa; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; } +.btn-icon:hover { border-color: #888; color: #ddd; } +.btn-del { background: none; border: 1px solid #553333; color: #ff613d; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; } +.btn-del:hover { border-color: #884444; color: #ff8866; } + +.td-actions { display: flex; gap: 4px; justify-content: flex-end; } + +/* Bulk actions */ +.bulk { display: flex; flex-direction: column; gap: 6px; } +.bulk #regen-all { width: 100%; } +.bulk-row { display: flex; gap: 6px; } +.bulk-row button { flex: 1; } diff --git a/options/options.html b/options/options.html new file mode 100644 index 0000000..1d11441 --- /dev/null +++ b/options/options.html @@ -0,0 +1,64 @@ + + + + + ContainSite Options + + + +
+
+

ContainSite

+ +
+ +
+
+

Fingerprint Vectors

+

Control which fingerprint vectors are spoofed. Changes take effect on next page load.

+
+
+ +
+

Domain Whitelist

+

Whitelisted domains are never containerized or fingerprint-spoofed.

+
+ + +
+
+
+ +
+

Containers

+

All containers managed by ContainSite.

+ + + + + + + + + + +
DomainSpoofing
+ +
+ +
+

Bulk Actions

+
+ +
+ + +
+
+
+
+
+ + + + diff --git a/options/options.js b/options/options.js new file mode 100644 index 0000000..5c5ed98 --- /dev/null +++ b/options/options.js @@ -0,0 +1,226 @@ +const VECTORS = { + canvas: "Canvas", + webgl: "WebGL", + audio: "Audio", + navigator: "Navigator", + screen: "Screen", + timezone: "Timezone", + webrtc: "WebRTC", + fonts: "Fonts", + clientRects: "Client Rects", + plugins: "Plugins", + battery: "Battery", + connection: "Connection" +}; + +// --- Vector Settings --- + +async function loadVectors() { + const settings = await browser.runtime.sendMessage({ type: "getVectorSettings" }); + const grid = document.getElementById("vector-grid"); + grid.innerHTML = ""; + + for (const [key, label] of Object.entries(VECTORS)) { + const item = document.createElement("div"); + item.className = "vector-item"; + + const span = document.createElement("span"); + span.className = "vector-label"; + span.textContent = label; + item.appendChild(span); + + const toggle = document.createElement("input"); + toggle.type = "checkbox"; + toggle.className = "toggle"; + toggle.checked = settings[key] !== false; + toggle.addEventListener("change", async () => { + settings[key] = toggle.checked; + toggle.disabled = true; + await browser.runtime.sendMessage({ type: "setVectorSettings", vectorSettings: settings }); + toggle.disabled = false; + }); + item.appendChild(toggle); + + grid.appendChild(item); + } +} + +// --- Whitelist --- + +let currentWhitelist = []; + +async function loadWhitelist() { + currentWhitelist = await browser.runtime.sendMessage({ type: "getWhitelist" }); + renderWhitelist(); +} + +function renderWhitelist() { + const list = document.getElementById("wl-list"); + list.innerHTML = ""; + + for (const domain of currentWhitelist) { + const chip = document.createElement("span"); + chip.className = "wl-chip"; + chip.textContent = domain; + + const btn = document.createElement("button"); + btn.textContent = "\u00d7"; + btn.title = "Remove"; + btn.addEventListener("click", async () => { + currentWhitelist = currentWhitelist.filter(d => d !== domain); + await browser.runtime.sendMessage({ type: "setWhitelist", whitelist: currentWhitelist }); + renderWhitelist(); + }); + chip.appendChild(btn); + list.appendChild(chip); + } +} + +document.getElementById("wl-add").addEventListener("click", addWhitelistEntry); +document.getElementById("wl-input").addEventListener("keydown", (e) => { + if (e.key === "Enter") addWhitelistEntry(); +}); + +async function addWhitelistEntry() { + const input = document.getElementById("wl-input"); + let domain = input.value.trim().toLowerCase(); + + // Strip protocol and path + domain = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, ""); + // Strip www. + domain = domain.replace(/^www\./, ""); + + if (!domain || !domain.includes(".")) return; + if (currentWhitelist.includes(domain)) { input.value = ""; return; } + + currentWhitelist.push(domain); + await browser.runtime.sendMessage({ type: "setWhitelist", whitelist: currentWhitelist }); + input.value = ""; + renderWhitelist(); +} + +// --- Containers --- + +async function loadContainers() { + const containers = await browser.runtime.sendMessage({ type: "getContainerList" }); + const tbody = document.getElementById("container-tbody"); + const empty = document.getElementById("no-containers"); + tbody.innerHTML = ""; + + // Only show containers that have a seed (managed by us) + const ours = containers.filter(c => c.hasSeed); + + if (ours.length === 0) { + empty.hidden = false; + document.getElementById("container-table").hidden = true; + return; + } + + empty.hidden = true; + document.getElementById("container-table").hidden = false; + + ours.sort((a, b) => (a.domain || a.name).localeCompare(b.domain || b.name)); + + for (const c of ours) { + const tr = document.createElement("tr"); + + // Color dot + const tdDot = document.createElement("td"); + const dot = document.createElement("span"); + dot.className = `dot dot-${c.color}`; + tdDot.appendChild(dot); + tr.appendChild(tdDot); + + // Domain + const tdDomain = document.createElement("td"); + tdDomain.textContent = c.domain || c.name; + tr.appendChild(tdDomain); + + // Enabled toggle + const tdToggle = document.createElement("td"); + const toggle = document.createElement("input"); + toggle.type = "checkbox"; + toggle.className = "toggle"; + toggle.checked = c.enabled; + toggle.addEventListener("change", async () => { + await browser.runtime.sendMessage({ + type: "toggleContainer", + cookieStoreId: c.cookieStoreId, + enabled: toggle.checked + }); + }); + tdToggle.appendChild(toggle); + tr.appendChild(tdToggle); + + // Actions + const tdActions = document.createElement("td"); + tdActions.className = "td-actions"; + + const regen = document.createElement("button"); + regen.className = "btn-icon"; + regen.textContent = "New"; + regen.title = "Generate new fingerprint"; + regen.addEventListener("click", async () => { + regen.textContent = "..."; + await browser.runtime.sendMessage({ type: "regenerateFingerprint", cookieStoreId: c.cookieStoreId }); + regen.textContent = "OK"; + setTimeout(() => { regen.textContent = "New"; }, 800); + }); + tdActions.appendChild(regen); + + const del = document.createElement("button"); + del.className = "btn-del"; + del.textContent = "Del"; + del.title = "Delete container"; + del.addEventListener("click", async () => { + if (!confirm(`Delete container for ${c.domain || c.name}? Cookies and data for this site will be lost.`)) return; + del.textContent = "..."; + await browser.runtime.sendMessage({ type: "deleteContainer", cookieStoreId: c.cookieStoreId }); + loadContainers(); + }); + tdActions.appendChild(del); + + tr.appendChild(tdActions); + tbody.appendChild(tr); + } +} + +// --- Bulk Actions --- + +document.getElementById("regen-all").addEventListener("click", async (e) => { + e.target.textContent = "Regenerating..."; + await browser.runtime.sendMessage({ type: "regenerateAll" }); + e.target.textContent = "Done!"; + setTimeout(() => { e.target.textContent = "Regenerate All Fingerprints"; }, 800); +}); + +document.getElementById("prune").addEventListener("click", async (e) => { + e.target.textContent = "Pruning..."; + const result = await browser.runtime.sendMessage({ type: "pruneContainers" }); + e.target.textContent = `Removed ${result.pruned}`; + setTimeout(() => { + e.target.textContent = "Prune Unused"; + loadContainers(); + }, 1200); +}); + +document.getElementById("reset").addEventListener("click", async (e) => { + if (!confirm("Remove all ContainSite containers and data? You will need to log in to all sites again.")) return; + e.target.textContent = "Resetting..."; + await browser.runtime.sendMessage({ type: "resetAll" }); + e.target.textContent = "Done!"; + setTimeout(() => { + e.target.textContent = "Reset All"; + loadContainers(); + }, 1200); +}); + +// --- Init --- + +async function init() { + const manifest = browser.runtime.getManifest(); + document.getElementById("version").textContent = `v${manifest.version}`; + await Promise.all([loadVectors(), loadWhitelist(), loadContainers()]); +} + +init();