Either fully removes navigator.bluetooth or replaces it with a fixed, non-revealing response set, to prevent fingerprinting/tracking via Bluetooth adapter presence and paired devices
// ==UserScript==
// @name Web Bluetooth API Spoofer
// @namespace https://violentmonkey.github.io/
// @version 1.0
// @description Either fully removes navigator.bluetooth or replaces it with a fixed, non-revealing response set, to prevent fingerprinting/tracking via Bluetooth adapter presence and paired devices
// @author Streampunk
// @icon data:image/png;base64,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
// @match *://*/*
// @run-at document-start
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ===================== CONFIG =====================
// 'block' - completely removes navigator.bluetooth, so any code checking
// `if (navigator.bluetooth)` or calling its methods behaves
// exactly as if the API never existed (indistinguishable from
// Firefox/Safari, which don't implement Web Bluetooth at all).
// 'fixed' - keeps navigator.bluetooth present (so feature-detection
// passes) but:
// - getAvailability() always resolves to a fixed value
// - getDevices() always resolves to an empty array
// (no previously-paired devices exposed)
// - requestDevice() always rejects with the same error a
// real browser throws when the user cancels the device
// picker, so no device-selection UI leaks any info and
// no site can silently detect connected devices.
const MODE = 'block'; // <-- change to 'fixed' if you prefer a fake-but-present API
// ====================================================
if (typeof navigator.bluetooth === 'undefined') {
return; // API not present in this browser at all, nothing to do
}
if (MODE === 'block') {
try {
Object.defineProperty(navigator, 'bluetooth', {
configurable: true,
get() { return undefined; },
});
} catch (e) {
try {
const proto = Object.getPrototypeOf(navigator);
Object.defineProperty(proto, 'bluetooth', {
configurable: true,
get() { return undefined; },
});
} catch (e2) {
console.warn('[block-bluetooth] failed to remove navigator.bluetooth', e2);
}
}
return;
}
if (MODE === 'fixed') {
// Fixed availability value. `false` is the safer/less revealing choice:
// most desktop and laptop machines without a Bluetooth adapter (or with
// it disabled) will report false, which is a very common, low-signal
// state. Change to `true` if you want to look like a machine that has
// Bluetooth hardware without exposing anything about it.
const FAKE_AVAILABILITY = false;
const listeners = {
availabilitychanged: [],
};
function notFoundError() {
// Mirrors the real DOMException a browser throws when the user
// dismisses the device chooser without selecting anything, or when
// no matching device is found.
return new DOMException(
'User cancelled the requestDevice() chooser.',
'NotFoundError'
);
}
const fakeBluetooth = {
getAvailability() {
return Promise.resolve(FAKE_AVAILABILITY);
},
getDevices() {
// No previously-paired/granted devices exposed.
return Promise.resolve([]);
},
requestDevice() {
// Always behaves as if the user closed the picker without
// choosing a device, so no real device (name, id, services) is
// ever revealed. This does not require/trigger any native UI.
return Promise.reject(notFoundError());
},
onavailabilitychanged: null,
addEventListener(type, listener) {
if (listeners[type] && typeof listener === 'function') {
listeners[type].push(listener);
}
},
removeEventListener(type, listener) {
if (listeners[type]) {
const idx = listeners[type].indexOf(listener);
if (idx !== -1) listeners[type].splice(idx, 1);
}
},
dispatchEvent(event) {
// Never actually triggered since availability never changes, kept
// only for interface completeness in case a site calls it directly.
const type = event && event.type;
if (listeners[type]) {
listeners[type].forEach((fn) => {
try { fn.call(fakeBluetooth, event); } catch (e) {}
});
}
const handlerName = 'on' + type;
if (typeof fakeBluetooth[handlerName] === 'function') {
try { fakeBluetooth[handlerName].call(fakeBluetooth, event); } catch (e) {}
}
return true;
},
};
try {
Object.defineProperty(navigator, 'bluetooth', {
configurable: true,
get() { return fakeBluetooth; },
});
} catch (e) {
try {
const proto = Object.getPrototypeOf(navigator);
Object.defineProperty(proto, 'bluetooth', {
configurable: true,
get() { return fakeBluetooth; },
});
} catch (e2) {
console.warn('[spoof-bluetooth] failed to override navigator.bluetooth', e2);
}
}
return;
}
console.warn('[bluetooth-privacy-script] unknown MODE value:', MODE);
})();