Greasy Fork is available in English.

Web Bluetooth API Spoofer

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==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);
})();