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:
parent
0f823ba583
commit
69c2afca81
2 changed files with 83 additions and 23 deletions
32
main.js
32
main.js
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue