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
This commit is contained in:
sal
2026-02-28 23:43:12 -06:00
parent 246933599f
commit ba6449c5b0
6 changed files with 617 additions and 211 deletions

359
inject.js
View File

@@ -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) {