Battery Status API Spoofer

Either fully removes navigator.getBattery() or replaces it with a fixed 100%-charged state, to prevent fingerprinting/tracking via battery level and charging status

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Battery Status API Spoofer
// @namespace    https://violentmonkey.github.io/
// @version      1.0
// @description  Either fully removes navigator.getBattery() or replaces it with a fixed 100%-charged state, to prevent fingerprinting/tracking via battery level and charging status
// @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 getBattery()/navigator.battery, so calls
  //           throw/are undefined, exactly as if the API never existed
  //           (indistinguishable from browsers that dropped it, e.g.
  //           current Firefox/Safari).
  // 'fixed' - keeps the API present but always returns a fixed, plausible
  //           100%-charged / plugged-in state instead of real battery data.
  const MODE = 'block'; // <-- change to 'fixed' if you prefer a fake value
  // ====================================================

  const hasGetBattery = typeof navigator.getBattery === 'function';
  const legacyProps = ['battery', 'mozBattery', 'webkitBattery'].filter((p) => p in navigator);

  if (!hasGetBattery && legacyProps.length === 0) {
    return; // nothing to do, API not present at all
  }

  if (MODE === 'block') {
    // --- Fully remove the API ---

    if (hasGetBattery) {
      try {
        Object.defineProperty(navigator, 'getBattery', {
          configurable: true,
          value: undefined,
        });
      } catch (e) {
        try {
          const proto = Object.getPrototypeOf(navigator);
          Object.defineProperty(proto, 'getBattery', {
            configurable: true,
            value: undefined,
          });
        } catch (e2) {
          console.warn('[block-battery] failed to remove getBattery', e2);
        }
      }
    }

    legacyProps.forEach((prop) => {
      try {
        Object.defineProperty(navigator, prop, {
          configurable: true,
          get() { return undefined; },
        });
      } catch (e) {
        console.warn(`[block-battery] failed to remove navigator.${prop}`, e);
      }
    });

    return;
  }

  if (MODE === 'fixed') {
    // --- Return a fixed, plausible battery state ---

    // Fully charged and plugged in: the most common/least informative
    // state (doesn't reveal usage patterns, discharge rate, or battery
    // health, which were previously used for cross-session tracking).
    const FAKE_BATTERY_STATE = {
      charging: true,
      chargingTime: 0,
      dischargingTime: Infinity,
      level: 1.0,
    };

    function createFakeBatteryManager() {
      const listeners = {
        chargingchange: [],
        chargingtimechange: [],
        dischargingtimechange: [],
        levelchange: [],
      };

      const state = { ...FAKE_BATTERY_STATE };

      // Minimal EventTarget-like object exposing the same interface as the
      // real BatteryManager (properties + on*change handlers + addEventListener)
      const battery = {
        get charging() { return state.charging; },
        get chargingTime() { return state.chargingTime; },
        get dischargingTime() { return state.dischargingTime; },
        get level() { return state.level; },

        onchargingchange: null,
        onchargingtimechange: null,
        ondischargingtimechange: null,
        onlevelchange: 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 state never changes, but kept
          // 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(battery, event); } catch (e) {}
            });
          }
          const handlerName = 'on' + type;
          if (typeof battery[handlerName] === 'function') {
            try { battery[handlerName].call(battery, event); } catch (e) {}
          }
          return true;
        },
      };

      return battery;
    }

    const fakeBattery = createFakeBatteryManager();
    const fakeBatteryPromise = Promise.resolve(fakeBattery);

    if (hasGetBattery) {
      try {
        Object.defineProperty(navigator, 'getBattery', {
          configurable: true,
          writable: true,
          value: function () {
            return fakeBatteryPromise;
          },
        });
      } catch (e) {
        try {
          const proto = Object.getPrototypeOf(navigator);
          Object.defineProperty(proto, 'getBattery', {
            configurable: true,
            writable: true,
            value: function () {
              return fakeBatteryPromise;
            },
          });
        } catch (e2) {
          console.warn('[spoof-battery] failed to override getBattery', e2);
        }
      }
    }

    legacyProps.forEach((prop) => {
      try {
        Object.defineProperty(navigator, prop, {
          configurable: true,
          get() { return fakeBattery; },
        });
      } catch (e) {
        console.warn(`[spoof-battery] failed to override navigator.${prop}`, e);
      }
    });

    return;
  }

  console.warn('[battery-privacy-script] unknown MODE value:', MODE);
})();