Either fully removes navigator.getBattery() or replaces it with a fixed 100%-charged state, to prevent fingerprinting/tracking via battery level and charging status
// ==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);
})();