Initial release — per-site container isolation with unique device fingerprints

Automatic per-domain containers with hardened fingerprint spoofing:
canvas, WebGL, audio, navigator, screen, timezone, WebRTC, fonts,
ClientRects, plugins, battery, and connection APIs.
This commit is contained in:
sal
2026-02-28 22:59:46 -06:00
commit a058d78c20
11 changed files with 1153 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.xpi
*.zip
icons/icon.svg

391
background.js Normal file
View File

@@ -0,0 +1,391 @@
// ContainSite — Background Script
// Every site gets its own container. Auth redirects stay in the originating container.
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)
const CONTAINER_COLORS = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple"];
const CONTAINER_ICONS = ["fingerprint", "fence", "briefcase", "cart", "circle", "gift", "tree", "chill"];
// --- Domain Extraction ---
function extractDomain(url) {
try {
const u = new URL(url);
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
// Skip localhost and local IPs
const h = u.hostname;
if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".local")) return null;
return h;
} catch(e) {
return null;
}
}
function getBaseDomain(hostname) {
const parts = hostname.split(".");
if (parts.length <= 2) return hostname;
const twoPartTLDs = ["co.uk", "co.jp", "co.kr", "com.au", "com.br", "co.nz", "co.in", "org.uk", "net.au"];
const lastTwo = parts.slice(-2).join(".");
if (twoPartTLDs.includes(lastTwo) && parts.length > 2) {
return parts.slice(-3).join(".");
}
return parts.slice(-2).join(".");
}
// --- Seed Management ---
function generateSeed() {
const arr = new Uint32Array(1);
crypto.getRandomValues(arr);
return arr[0];
}
// --- Inject Source Loading ---
async function getInjectSource() {
if (!injectSourceCache) {
const resp = await fetch(browser.runtime.getURL("inject.js"));
injectSourceCache = await resp.text();
}
return injectSourceCache;
}
// --- Per-Container Script Registration ---
async function registerForContainer(cookieStoreId, profile) {
if (registeredScripts[cookieStoreId]) {
try { await registeredScripts[cookieStoreId].unregister(); } catch(e) {}
delete registeredScripts[cookieStoreId];
}
const injectSource = await getInjectSource();
// Set config then run inject.js — both execute in ISOLATED world
// inject.js uses exportFunction/wrappedJSObject to modify page context (bypasses CSP)
const configCode = `window.__csConfig = ${JSON.stringify(profile)};`;
registeredScripts[cookieStoreId] = await browser.contentScripts.register({
matches: ["<all_urls>"],
js: [{ code: configCode }, { code: injectSource }],
runAt: "document_start",
allFrames: true,
cookieStoreId: cookieStoreId
});
}
async function registerAllKnownContainers() {
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
const seeds = stored.containerSeeds || {};
const settings = stored.containerSettings || {};
for (const [cid, script] of Object.entries(registeredScripts)) {
try { await script.unregister(); } catch(e) {}
}
for (const key of Object.keys(registeredScripts)) {
delete registeredScripts[key];
}
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);
}
}
// --- Storage ---
async function loadDomainMap() {
const stored = await browser.storage.local.get("domainMap");
domainMap = stored.domainMap || {};
}
async function saveDomainMap() {
await browser.storage.local.set({ domainMap });
}
// --- Auto-Containment ---
async function getOrCreateContainerForDomain(baseDomain) {
if (domainMap[baseDomain]) {
return domainMap[baseDomain];
}
const colorIndex = Object.keys(domainMap).length % CONTAINER_COLORS.length;
const iconIndex = Object.keys(domainMap).length % CONTAINER_ICONS.length;
const container = await browser.contextualIdentities.create({
name: baseDomain,
color: CONTAINER_COLORS[colorIndex],
icon: CONTAINER_ICONS[iconIndex]
});
const cookieStoreId = container.cookieStoreId;
domainMap[baseDomain] = cookieStoreId;
await saveDomainMap();
const stored = await browser.storage.local.get("containerSeeds");
const seeds = stored.containerSeeds || {};
seeds[cookieStoreId] = generateSeed();
await browser.storage.local.set({ containerSeeds: seeds });
const profile = generateFingerprintProfile(seeds[cookieStoreId]);
await registerForContainer(cookieStoreId, profile);
return cookieStoreId;
}
// Set of tabIds we just created — skip these entirely to prevent loops
const createdByUs = {};
// 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
if (createdByUs[tabId]) return;
if (pendingTabs[tabId]) return;
pendingTabs[tabId] = true;
try {
const tab = await browser.tabs.get(tabId);
// If the tab is in ANY non-default container, leave it alone.
// Either it's one of ours, or the user put it there intentionally.
if (tab.cookieStoreId !== "firefox-default") {
delete pendingTabs[tabId];
return;
}
// Tab is in the default (uncontained) context — assign it to the right container
const cookieStoreId = await getOrCreateContainerForDomain(baseDomain);
if (tab.cookieStoreId === cookieStoreId) {
delete pendingTabs[tabId];
return;
}
const newTab = await browser.tabs.create({
url: url,
cookieStoreId: cookieStoreId,
index: tab.index + 1,
active: tab.active
});
// Mark the new tab so we never redirect it again
createdByUs[newTab.id] = true;
setTimeout(() => { delete createdByUs[newTab.id]; }, 5000);
await browser.tabs.remove(tabId);
} catch(e) {}
delete pendingTabs[tabId];
}
// Intercept new navigations
browser.webRequest.onBeforeRequest.addListener(
function(details) {
if (details.type !== "main_frame") return {};
if (details.tabId === -1) return {};
const domain = extractDomain(details.url);
if (!domain) return {};
const baseDomain = getBaseDomain(domain);
// Trigger async container assignment
assignTabToContainer(details.tabId, details.url, baseDomain);
return {};
},
{ urls: ["<all_urls>"] },
["blocking"]
);
// Handle in-tab navigations (address bar, link clicks)
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (!changeInfo.url) return;
if (pendingTabs[tabId]) return;
const domain = extractDomain(changeInfo.url);
if (!domain) return;
const baseDomain = getBaseDomain(domain);
await assignTabToContainer(tabId, changeInfo.url, baseDomain);
});
// Clean up tab tracking when tabs close
browser.tabs.onRemoved.addListener((tabId) => {
delete pendingTabs[tabId];
delete tabOrigins[tabId];
});
// --- Message Handling (from popup) ---
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();
}
});
async function handleGetContainerList() {
const containers = await browser.contextualIdentities.query({});
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
const seeds = stored.containerSeeds || {};
const settings = stored.containerSettings || {};
const reverseDomainMap = {};
for (const [domain, cid] of Object.entries(domainMap)) {
reverseDomainMap[cid] = domain;
}
return containers.map(c => ({
name: c.name,
cookieStoreId: c.cookieStoreId,
color: c.color,
icon: c.icon,
domain: reverseDomainMap[c.cookieStoreId] || null,
enabled: (settings[c.cookieStoreId]?.enabled !== false),
hasSeed: !!seeds[c.cookieStoreId]
}));
}
async function handleToggle(cookieStoreId, enabled) {
const stored = await browser.storage.local.get("containerSettings");
const settings = stored.containerSettings || {};
settings[cookieStoreId] = { ...settings[cookieStoreId], enabled };
await browser.storage.local.set({ containerSettings: settings });
if (!enabled) {
if (registeredScripts[cookieStoreId]) {
try { await registeredScripts[cookieStoreId].unregister(); } catch(e) {}
delete registeredScripts[cookieStoreId];
}
} else {
const seedStored = await browser.storage.local.get("containerSeeds");
const seeds = seedStored.containerSeeds || {};
if (seeds[cookieStoreId]) {
const profile = generateFingerprintProfile(seeds[cookieStoreId]);
await registerForContainer(cookieStoreId, profile);
}
}
return { ok: true };
}
async function handleRegenerate(cookieStoreId) {
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
const seeds = stored.containerSeeds || {};
const settings = stored.containerSettings || {};
seeds[cookieStoreId] = generateSeed();
await browser.storage.local.set({ containerSeeds: seeds });
const cfg = settings[cookieStoreId] || { enabled: true };
if (cfg.enabled) {
const profile = generateFingerprintProfile(seeds[cookieStoreId]);
await registerForContainer(cookieStoreId, profile);
}
return { ok: true };
}
async function handleRegenerateAll() {
const stored = await browser.storage.local.get("containerSeeds");
const seeds = stored.containerSeeds || {};
for (const cid of Object.keys(seeds)) {
seeds[cid] = generateSeed();
}
await browser.storage.local.set({ containerSeeds: seeds });
await registerAllKnownContainers();
return { ok: true };
}
async function handleResetAll() {
// Unregister all content scripts
for (const [cid, script] of Object.entries(registeredScripts)) {
try { await script.unregister(); } catch(e) {}
}
for (const key of Object.keys(registeredScripts)) {
delete registeredScripts[key];
}
// Remove all ContainSite-managed containers
const containers = await browser.contextualIdentities.query({});
const ourContainerIds = new Set(Object.values(domainMap));
for (const c of containers) {
if (ourContainerIds.has(c.cookieStoreId)) {
try { await browser.contextualIdentities.remove(c.cookieStoreId); } catch(e) {}
}
}
// Clear all storage
domainMap = {};
pendingTabs = {};
tabOrigins = {};
await browser.storage.local.clear();
return { ok: true };
}
async function handlePruneContainers() {
// Remove containers that have no open tabs
const containers = await browser.contextualIdentities.query({});
const ourContainerIds = new Set(Object.values(domainMap));
const tabs = await browser.tabs.query({});
// Collect cookieStoreIds that have open tabs
const activeContainers = new Set(tabs.map(t => t.cookieStoreId));
let pruned = 0;
for (const c of containers) {
if (ourContainerIds.has(c.cookieStoreId) && !activeContainers.has(c.cookieStoreId)) {
try {
await browser.contextualIdentities.remove(c.cookieStoreId);
pruned++;
} catch(e) {}
// domainMap cleanup happens via the onRemoved listener
}
}
return { pruned };
}
// --- Container Lifecycle ---
browser.contextualIdentities.onRemoved.addListener(async ({ contextualIdentity }) => {
const cid = contextualIdentity.cookieStoreId;
if (registeredScripts[cid]) {
try { await registeredScripts[cid].unregister(); } catch(e) {}
delete registeredScripts[cid];
}
for (const [domain, cookieStoreId] of Object.entries(domainMap)) {
if (cookieStoreId === cid) {
delete domainMap[domain];
}
}
await saveDomainMap();
});
// --- Init ---
async function init() {
await loadDomainMap();
await registerAllKnownContainers();
}
init();

BIN
icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
icons/icon-96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

448
inject.js Normal file
View File

@@ -0,0 +1,448 @@
// ContainSite — Hardened fingerprint overrides
// Uses Firefox exportFunction/wrappedJSObject APIs (bypasses CSP)
(function() {
"use strict";
const CONFIG = window.__csConfig;
if (!CONFIG) return;
delete window.__csConfig;
const pageWindow = window.wrappedJSObject;
// --- PRNG (Mulberry32) ---
function mulberry32(seed) {
return function() {
seed |= 0;
seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = t + Math.imul(t ^ (t >>> 7), 61 | t) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// =========================================================================
// CANVAS SPOOFING
// =========================================================================
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);
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) {
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;
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" });
}
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)));
}
}
}, 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
};
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
});
// 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
});
// Spoof connection info
if (pageWindow.navigator.connection) {
try {
Object.defineProperty(pageWindow.Navigator.prototype, "connection", {
get: exportFunction(function() {
return cloneInto({
effectiveType: "4g",
downlink: 10,
rtt: 50,
saveData: false
}, pageWindow);
}, pageWindow),
configurable: true,
enumerable: true
});
} catch(e) {}
}
// Block Battery API
if (pageWindow.navigator.getBattery) {
exportFunction(function() {
return new pageWindow.Promise(exportFunction(function(resolve) {
resolve(cloneInto({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 1.0,
addEventListener: function() {},
removeEventListener: function() {}
}, pageWindow, { cloneFunctions: true }));
}, pageWindow));
}, pageWindow.Navigator.prototype, { defineAs: "getBattery" });
}
// =========================================================================
// 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
};
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
});
// =========================================================================
// TIMEZONE SPOOFING
// =========================================================================
if (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() {
const opts = origResolvedOptions.call(this);
try { opts.timeZone = tzName; } catch(e) {}
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",
"Europe/London": "GMT", "Europe/Berlin": "CET",
"Europe/Paris": "CET", "Asia/Tokyo": "JST",
"Australia/Sydney": "AEST", "America/Toronto": "EST",
"America/Phoenix": "MST"
};
return abbrevMap[tzName] || "UTC";
}
function buildTzString(date) {
try {
const fmt = new OrigDateTimeFormat("en-US", {
timeZone: tzName,
weekday: "short", year: "numeric", month: "short", day: "2-digit",
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
});
const parts = fmt.format(date);
const sign = tzOffset <= 0 ? "+" : "-";
const absOff = Math.abs(tzOffset);
const h = String(Math.floor(absOff / 60)).padStart(2, "0");
const m = String(absOff % 60).padStart(2, "0");
const abbrev = formatTzAbbrev(tzName);
return `${parts} GMT${sign}${h}${m} (${abbrev})`;
} catch(e) {
return origToString.call(date);
}
}
exportFunction(function() {
return buildTzString(this);
}, pageWindow.Date.prototype, { defineAs: "toString" });
exportFunction(function() {
try {
const fmt = new OrigDateTimeFormat("en-US", {
timeZone: tzName,
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
});
const parts = fmt.format(this);
const sign = tzOffset <= 0 ? "+" : "-";
const absOff = Math.abs(tzOffset);
const h = String(Math.floor(absOff / 60)).padStart(2, "0");
const m = String(absOff % 60).padStart(2, "0");
const abbrev = formatTzAbbrev(tzName);
return `${parts} GMT${sign}${h}${m} (${abbrev})`;
} catch(e) {
return origToTimeString.call(this);
}
}, pageWindow.Date.prototype, { defineAs: "toTimeString" });
}
// =========================================================================
// WEBRTC LEAK PROTECTION
// =========================================================================
if (CONFIG.webrtc && CONFIG.webrtc.blockLocal) {
// Wrap RTCPeerConnection to prevent local IP leaks
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";
}
const pc = new OrigRTC(config, constraints);
return pc;
}, pageWindow);
try {
wrappedRTC.prototype = pageWindow.RTCPeerConnection.prototype;
pageWindow.RTCPeerConnection = wrappedRTC;
} catch(e) {}
}
if (pageWindow.webkitRTCPeerConnection) {
try {
pageWindow.webkitRTCPeerConnection = pageWindow.RTCPeerConnection;
} catch(e) {}
}
}
// =========================================================================
// FONT FINGERPRINT PROTECTION
// =========================================================================
if (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;
try {
Object.defineProperty(metrics, "width", {
get: function() { return origWidth + noise; },
configurable: true
});
} catch(e) {}
return metrics;
}, pageWindow.CanvasRenderingContext2D.prototype, { defineAs: "measureText" });
}
// =========================================================================
// CLIENTRECTS FINGERPRINT PROTECTION
// =========================================================================
if (CONFIG.rectSeed) {
const rectRng = mulberry32(CONFIG.rectSeed);
function addRectNoise(rect) {
const noise = (rectRng() - 0.5) * 0.1;
try {
const origX = rect.x, origY = rect.y;
const origW = rect.width, origH = rect.height;
const origT = rect.top, origL = rect.left;
const origB = rect.bottom, origR = rect.right;
Object.defineProperties(rect, {
x: { get: () => origX + noise, configurable: true },
y: { get: () => origY + noise, configurable: true },
width: { get: () => origW + noise, configurable: true },
height: { get: () => origH + noise, configurable: true },
top: { get: () => origT + noise, configurable: true },
left: { get: () => origL + noise, configurable: true },
bottom: { get: () => origB + noise, configurable: true },
right: { get: () => origR + noise, configurable: true }
});
} catch(e) {}
return rect;
}
const origGetBCR = window.Element.prototype.getBoundingClientRect;
exportFunction(function() {
const rect = origGetBCR.call(this);
return addRectNoise(rect);
}, pageWindow.Element.prototype, { defineAs: "getBoundingClientRect" });
const origGetCR = window.Element.prototype.getClientRects;
exportFunction(function() {
const rects = origGetCR.call(this);
for (let i = 0; i < rects.length; i++) {
addRectNoise(rects[i]);
}
return rects;
}, pageWindow.Element.prototype, { defineAs: "getClientRects" });
}
})();

122
lib/fingerprint-gen.js Normal file
View File

@@ -0,0 +1,122 @@
// Deterministic fingerprint profile generator
// Given the same seed, always produces the same device identity
// Real hardware values — spoofed values must NEVER match these
const REAL_HARDWARE = {
hardwareConcurrency: 4, // 2 cores / 4 threads
screenWidth: 1920,
screenHeight: 1080
};
function generateFingerprintProfile(masterSeed) {
const rng = mulberry32(masterSeed);
function pick(arr) {
return arr[Math.floor(rng() * arr.length)];
}
// Pick from array, but never the excluded value. Rerolls if needed.
function pickExcluding(arr, exclude) {
const filtered = arr.filter(v => {
if (typeof exclude === "object" && exclude !== null) {
return Object.keys(exclude).some(k => v[k] !== exclude[k]);
}
return v !== exclude;
});
return filtered.length > 0 ? pick(filtered) : pick(arr);
}
function subSeed() {
return (rng() * 0xFFFFFFFF) >>> 0;
}
const platforms = ["Win32", "Linux x86_64", "MacIntel"];
const vendors = [
"Google Inc. (NVIDIA)",
"Google Inc. (AMD)",
"Google Inc. (Intel)",
"Google Inc."
];
const renderers = [
"ANGLE (NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0)",
"ANGLE (AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)",
"ANGLE (Intel HD Graphics 630 Direct3D11 vs_5_0 ps_5_0)",
"ANGLE (NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0)",
"ANGLE (AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0)",
"Mesa Intel(R) UHD Graphics 620",
"Mesa AMD Radeon RX 580",
"ANGLE (Intel, Mesa Intel(R) UHD Graphics 620, OpenGL 4.6)"
];
const resolutions = [
{ width: 2560, height: 1440 },
{ width: 1366, height: 768 },
{ width: 1536, height: 864 },
{ width: 1440, height: 900 },
{ width: 1680, height: 1050 },
{ width: 2560, height: 1080 },
{ width: 3440, height: 1440 },
{ width: 1600, height: 900 }
];
const languageSets = [
["en-US", "en"],
["en-GB", "en"],
["en-US"],
["de-DE", "de", "en-US", "en"],
["fr-FR", "fr", "en-US", "en"]
];
// Exclude real hardwareConcurrency (4)
const hardwareConcurrencies = [2, 6, 8, 12, 16];
const deviceMemories = [4, 8, 16, 32];
const colorDepths = [24, 30, 32];
// Timezones for spoofing — common real-world timezones
const timezones = [
{ name: "America/New_York", offset: 300 },
{ name: "America/Chicago", offset: 360 },
{ name: "America/Denver", offset: 420 },
{ name: "America/Los_Angeles", offset: 480 },
{ name: "Europe/London", offset: 0 },
{ name: "Europe/Berlin", offset: -60 },
{ name: "Europe/Paris", offset: -60 },
{ name: "Asia/Tokyo", offset: -540 },
{ name: "Australia/Sydney", offset: -660 },
{ name: "America/Toronto", offset: 300 },
{ name: "America/Phoenix", offset: 420 }
];
// Resolution: never match real 1920x1080
const res = pickExcluding(resolutions, { width: REAL_HARDWARE.screenWidth, height: REAL_HARDWARE.screenHeight });
return {
seed: masterSeed,
canvasSeed: subSeed(),
audioSeed: subSeed(),
fontSeed: subSeed(),
rectSeed: subSeed(),
nav: {
hardwareConcurrency: pick(hardwareConcurrencies),
platform: pick(platforms),
languages: pick(languageSets),
deviceMemory: pick(deviceMemories),
maxTouchPoints: 0
},
screen: {
width: res.width,
height: res.height,
colorDepth: pick(colorDepths)
},
webgl: {
vendor: pick(vendors),
renderer: pick(renderers)
},
timezone: pick(timezones),
webrtc: {
blockLocal: true
}
};
}

11
lib/prng.js Normal file
View File

@@ -0,0 +1,11 @@
// Mulberry32 — fast, deterministic 32-bit PRNG
// Same seed always produces the same sequence
function mulberry32(seed) {
return function() {
seed |= 0;
seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = t + Math.imul(t ^ (t >>> 7), 61 | t) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}

36
manifest.json Normal file
View File

@@ -0,0 +1,36 @@
{
"manifest_version": 2,
"name": "ContainSite",
"version": "0.1.0",
"description": "Per-container fingerprint isolation — each container gets its own device identity",
"permissions": [
"contextualIdentities",
"cookies",
"storage",
"tabs",
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"background": {
"scripts": ["lib/prng.js", "lib/fingerprint-gen.js", "background.js"]
},
"browser_action": {
"default_popup": "popup/popup.html",
"default_icon": {
"48": "icons/icon-48.png",
"96": "icons/icon-96.png"
},
"default_title": "ContainSite"
},
"icons": {
"48": "icons/icon-48.png",
"96": "icons/icon-96.png"
},
"browser_specific_settings": {
"gecko": {
"id": "containsite@salmutt.dev",
"strict_min_version": "100.0"
}
}
}

35
popup/popup.css Normal file
View File

@@ -0,0 +1,35 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body { width: 300px; font: 13px/1.4 system-ui, sans-serif; color: #e0e0e0; background: #1e1e2e; }
h1 { padding: 10px 12px 6px; font-size: 15px; font-weight: 600; border-bottom: 1px solid #333; }
#container-list { max-height: 320px; overflow-y: auto; }
.row { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-bottom: 1px solid #2a2a3a; }
.row:hover { background: #2a2a3a; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.name { flex: 1; overflow: hidden; min-width: 0; }
.name-primary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.name-domain { font-size: 10px; color: #888; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; }
.regen { background: none; border: 1px solid #555; color: #aaa; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; flex-shrink: 0; }
.regen:hover { border-color: #888; color: #ddd; }
.actions { padding: 8px 12px; border-top: 1px solid #333; }
#regen-all { width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; }
#regen-all:hover { background: #444; color: #fff; }
.actions-row { display: flex; gap: 6px; margin-top: 6px; }
.actions-row button { flex: 1; padding: 6px; border-radius: 4px; cursor: pointer; font-size: 12px; }
.secondary { background: #2a2a3a; border: 1px solid #555; color: #aaa; }
.secondary:hover { background: #333; color: #ddd; }
.danger { background: #3a1a1a; border: 1px solid #663333; color: #ff613d; }
.danger:hover { background: #4a2020; color: #ff8866; }
.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; }

19
popup/popup.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<h1>ContainSite</h1>
<div id="container-list"></div>
<div class="actions">
<button id="regen-all">Regenerate All</button>
<div class="actions-row">
<button id="prune" class="secondary">Prune Unused</button>
<button id="reset" class="danger">Reset All</button>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

88
popup/popup.js Normal file
View File

@@ -0,0 +1,88 @@
async function loadContainers() {
const containers = await browser.runtime.sendMessage({ type: "getContainerList" });
const list = document.getElementById("container-list");
list.innerHTML = "";
for (const c of containers) {
const row = document.createElement("div");
row.className = "row";
const dot = document.createElement("span");
dot.className = `dot dot-${c.color}`;
row.appendChild(dot);
const nameWrap = document.createElement("div");
nameWrap.className = "name";
const name = document.createElement("div");
name.className = "name-primary";
name.textContent = c.name;
nameWrap.appendChild(name);
if (c.domain) {
const domain = document.createElement("div");
domain.className = "name-domain";
domain.textContent = c.domain;
nameWrap.appendChild(domain);
}
row.appendChild(nameWrap);
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
});
});
row.appendChild(toggle);
const regen = document.createElement("button");
regen.className = "regen";
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);
});
row.appendChild(regen);
list.appendChild(row);
}
}
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"; }, 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);
});
loadContainers();