Merge stealth-overhaul into main

Resolved conflicts:
- page.rs: keep both set_blocked_urls (#315) and stealth client wiring
- ops.rs: keep SHA-512/224 + SHA-512/256 from stealth branch; keep
  the unreachable catch-all (vec![]) from #314 instead of silent
  SHA-256 fallback; updated comment accordingly
- bootstrap.js: extend the digest algorithm allowlist to include
  SHA-512/224 and SHA-512/256 to match the Rust additions
This commit is contained in:
SGavrl committed 2026-06-23 18:05:55 +00:00
commit 8582fcceca
9 files changed
+471 -88

No files matched your search

+134 -46
View File
@@ -154,7 +154,9 @@ function _fpNoise(x, y, channel) {
var _fpCache = null;
function _getFp() {
if (_fpCache) return _fpCache;
const isMac = (globalThis.__obscura_ua_platform || 'Windows') === 'macOS';
const _uaPlat = globalThis.__obscura_ua_platform || 'Windows';
const isMac = _uaPlat === 'macOS';
const isLinux = _uaPlat === 'Linux';
const gpuPool = isMac ? [
'ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)',
'ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Pro, Unspecified Version)',
@@ -162,6 +164,14 @@ function _getFp() {
'ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Pro, Unspecified Version)',
'ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)',
'ANGLE (Intel Inc., ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)',
] : isLinux ? [
'ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)',
'ANGLE (Intel, Mesa Intel(R) Iris(R) Xe Graphics (TGL GT2), OpenGL 4.6)',
'ANGLE (Intel, Mesa Intel(R) UHD Graphics 770 (RPL-S), OpenGL 4.6)',
'ANGLE (AMD, AMD Radeon RX 580 (polaris10, LLVM 15.0.7, DRM 3.54, LLVM 15.0.7), OpenGL 4.6)',
'ANGLE (AMD, AMD Radeon RX 6700 XT (navi22, LLVM 16.0.6, DRM 3.54, LLVM 16.0.6), OpenGL 4.6)',
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 OpenGL 4.6)',
'ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 OpenGL 4.6)',
] : [
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)',
'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)',
@@ -180,6 +190,10 @@ function _getFp() {
'Google Inc. (Apple)','Google Inc. (Apple)','Google Inc. (Apple)',
'Google Inc. (Apple)','Google Inc. (Apple)',
'Google Inc. (Intel Inc.)',
] : isLinux ? [
'Google Inc. (Intel)','Google Inc. (Intel)','Google Inc. (Intel)',
'Google Inc. (AMD)','Google Inc. (AMD)',
'Google Inc. (NVIDIA)','Google Inc. (NVIDIA)',
] : [
'Google Inc. (NVIDIA)','Google Inc. (NVIDIA)','Google Inc. (NVIDIA)',
'Google Inc. (Intel)','Google Inc. (Intel)',
@@ -2577,7 +2591,7 @@ function __currentUrl() {
}
globalThis.location = {
get href() { return __currentUrl(); },
set href(url) { globalThis.__virtualUrl = null; Deno.core.ops.op_navigate(_resolveUrl(url), 'GET', ''); },
set href(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
get origin() { try { return new URL(this.href).origin; } catch { return ""; } },
get protocol() { try { return new URL(this.href).protocol; } catch { return ""; } },
get host() { try { return new URL(this.href).host; } catch { return ""; } },
@@ -2587,14 +2601,14 @@ globalThis.location = {
get hash() { try { return new URL(this.href).hash; } catch { return ""; } },
get port() { try { return new URL(this.href).port; } catch { return ""; } },
toString() { return this.href; },
assign(url) { globalThis.__virtualUrl = null; Deno.core.ops.op_navigate(_resolveUrl(url), 'GET', ''); },
reload() {},
replace(url) { globalThis.__virtualUrl = null; Deno.core.ops.op_navigate(_resolveUrl(url), 'GET', ''); },
assign(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
reload() { var r = _resolveUrl(this.href); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
replace(url) { var r = _resolveUrl(url); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
};
const _locationObj = globalThis.location;
Object.defineProperty(globalThis, 'location', {
get() { return _locationObj; },
set(url) { Deno.core.ops.op_navigate(_resolveUrl(String(url)), 'GET', ''); },
set(url) { var r = _resolveUrl(String(url)); globalThis.__virtualUrl = r; Deno.core.ops.op_navigate(r, 'GET', ''); },
configurable: false,
enumerable: true,
});
@@ -2752,8 +2766,35 @@ globalThis.NetworkInformation = NetworkInformation;
globalThis.ContentIndex = class ContentIndex {};
function _chromeMajor() {
var m = (globalThis.__obscura_ua || '').match(/Chrome\/(\d+)/);
return m ? (m[1] | 0) : 145;
}
// Chromium derives the sec-ch-ua GREASE brand, version, and brand order
// deterministically from the Chrome major version
// (components/embedder_support/user_agent_utils.cc). Replicating it keeps
// sec-ch-ua and userAgentData exact for every profile version rather than
// hardcoding one static token.
var _GREASE_CHARS = [' ', '(', ':', '-', '.', '/', ')', ';', '=', '?', '_'];
var _GREASE_VER = ['8', '99', '24'];
var _BRAND_PERMS = [[0,1,2],[0,2,1],[1,0,2],[1,2,0],[2,0,1],[2,1,0]];
function _uaBrands() {
var seed = _chromeMajor();
var grease = {
brand: 'Not' + _GREASE_CHARS[seed % 11] + 'A' + _GREASE_CHARS[(seed + 1) % 11] + 'Brand',
version: _GREASE_VER[seed % 3],
};
var ordered = [
grease,
{brand: 'Chromium', version: String(seed)},
{brand: 'Google Chrome', version: String(seed)},
];
var p = _BRAND_PERMS[seed % 6];
return [ordered[p[0]], ordered[p[1]], ordered[p[2]]];
}
globalThis.navigator = {
get userAgent() { return globalThis.__obscura_ua || "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; },
get userAgent() { return globalThis.__obscura_ua || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; },
get appVersion() { return this.userAgent.replace('Mozilla/', ''); },
language: "en-US", languages: ["en-US","en"], get platform() { return globalThis.__obscura_platform || "Win32"; },
onLine: true, cookieEnabled: true, hardwareConcurrency: 8,
@@ -2779,24 +2820,22 @@ globalThis.navigator = {
]);
},
userAgentData: {
brands: [
{brand: "Google Chrome", version: "145"},
{brand: "Chromium", version: "145"},
{brand: "Not;A=Brand", version: "24"},
],
mobile: false,
get brands() { return _uaBrands(); },
get platform() { return globalThis.__obscura_ua_platform || "Windows"; },
getHighEntropyValues(hints) {
var brands = _uaBrands();
return Promise.resolve({
architecture: "x86",
bitness: "64",
brands: [{brand:"Google Chrome",version:"145"},{brand:"Chromium",version:"145"},{brand:"Not;A=Brand",version:"24"}],
fullVersionList: [{brand:"Google Chrome",version:"145.0.0.0"},{brand:"Chromium",version:"145.0.0.0"},{brand:"Not;A=Brand",version:"24.0.0.0"}],
brands: brands,
fullVersionList: brands.map(function(b) { return {brand: b.brand, version: b.version + ".0.0.0"}; }),
mobile: false,
model: "",
platform: globalThis.__obscura_ua_platform || "Windows",
platformVersion: globalThis.__obscura_ua_platform_version || "15.0.0",
uaFullVersion: "145.0.0.0",
uaFullVersion: _chromeMajor() + ".0.0.0",
wow64: false,
});
},
toJSON() { return {brands:this.brands,mobile:this.mobile,platform:this.platform}; },
@@ -2817,8 +2856,12 @@ globalThis.navigator = {
},
clipboard: { writeText(){return Promise.resolve();}, readText(){return Promise.resolve("");} },
permissions: { query(params){
if (params?.name === 'notifications') return Promise.resolve({state:"prompt",onchange:null});
return Promise.resolve({state:"granted"});
var n = params && params.name;
// Chrome defaults privacy-sensitive permissions to "prompt", not "granted";
// returning "granted" for camera/microphone is a bot tell.
if (n === 'notifications') return Promise.resolve({state: (globalThis.Notification && Notification.permission === 'granted') ? 'granted' : 'prompt', onchange: null});
if (n === 'geolocation' || n === 'camera' || n === 'microphone' || n === 'midi') return Promise.resolve({state: 'prompt', onchange: null});
return Promise.resolve({state: 'granted', onchange: null});
} },
getBattery() { return Promise.resolve({ charging: _fp('batteryCharging'), chargingTime: _fp('batteryCharging') ? 0 : Infinity, dischargingTime: _fp('batteryCharging') ? Infinity : Math.floor(3600 + _fpRand(250) * 7200), level: _fp('batteryLevel'), addEventListener(){} }); },
getGamepads() { return []; },
@@ -3003,6 +3046,51 @@ function _installWasmStreamingFallback() {
}
_installWasmStreamingFallback();
// Serialize a FormData into a multipart/form-data body the way a browser does
// when it is passed as fetch()/XHR body. The previous shim did String(body),
// so a FormData became the literal "[object Object]" and the multipart payload
// (with its boundary) was lost; servers replied "Invalid boundary for
// multipart/form-data" (e.g. the AWS WAF challenge /mp_verify POST).
function _formDataToMultipart(fd) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let bnd = '----WebKitFormBoundary';
for (let i = 0; i < 16; i++) bnd += chars[Math.floor(Math.random() * chars.length)];
let out = '';
const entries = fd._d || [];
for (let i = 0; i < entries.length; i++) {
const k = entries[i][0], v = entries[i][1];
out += '--' + bnd + '\r\n';
if (v != null && typeof v === 'object' && v._bytes != null) {
out += 'Content-Disposition: form-data; name="' + k + '"; filename="' + (v.name || 'blob') + '"\r\n';
out += 'Content-Type: ' + (v.type || 'application/octet-stream') + '\r\n\r\n';
try { out += new TextDecoder().decode(v._bytes); } catch (e) {}
out += '\r\n';
} else {
out += 'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + String(v) + '\r\n';
}
}
out += '--' + bnd + '--\r\n';
return { boundary: bnd, body: out };
}
// Coerce a fetch()/XHR body into the string op_fetch_url expects, attaching a
// Content-Type header for body types that need one (FormData, URLSearchParams).
function _serializeBody(initBody, headers) {
if (initBody == null || initBody === '') return '';
if (initBody instanceof FormData) {
const mp = _formDataToMultipart(initBody);
headers['Content-Type'] = 'multipart/form-data; boundary=' + mp.boundary;
return mp.body;
}
if (initBody instanceof URLSearchParams) {
if (!Object.keys(headers).some(k => k.toLowerCase() === 'content-type')) {
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
}
return initBody.toString();
}
return typeof initBody === 'string' ? initBody : String(initBody);
}
globalThis.fetch = async (input, init = {}) => {
let url = typeof input === "string"
? input
@@ -3016,8 +3104,9 @@ globalThis.fetch = async (input, init = {}) => {
} catch(e) { /* keep as-is if URL resolution fails */ }
}
const method = init.method || (input instanceof Request ? input.method : "GET");
const hdrs = JSON.stringify(init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init.headers || {});
const body = init.body ? String(init.body) : "";
let _h = init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : (init.headers || {});
const body = _serializeBody(init.body, _h);
const hdrs = JSON.stringify(_h);
const fetchMode = init.mode || (input instanceof Request ? input.mode : "cors");
const pageOrigin = (function() { try { const u = new URL(_domParse("document_url") || "about:blank"); return u.origin; } catch(e) { return ""; } })();
const raw = await Deno.core.ops.op_fetch_url(url, method, hdrs, body, pageOrigin, fetchMode);
@@ -6328,11 +6417,25 @@ globalThis.Worker = class Worker {
};
globalThis.__blobStore = globalThis.__blobStore || {};
const _origCreateObjectURL = URL.createObjectURL;
URL.createObjectURL = function(blob) {
if (blob && typeof blob.text === 'function') {
if (blob) {
const id = 'blob:obscura/' + Math.random().toString(36).substring(2);
blob.text().then(text => { globalThis.__blobStore[id] = text; });
// Store synchronously so a Worker built from the blob URL in the same
// tick sees its source. Blob-URL Worker construction is synchronous in
// real browsers; the previous async blob.text().then() store raced the
// Worker constructor, so new Worker(blobURL) fell through to fetch() and
// failed (net::ERR_FAILED), which broke AWS WAF's proof-of-work worker.
// The obscura Blob materializes _bytes in its constructor; fall back to
// the async text() store only for foreign Blob shims without _bytes.
if (blob._bytes) {
let text = '';
try { text = new TextDecoder().decode(blob._bytes); } catch (e) {}
globalThis.__blobStore[id] = text;
} else if (typeof blob.text === 'function') {
blob.text().then(text => { globalThis.__blobStore[id] = text; });
} else {
globalThis.__blobStore[id] = '';
}
return id;
}
return 'blob:obscura/fallback';
@@ -6421,7 +6524,8 @@ if (!globalThis.crypto.subtle) {
globalThis.crypto.subtle = {
async digest(algorithm, data) {
const name = (typeof algorithm === 'string' ? algorithm : algorithm?.name || '').toUpperCase().replace('_', '-');
if (name !== 'SHA-1' && name !== 'SHA-256' && name !== 'SHA-384' && name !== 'SHA-512') {
if (name !== 'SHA-1' && name !== 'SHA-256' && name !== 'SHA-384' && name !== 'SHA-512' &&
name !== 'SHA-512/224' && name !== 'SHA-512/256') {
throw new DOMException('Unrecognized algorithm name', 'NotSupportedError');
}
let bytes;
@@ -6840,6 +6944,10 @@ if (typeof ShadowRoot !== 'undefined' && !ShadowRoot.prototype.elementFromPoint)
globalThis.__obscura_init = function() {
_fpSeed = Date.now() ^ (Math.random() * 0xFFFFFFFF >>> 0);
_fpCache = null;
// A real navigation just completed (this runs after set_url), so drop any
// URL a location setter previewed synchronously and let document_url drive
// location.href again, including any redirect target.
globalThis.__virtualUrl = null;
_installWasmStreamingFallback();
globalThis.document = new Document(+_dom("document_node_id"));
@@ -6868,29 +6976,9 @@ globalThis.__obscura_init = function() {
};
globalThis.Notification.permission = "default";
if (globalThis.__obscura_stealth) {
var _stealthBrands = [
{brand: "Not:A-Brand", version: "99"},
{brand: "Google Chrome", version: "145"},
{brand: "Chromium", version: "145"},
];
globalThis.navigator.userAgentData.brands = _stealthBrands;
globalThis.navigator.userAgentData.getHighEntropyValues = function(hints) {
return Promise.resolve({
architecture: "x86", bitness: "64",
brands: _stealthBrands,
fullVersionList: [
{brand:"Not:A-Brand",version:"99.0.0.0"},
{brand:"Google Chrome",version:"145.0.0.0"},
{brand:"Chromium",version:"145.0.0.0"},
],
mobile: false, model: "",
platform: "Windows",
platformVersion: "10.0.0",
uaFullVersion: "145.0.0.0",
});
};
}
// userAgentData brands and getHighEntropyValues now derive the Chrome
// version from navigator.userAgent and read the platform from the page
// globals, so every stealth surface agrees without a per-mode override.
// Hide internals (_*, obscura, Obscura). The set of keys is static at
// snapshot-build time, so we precompute it ONCE below (after this