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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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