Theseus: media-device privacy fix + region/language pickers, AF under Privacy

- Privacy fix: enumerateDevices() leaked speaker (audiooutput) labels + device/
  group IDs even with camera/mic blocked. New "Hide media devices" (default on)
  blanks every device's label/deviceId/groupId and collapses to one per kind,
  matching Firefox — closes the WebRTC device-fingerprinting leak.
- Anti-fingerprinting moved into the Privacy section (own sidebar item removed).
- Location spoof: pick a world region (Europe/Asia/N&S America/Africa/Middle
  East/Australia) → representative coordinates, or Manual for exact lat/lon.
- Language spoof: pick from the top-10 world languages, or Manual for any locale.
This commit is contained in:
Local Dev 2026-07-30 23:11:28 +02:00
parent 0f823ba583
commit 69c2afca81
2 changed files with 83 additions and 23 deletions

32
main.js
View file

@ -115,12 +115,13 @@ const SETTINGS_DEFAULTS = {
webrtcMode: "public_only", // WebRTC IP policy: default | public_only | public_private | disable_udp webrtcMode: "public_only", // WebRTC IP policy: default | public_only | public_private | disable_udp
blockCamera: true, // deny camera by default (also hides camera labels from fingerprinting) blockCamera: true, // deny camera by default (also hides camera labels from fingerprinting)
blockMicrophone: true, // deny microphone by default (also hides mic labels) blockMicrophone: true, // deny microphone by default (also hides mic labels)
hideMediaDevices: true, // blank all enumerateDevices info (esp. speaker labels/ids) like Firefox
restoreSession: true, // reopen last session's tabs on launch restoreSession: true, // reopen last session's tabs on launch
backgroundThrottle: true, // throttle inactive tabs / the window when unfocused backgroundThrottle: true, // throttle inactive tabs / the window when unfocused
// Anti-fingerprinting — each: show (real) | hide (neutral) | spoof (auto decoy) | manual (user value) // Anti-fingerprinting — each: show (real) | hide (neutral) | spoof (auto decoy) | manual (user value)
timezoneMode: "show", timezoneValue: "Europe/Berlin", // IANA zone for manual timezoneMode: "show", timezoneValue: "Europe/Berlin", // IANA zone for manual
languageMode: "show", languageValue: "en-US", // locale for manual languageMode: "show", languageSpoof: "en-US", languageValue: "en-US", // spoof = top-10 pick, manual = free text
locationMode: "hide", locationLat: "40.7128", locationLon: "-74.0060", // coords for manual (default: deny) locationMode: "hide", locationRegion: "europe", locationLat: "40.7128", locationLon: "-74.0060", // spoof by region, or manual coords
searchEngine: "duckduckgo",// default search engine (built-in id or a custom id) searchEngine: "duckduckgo",// default search engine (built-in id or a custom id)
enabledEngines: DEFAULT_ENABLED.slice(), // which built-in engines show in the dropdown enabledEngines: DEFAULT_ENABLED.slice(), // which built-in engines show in the dropdown
customEngines: [], // user-added: [{ id, name, url-with-%s }] customEngines: [], // user-added: [{ id, name, url-with-%s }]
@ -178,17 +179,28 @@ function effTimezone() {
function effLocale() { function effLocale() {
switch (settings.languageMode) { switch (settings.languageMode) {
case "hide": return "en-US"; case "hide": return "en-US";
case "spoof": return SPOOF.lang; case "spoof": return settings.languageSpoof || SPOOF.lang; // chosen from the top-languages list
case "manual": return settings.languageValue || "en-US"; case "manual": return settings.languageValue || "en-US";
default: return null; default: return null;
} }
} }
// Representative coordinates per world region — used when the spoofed location is
// set to a region rather than exact coordinates (a major city stands in for each).
const REGIONS = {
europe: { lat: 52.5200, lon: 13.4050 }, // Berlin
asia: { lat: 35.6762, lon: 139.6503 }, // Tokyo
north_america: { lat: 40.7128, lon: -74.0060 }, // New York
south_america: { lat: -23.5505, lon: -46.6333 }, // São Paulo
africa: { lat: -1.2921, lon: 36.8219 }, // Nairobi
middle_east: { lat: 25.2048, lon: 55.2708 }, // Dubai
australia: { lat: -33.8688, lon: 151.2093 }, // Sydney
};
// Geolocation: null = show (real, allowed); "deny" = hide (blocked); // Geolocation: null = show (real, allowed); "deny" = hide (blocked);
// {lat,lon} = spoof/manual (allowed, but coordinates are overridden in-page). // {lat,lon} = spoof (region-based) / manual (exact) — overridden in-page.
function effLocation() { function effLocation() {
const m = settings.locationMode; const m = settings.locationMode;
if (m === "hide") return "deny"; if (m === "hide") return "deny";
if (m === "spoof") return { lat: SPOOF.lat, lon: SPOOF.lon }; if (m === "spoof") { const r = REGIONS[settings.locationRegion] || REGIONS.europe; return { lat: r.lat, lon: r.lon }; }
if (m === "manual") return { lat: Number(settings.locationLat) || 0, lon: Number(settings.locationLon) || 0 }; if (m === "manual") return { lat: Number(settings.locationLat) || 0, lon: Number(settings.locationLon) || 0 };
return null; // show return null; // show
} }
@ -219,6 +231,12 @@ async function applyFingerprint(wc) {
const pos = `{coords:{latitude:${geo.lat},longitude:${geo.lon},accuracy:100,altitude:null,altitudeAccuracy:null,heading:null,speed:null},timestamp:Date.now()}`; const pos = `{coords:{latitude:${geo.lat},longitude:${geo.lon},accuracy:100,altitude:null,altitudeAccuracy:null,heading:null,speed:null},timestamp:Date.now()}`;
src += `try{const p=()=>(${pos});if(navigator.geolocation){navigator.geolocation.getCurrentPosition=(ok)=>{try{ok(p())}catch(e){}};navigator.geolocation.watchPosition=(ok)=>{try{ok(p())}catch(e){}return 0};}}catch(e){}`; src += `try{const p=()=>(${pos});if(navigator.geolocation){navigator.geolocation.getCurrentPosition=(ok)=>{try{ok(p())}catch(e){}};navigator.geolocation.watchPosition=(ok)=>{try{ok(p())}catch(e){}return 0};}}catch(e){}`;
} }
// Media-device privacy: Chromium leaks audiooutput (speaker) labels + deviceIds
// via enumerateDevices even when camera/mic are blocked. Like Firefox, blank
// every device's label/deviceId/groupId and collapse to one entry per kind.
if (settings.hideMediaDevices) {
src += `try{const md=navigator.mediaDevices;if(md&&md.enumerateDevices){const o=md.enumerateDevices.bind(md);md.enumerateDevices=async()=>{let l=[];try{l=await o()}catch(e){}const ks=[...new Set(l.map(d=>d.kind))];return ks.map(kind=>({deviceId:'',kind:kind,label:'',groupId:'',toJSON(){return{deviceId:'',kind:kind,label:'',groupId:''}}}))};}}catch(e){}`;
}
if (src) { if (src) {
const res = await wc.debugger.sendCommand("Page.addScriptToEvaluateOnNewDocument", { source: src }); const res = await wc.debugger.sendCommand("Page.addScriptToEvaluateOnNewDocument", { source: src });
wc._langScript = res.identifier; wc._langScript = res.identifier;
@ -826,8 +844,8 @@ ipcMain.handle("settings-set", (_e, key, val) => {
if (key === "webrtcMode") applyWebRTCPolicy(); if (key === "webrtcMode") applyWebRTCPolicy();
if (key === "theme") applyTheme(); if (key === "theme") applyTheme();
if (key === "backgroundThrottle") applyThrottle(); if (key === "backgroundThrottle") applyThrottle();
if (["timezoneMode", "timezoneValue", "languageMode", "languageValue", if (["timezoneMode", "timezoneValue", "languageMode", "languageSpoof", "languageValue",
"locationMode", "locationLat", "locationLon"].includes(key)) { applyFingerprintAll(); applyAcceptLanguage(); } "locationMode", "locationRegion", "locationLat", "locationLon", "hideMediaDevices"].includes(key)) { applyFingerprintAll(); applyAcceptLanguage(); }
return settings; return settings;
}); });
ipcMain.handle("set-chrome-height", (_e, h) => { ipcMain.handle("set-chrome-height", (_e, h) => {

View file

@ -18,6 +18,8 @@
section{max-width:640px} section{max-width:640px}
h1{font-size:1.4rem;margin:0 0 .2rem} h1{font-size:1.4rem;margin:0 0 .2rem}
.lede{color:var(--mut);margin:0 0 1.8rem;font-size:13.5px} .lede{color:var(--mut);margin:0 0 1.8rem;font-size:13.5px}
h2.sub{font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);margin:1.8rem 0 .2rem;border-top:1px solid var(--line);padding-top:1.4rem}
.subd{color:var(--mut);margin:0 0 1rem;font-size:13px}
.row{display:flex;align-items:center;gap:16px;background:var(--panel);border:1px solid var(--line); .row{display:flex;align-items:center;gap:16px;background:var(--panel);border:1px solid var(--line);
border-radius:12px;padding:14px 18px;margin-bottom:10px} border-radius:12px;padding:14px 18px;margin-bottom:10px}
.row .txt{flex:1} .row .txt{flex:1}
@ -71,7 +73,6 @@
<a data-sec="general" class="active">General</a> <a data-sec="general" class="active">General</a>
<a data-sec="performance">Performance</a> <a data-sec="performance">Performance</a>
<a data-sec="privacy">Privacy</a> <a data-sec="privacy">Privacy</a>
<a data-sec="fingerprint">Anti-fingerprinting</a>
</nav> </nav>
<div class="content"> <div class="content">
<!-- GENERAL --> <!-- GENERAL -->
@ -133,11 +134,13 @@
<div class="txt"><div class="t">Block microphone</div><div class="d">Deny microphone by default — also hides its name from fingerprinting.</div></div> <div class="txt"><div class="t">Block microphone</div><div class="d">Deny microphone by default — also hides its name from fingerprinting.</div></div>
<label class="sw"><input type="checkbox" id="blockMicrophone"><span class="track"><span class="knob"></span></span></label> <label class="sw"><input type="checkbox" id="blockMicrophone"><span class="track"><span class="knob"></span></span></label>
</div> </div>
</section> <div class="row">
<!-- ANTI-FINGERPRINTING --> <div class="txt"><div class="t">Hide media devices</div><div class="d">Blank the labels and IDs of all cameras, microphones <b>and speakers</b> from <code>enumerateDevices()</code> — closes a WebRTC fingerprinting leak, like Firefox.</div></div>
<section id="fingerprint" hidden> <label class="sw"><input type="checkbox" id="hideMediaDevices"><span class="track"><span class="knob"></span></span></label>
<h1>Anti-fingerprinting</h1> </div>
<p class="lede">For each, choose <b>Show</b> (real), <b>Hide</b> (neutral value), <b>Spoof</b> (an automatic decoy), or <b>Manual</b> (set your own).</p>
<h2 class="sub">Anti-fingerprinting</h2>
<p class="subd">For each: <b>Show</b> (real), <b>Hide</b> (neutral value), <b>Spoof</b> (a decoy), or <b>Manual</b> (set your own).</p>
<div class="row"> <div class="row">
<div class="txt"><div class="t">Timezone</div><div class="d">What sites read via JavaScript (Intl / Date).</div></div> <div class="txt"><div class="t">Timezone</div><div class="d">What sites read via JavaScript (Intl / Date).</div></div>
<div class="ctl"> <div class="ctl">
@ -146,16 +149,37 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="txt"><div class="t">Language</div><div class="d">navigator.language and the Accept-Language header.</div></div> <div class="txt"><div class="t">Language</div><div class="d">navigator.language and the Accept-Language header. <b>Spoof</b> reports a major world language; <b>Manual</b> lets you type any locale.</div></div>
<div class="ctl"> <div class="ctl">
<select id="languageMode"><option value="show">Show real</option><option value="hide">Hide (en-US)</option><option value="spoof">Spoof (auto)</option><option value="manual">Manual…</option></select> <select id="languageMode"><option value="show">Show real</option><option value="hide">Hide (en-US)</option><option value="spoof">Spoof (choose)</option><option value="manual">Manual…</option></select>
<input id="languageValue" placeholder="e.g. de-DE" hidden> <select id="languageSpoof" hidden>
<option value="en-US">English</option>
<option value="zh-CN">Chinese (中文)</option>
<option value="es-ES">Spanish (Español)</option>
<option value="hi-IN">Hindi (हिन्दी)</option>
<option value="ar">Arabic (العربية)</option>
<option value="pt-BR">Portuguese (Português)</option>
<option value="ru-RU">Russian (Русский)</option>
<option value="fr-FR">French (Français)</option>
<option value="de-DE">German (Deutsch)</option>
<option value="ja-JP">Japanese (日本語)</option>
</select>
<input id="languageValue" placeholder="e.g. it-IT" hidden>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="txt"><div class="t">Location</div><div class="d"><b>Hide</b> denies geolocation; <b>Spoof</b>/<b>Manual</b> report coordinates you choose.</div></div> <div class="txt"><div class="t">Location</div><div class="d"><b>Hide</b> denies geolocation; <b>Spoof</b> reports a chosen region; <b>Manual</b> reports exact coordinates.</div></div>
<div class="ctl"> <div class="ctl">
<select id="locationMode"><option value="show">Show real</option><option value="hide">Hide (block)</option><option value="spoof">Spoof (auto)</option><option value="manual">Manual…</option></select> <select id="locationMode"><option value="show">Show real</option><option value="hide">Hide (block)</option><option value="spoof">Spoof (region)</option><option value="manual">Manual…</option></select>
<select id="locationRegion" hidden>
<option value="europe">Europe</option>
<option value="asia">Asia</option>
<option value="north_america">North America</option>
<option value="south_america">South America</option>
<option value="africa">Africa</option>
<option value="middle_east">Middle East</option>
<option value="australia">Australia</option>
</select>
<div class="coords" id="locationCoords" hidden> <div class="coords" id="locationCoords" hidden>
<input id="locationLat" placeholder="lat"><input id="locationLon" placeholder="lon"> <input id="locationLat" placeholder="lat"><input id="locationLon" placeholder="lon">
</div> </div>
@ -168,13 +192,13 @@
<script> <script>
const C = window.cfg; const C = window.cfg;
// sidebar navigation // sidebar navigation
const sections = ["general", "performance", "privacy", "fingerprint"]; const sections = ["general", "performance", "privacy"];
document.querySelectorAll(".side a").forEach((a) => a.onclick = () => { document.querySelectorAll(".side a").forEach((a) => a.onclick = () => {
document.querySelectorAll(".side a").forEach((x) => x.classList.toggle("active", x === a)); document.querySelectorAll(".side a").forEach((x) => x.classList.toggle("active", x === a));
for (const s of sections) document.getElementById(s).hidden = (s !== a.dataset.sec); for (const s of sections) document.getElementById(s).hidden = (s !== a.dataset.sec);
}); });
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone"]; const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices"];
C.get().then((s) => { C.get().then((s) => {
for (const k of TOGGLES) { for (const k of TOGGLES) {
const el = document.getElementById(k); if (!el) continue; const el = document.getElementById(k); if (!el) continue;
@ -229,10 +253,28 @@
m.addEventListener("change", () => { C.set(mode, m.value); apply(m.value === "manual"); }); m.addEventListener("change", () => { C.set(mode, m.value); apply(m.value === "manual"); });
}; };
const val = (id) => { const v = document.getElementById(id); v.value = s[id] ?? ""; v.addEventListener("change", () => C.set(id, v.value.trim())); return v; }; const val = (id) => { const v = document.getElementById(id); v.value = s[id] ?? ""; v.addEventListener("change", () => C.set(id, v.value.trim())); return v; };
const tzV = val("timezoneValue"), lgV = val("languageValue"), laV = val("locationLat"), loV = val("locationLon"); const tzV = val("timezoneValue"), lgV = val("languageValue");
val("locationLat"); val("locationLon");
bind("timezoneMode", null, (manual) => tzV.hidden = !manual); bind("timezoneMode", null, (manual) => tzV.hidden = !manual);
bind("languageMode", null, (manual) => lgV.hidden = !manual); // language: top-10 dropdown when spoofing, free-text locale when manual
bind("locationMode", null, (manual) => document.getElementById("locationCoords").hidden = !manual); const lngMode = document.getElementById("languageMode");
const lngSpoof = document.getElementById("languageSpoof");
lngSpoof.value = s.languageSpoof || "en-US";
lngSpoof.addEventListener("change", () => C.set("languageSpoof", lngSpoof.value));
const applyLng = () => { lngSpoof.hidden = lngMode.value !== "spoof"; lgV.hidden = lngMode.value !== "manual"; };
lngMode.value = s.languageMode || "show";
applyLng();
lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); });
// location: region dropdown when spoofing, exact coords when manual
const locMode = document.getElementById("locationMode");
const locRegion = document.getElementById("locationRegion");
const locCoords = document.getElementById("locationCoords");
locRegion.value = s.locationRegion || "europe";
locRegion.addEventListener("change", () => C.set("locationRegion", locRegion.value));
const applyLoc = () => { locRegion.hidden = locMode.value !== "spoof"; locCoords.hidden = locMode.value !== "manual"; };
locMode.value = s.locationMode || "show";
applyLoc();
locMode.addEventListener("change", () => { C.set("locationMode", locMode.value); applyLoc(); });
}); });
</script> </script>
</body> </body>