diff --git a/.documentation/addon description/de/description.txt b/.documentation/addon description/de/description.txt index d0b63b6..4a02c22 100644 --- a/.documentation/addon description/de/description.txt +++ b/.documentation/addon description/de/description.txt @@ -38,6 +38,7 @@ Beschützte "Fingerprinting"-APIs:
  • window (standardmäßig deaktiviert)
  • DOMRect
  • navigator (standardmäßig deaktiviert)
  • +
  • screen
  • Falls Sie Fehler finden oder Verbesserungsvorschläge haben, teilen Sie mir das bitte auf https://github.com/kkapsner/CanvasBlocker/issues mit. \ No newline at end of file diff --git a/.documentation/addon description/en/description.txt b/.documentation/addon description/en/description.txt index 8a0ae78..6108243 100644 --- a/.documentation/addon description/en/description.txt +++ b/.documentation/addon description/en/description.txt @@ -39,6 +39,7 @@ Protected "fingerprinting" APIs:
  • window (disabled by default)
  • DOMRect
  • navigator (disabled by default)
  • +
  • screen
  • Please report issues and feature requests at https://github.com/kkapsner/CanvasBlocker/issues diff --git a/README.md b/README.md index 9420640..58333d8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Protected "fingerprinting" APIs: * window (disabled by default) * DOMRect * navigator (disabled by default) + * screen Special thanks to: * spodermenpls for finding all the typos diff --git a/_locales/de/messages.json b/_locales/de/messages.json index bce8160..d7d468a 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -147,6 +147,10 @@ "message": "Navigator-API", "description": "" }, + "section_Screen-api": { + "message": "Screen-API", + "description": "" + }, "displayAdvancedSettings_title": { "message": "Expertenmodus", "description": "" @@ -271,6 +275,18 @@ "message": "Wollen Sie das Auslesen über die Navigator-API erlauben?", "description": "" }, + "askForScreenPermission": { + "message": "Wollen Sie die Screen-API erlauben?", + "description": "" + }, + "askForScreenInputPermission": { + "message": "Wollen Sie das Schreiben über die Screen-API erlauben?", + "description": "" + }, + "askForScreenReadoutPermission": { + "message": "Wollen Sie das Auslesen über die Screen-API erlauben?", + "description": "" + }, "askOnlyOnce_title": { "message": "Nur einmal nachfragen", "description": "" @@ -655,6 +671,10 @@ "message": "Navigator-Auslese vorgetäuscht auf {url}", "description": "" }, + "fakedScreenReadout": { + "message": "Screen-Auslese vorgetäuscht auf {url}", + "description": "" + }, "fakedInput": { "message": "Bei Ausgabe vorgetäuscht auf {url}", "description": "" @@ -1123,6 +1143,34 @@ "message": "Zurücksetzen", "description": "" }, + "protectScreen_title": { + "message": "Screen-API beschützen", + "description": "" + }, + "protectScreen_description": { + "message": "Dies schützt vor Fingerprinting, das die Bildschirmgröße einbezieht.", + "description": "" + }, + "protectScreen_urlSpecific": { + "message": "Um bestimmte Seiten von diesem Schutz auszuschließen, klicken Sie auf den schwarzen Pfeil um das Menü zu öffnen, fügen Sie die gewünschte Domain oder URL mit einem Klick auf \"+\" hinzu und entfernen Sie das zugehörige Häkchen.", + "description": "" + }, + "screenSize_title": { + "message": "Bildschirmgröße", + "description": "" + }, + "screenSize_description": { + "message": "Wenn dies auf einen Wert \"...x...\" gesetzt wird, werden diese Größen als Bildschirmgröße verwendet.", + "description": "" + }, + "fakeMinimalScreenSize_title": { + "message": "Minimale Bildschirmgröße vortäuschen", + "description": "" + }, + "fakeMinimalScreenSize_description": { + "message": "Verwende die minimale Bildschirmgröße aus der folgenden Liste, die zur inneren Browserfenstergröße passt. Bildschirmgrößen: 1366x768, 1440x900, 1600x900, 1920x1080, 4096x2160, 8192x4320", + "description": "" + }, "theme_title": { "message": "Theme", "description": "" @@ -1407,6 +1455,10 @@ "message": "Teilen Sie die persistenten Zufallszahlen nicht zwischen Domains, da dies den Browser 100% eindeutig identifizierbar macht.", "description": "" }, + "sanitation_error.customScreenSize": { + "message": "Verwenden Sie keine benutzerdefinierte Bildschirmgröße, da sie den Browser verfolgbarer macht.", + "description": "" + }, "whitelist_inspection_title": { "message": "CanvasBlocker Erlaubnisse ansehen", "description": "" diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 83d9caf..cb841f3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -154,6 +154,10 @@ "message": "Navigator API", "description": "" }, + "section_Screen-api":{ + "message": "Screen API", + "description": "" + }, "displayAdvancedSettings_title": { "message": "Expert mode", @@ -283,6 +287,18 @@ "message": "Do you want to allow navigator API readout?", "description": "" }, + "askForScreenPermission": { + "message": "Do you want to allow the screen API?", + "description": "" + }, + "askForScreenInputPermission": { + "message": "Do you want to allow screen API input?", + "description": "" + }, + "askForScreenReadoutPermission": { + "message": "Do you want to allow screen API readout?", + "description": "" + }, "askOnlyOnce_title": { "message": "Ask only once", "description": "" @@ -689,6 +705,10 @@ "message": "Faked navigator readout on {url}", "description": "" }, + "fakedScreenReadout": { + "message": "Faked screen readout on {url}", + "description": "" + }, "fakedInput": { "message": "Faked at input on {url}", "description": "" @@ -1172,6 +1192,35 @@ "description": "" }, + "protectScreen_title": { + "message": "Protect screen API", + "description": "" + }, + "protectScreen_description": { + "message": "This protects against fingerprinting attempts including the screen size.", + "description": "" + }, + "protectScreen_urlSpecific": { + "message": "To exclude specific websites from this protection, click on the black arrow to open the menu, add the domain or URL by clicking on \"+\" and remove its checkmark.", + "description": "" + }, + "screenSize_title": { + "message": "Screen size", + "description": "" + }, + "screenSize_description": { + "message": "If this is set with a value \"...x...\" the specified dimensions will be reported as the screen size.", + "description": "" + }, + "fakeMinimalScreenSize_title": { + "message": "Fake minimal screen size", + "description": "" + }, + "fakeMinimalScreenSize_description": { + "message": "Use a minimal screen size from the following set that can fit the inner window dimensions. Screen sizes: 1366x768, 1440x900, 1600x900, 1920x1080, 4096x2160, 8192x4320", + "description": "" + }, + "theme_title": { "message": "Theme", "description": "" @@ -1466,6 +1515,10 @@ "message": "Do not share persistent randomness between domains because this makes the browser 100% trackable.", "description": "" }, + "sanitation_error.customScreenSize": { + "message": "Do not use a custom screen size as it makes the browser more trackable.", + "description": "" + }, "whitelist_inspection_title": { "message": "CanvasBlocker whitelist inspection", diff --git a/lib/modifiedAPI.js b/lib/modifiedAPI.js index 84a0d2f..329c46c 100644 --- a/lib/modifiedAPI.js +++ b/lib/modifiedAPI.js @@ -40,4 +40,5 @@ appendModified(require("./modifiedWindowAPI")); appendModified(require("./modifiedDOMRectAPI")); appendModified(require("./modifiedNavigatorAPI")); + appendModified(require("./modifiedScreenAPI")); }()); \ No newline at end of file diff --git a/lib/modifiedScreenAPI.js b/lib/modifiedScreenAPI.js new file mode 100644 index 0000000..3b0fac2 --- /dev/null +++ b/lib/modifiedScreenAPI.js @@ -0,0 +1,338 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +(function(){ + "use strict"; + + var scope; + if ((typeof exports) !== "undefined"){ + scope = exports; + } + else { + scope = require.register("./modifiedScreenAPI", {}); + } + + const {checkerWrapper} = require("./modifiedAPIFunctions"); + + const physical = { + width: Math.round(window.screen.width * window.devicePixelRatio), + height: Math.round(window.screen.height * window.devicePixelRatio) + }; + if (!window.matchMedia(`(device-width: ${physical.width / window.devicePixelRatio}px`).matches){ + let minWidth = Math.ceil((window.screen.width - 0.5) * window.devicePixelRatio); + let maxWidth = Math.floor((window.screen.width + 0.5) * window.devicePixelRatio); + for (let width = minWidth; width <= maxWidth; width += 1){ + if (window.matchMedia(`(device-width: ${width / window.devicePixelRatio}px`).matches){ + physical.width = width; + break; + } + } + } + if (!window.matchMedia(`(device-height: ${physical.height / window.devicePixelRatio}px`).matches){ + let minHeight = Math.ceil((window.screen.height - 0.5) * window.devicePixelRatio); + let maxHeight = Math.floor((window.screen.height + 0.5) * window.devicePixelRatio); + for (let height = minHeight; height <= maxHeight; height += 1){ + if (window.matchMedia(`(device-height: ${height / window.devicePixelRatio}px`).matches){ + physical.height = height; + break; + } + } + } + + const resolutions = { + portrait: [ + {height: 1366, width: 768}, + {height: 1440, width: 900}, + {height: 1600, width: 900}, + {height: 1920, width: 1080}, + {height: 4096, width: 2160}, + {height: 8192, width: 4320}, + ], + landscape: [ + {width: 1366, height: 768}, + {width: 1440, height: 900}, + {width: 1600, height: 900}, + {width: 1920, height: 1080}, + {width: 4096, height: 2160}, + {width: 8192, height: 4320}, + ] + }; + + function getScreenDimensions(prefs, window){ + const screenSize = prefs("screenSize", window.location); + if (screenSize.match(/\s*\d+\s*x\s*\d+\s*$/)){ + const [width, height] = screenSize.split("x").map(function(value){ + return Math.round(parseFloat(value.trim())); + }); + return { + width: width / window.devicePixelRatio, + height: height / window.devicePixelRatio + }; + } + if (!prefs("fakeMinimalScreenSize", window.location)){ + return window.screen; + } + const isLandscape = window.screen.width > window.screen.height; + // subtract 0.5 to adjust for potential rounding errors + const innerWidth = (window.innerWidth - 0.5) * window.devicePixelRatio; + const innerHeight = (window.innerHeight - 0.5) * window.devicePixelRatio; + for (let resolution of resolutions[isLandscape? "landscape": "portrait"]){ + if (resolution.width >= innerWidth && resolution.height >= innerHeight){ + return { + width: resolution.width / window.devicePixelRatio, + height: resolution.height / window.devicePixelRatio + }; + } + } + return window.screen; + } + + scope.changedGetters = [ + { + objectGetters: [function(window){return window.Screen && window.Screen.prototype;}], + name: "width", + getterGenerator: function(checker){ + const temp = { + get width(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + const returnValue = Math.round(getScreenDimensions(prefs, window).width); + if (originalValue !== returnValue){ + notify("fakedScreenReadout"); + } + return returnValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "width").get; + } + }, + { + objectGetters: [function(window){return window.Screen && window.Screen.prototype;}], + name: "height", + getterGenerator: function(checker){ + const temp = { + get height(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + const returnValue = Math.round(getScreenDimensions(prefs, window).height); + if (originalValue !== returnValue){ + notify("fakedScreenReadout"); + } + return returnValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "height").get; + } + }, + { + objectGetters: [function(window){return window.Screen && window.Screen.prototype;}], + name: "availWidth", + getterGenerator: function(checker){ + const temp = { + get availWidth(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + const returnValue = Math.round(getScreenDimensions(prefs, window).width); + if (originalValue !== returnValue){ + notify("fakedScreenReadout"); + } + return returnValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "availWidth").get; + } + }, + { + objectGetters: [function(window){return window.Screen && window.Screen.prototype;}], + name: "availHeight", + getterGenerator: function(checker){ + const temp = { + get availHeight(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + const returnValue = Math.round(getScreenDimensions(prefs, window).height); + if (originalValue !== returnValue){ + notify("fakedScreenReadout"); + } + return returnValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "availHeight").get; + } + }, + { + objectGetters: [function(window){return window.Screen && window.Screen.prototype;}], + name: "availLeft", + getterGenerator: function(checker){ + const temp = { + get availLeft(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + if (originalValue !== 0){ + notify("fakedScreenReadout"); + } + return 0; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "availLeft").get; + } + }, + { + objectGetters: [function(window){return window.Screen && window.Screen.prototype;}], + name: "availTop", + getterGenerator: function(checker){ + const temp = { + get availTop(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + if (originalValue !== 0){ + notify("fakedScreenReadout"); + } + return 0; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "availTop").get; + } + }, + { + objectGetters: [function(window){return window;}], + name: "outerWidth", + getterGenerator: function(checker){ + const temp = { + get outerWidth(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + const returnValue = window.innerWidth; + if (originalValue !== returnValue){ + notify("fakedScreenReadout"); + } + return returnValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "outerWidth").get; + } + }, + { + objectGetters: [function(window){return window;}], + name: "outerHeight", + getterGenerator: function(checker){ + const temp = { + get outerHeight(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + const returnValue = window.innerHeight; + if (originalValue !== returnValue){ + notify("fakedScreenReadout"); + } + return returnValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "outerHeight").get; + } + }, + { + objectGetters: [function(window){return window.MediaQueryList && window.MediaQueryList.prototype;}], + name: "matches", + getterGenerator: function(checker){ + const temp = { + get matches(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + const screenSize = prefs("screenSize", window.location); + if ( + ( + screenSize.match(/\s*\d+\s*x\s*\d+\s*$/) || + prefs("fakeMinimalScreenSize", window.location) + ) && + this.media.match(/device-(width|height)/) + ){ + const dimensions = getScreenDimensions(prefs, window); + const originalMedia = this.media; + const alteredMedia = this.media.replace( + /\(\s*(?:(min|max)-)?device-(width|height):\s+(\d+\.?\d*)px\s*\)/, + function(m, type, dimension, value){ + value = parseFloat(value); + let newCompareValue = value; + switch (type){ + case "min": + if (value <= dimensions[dimension]){ + newCompareValue = 0; + } + else { + newCompareValue = 2 * physical[dimension]; + } + break; + case "max": + if (value >= dimensions[dimension]){ + newCompareValue = 2 * physical[dimension]; + } + else { + newCompareValue = 0; + } + break; + default: + if ( + Math.round(value * 100) === + Math.round(dimensions[dimension] * 100) + ){ + newCompareValue = physical[dimension]; + } + else { + newCompareValue = 0; + } + } + return "(" + (type? type + "-": "") + + "device-" + dimension + ": " + + ( + newCompareValue / + window.devicePixelRatio + ) + "px)"; + } + ); + if (alteredMedia !== originalMedia){ + const alteredQuery = window.matchMedia(alteredMedia); + const fakedValue = original.call(alteredQuery); + if (originalValue !== fakedValue){ + notify("fakedScreenReadout"); + } + return fakedValue; + } + } + return originalValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "matches").get; + } + }, + ]; + + function getStatus(obj, status, prefs){ + status = Object.create(status); + status.active = prefs("protectScreen", status.url); + return status; + } + + scope.changedGetters.forEach(function(changedGetter){ + changedGetter.type = "readout"; + changedGetter.getStatus = getStatus; + changedGetter.api = "screen"; + }); +}()); \ No newline at end of file diff --git a/lib/settingDefinitions.js b/lib/settingDefinitions.js index cea546c..2637de6 100644 --- a/lib/settingDefinitions.js +++ b/lib/settingDefinitions.js @@ -130,6 +130,16 @@ "userAgent @ navigator", "vendor @ navigator", "vendorSub @ navigator", + {name: "Screen-API", level: 1}, + "width @ screen", + "height @ screen", + "availWidth @ screen", + "availHeight @ screen", + "availTop @ screen", + "availLeft @ screen", + "matches @ screen", + "outerWidth @ screen", + "outerHeight @ screen", ], defaultKeyValue: true }, @@ -334,6 +344,21 @@ name: "navigatorDetails", defaultValue: {}, }, + { + name: "protectScreen", + defaultValue: true, + urlSpecific: true + }, + { + name: "screenSize", + defaultValue: "", + urlSpecific: true + }, + { + name: "fakeMinimalScreenSize", + defaultValue: true, + urlSpecific: true + }, { name: "displayAdvancedSettings", defaultValue: false diff --git a/manifest.json b/manifest.json index 607d1f0..c7cffdf 100644 --- a/manifest.json +++ b/manifest.json @@ -51,6 +51,7 @@ "lib/modifiedDOMRectAPI.js", "lib/navigator.js", "lib/modifiedNavigatorAPI.js", + "lib/modifiedScreenAPI.js", "lib/modifiedAPI.js", "lib/randomSupplies.js", "lib/intercept.js", diff --git a/options/presets.json b/options/presets.json index db640c5..1fcc041 100644 --- a/options/presets.json +++ b/options/presets.json @@ -11,7 +11,8 @@ "rng": "persistent", "ignoreFrequentColors": 3, "minColors": 3, - "storePersistentRnd": true + "storePersistentRnd": true, + "fakeMinimalScreenSize": false }, "max_protection": { "minFakeSize": 0, diff --git a/options/sanitationRules.js b/options/sanitationRules.js index 3d0b275..c05da90 100644 --- a/options/sanitationRules.js +++ b/options/sanitationRules.js @@ -81,6 +81,7 @@ {mainFlag: "protectWindow", section: "Window-API"}, {mainFlag: "protectDOMRect", section: "DOMRect-API"}, {mainFlag: "protectNavigator", section: "Navigator-API"}, + {mainFlag: "protectScreen", section: "Screen-API"}, ].forEach(function(api){ if (settings.get(api.mainFlag) !== (api.mainFlagDisabledValue || false)){ let inSection = false; @@ -297,6 +298,19 @@ }] }); } + if (settings.protectScreen && settings.screenSize){ + errorCallback({ + message: extension.getTranslation("sanitation_error.customScreenSize"), + severity: "medium", + resolutions: [{ + label: extension.getTranslation("sanitation_resolution.setTo") + .replace(/{value}/g, "\"\""), + callback: function(){ + settings.screenSize = ""; + } + }] + }); + } } }, ]; diff --git a/options/settingsDisplay.js b/options/settingsDisplay.js index ac68735..8b47c41 100644 --- a/options/settingsDisplay.js +++ b/options/settingsDisplay.js @@ -632,6 +632,44 @@ }, ] }, + { + name: "Screen-API", + settings: [ + { + "name": "protectScreen" + }, + { + "name": "protectedAPIFeatures", + "replaceKeyPattern": / @ .+$/, + "displayedSection": "Screen-API", + "displayDependencies": [ + { + "protectScreen": [true], + "displayAdvancedSettings": [true] + } + ] + }, + { + "name": "screenSize", + "displayDependencies": [ + { + "protectScreen": [true], + "fakeMinimalScreenSize": [false], + "displayAdvancedSettings": [true] + } + ] + }, + { + "name": "fakeMinimalScreenSize", + "displayDependencies": [ + { + "protectScreen": [true], + "screenSize": [""] + } + ] + }, + ] + }, ] }, { diff --git a/releaseNotes.txt b/releaseNotes.txt index 13414ff..a75bc42 100644 --- a/releaseNotes.txt +++ b/releaseNotes.txt @@ -3,7 +3,7 @@ Version 0.5.15: - improved storage of protected API features new features: - - + - added screen protection fixes: - background color of the textarea in the settings export was not readable in the dark theme when the value was invalid diff --git a/versions/updates.json b/versions/updates.json index 4542e66..2d65f28 100644 --- a/versions/updates.json +++ b/versions/updates.json @@ -77,6 +77,10 @@ { "version": "0.5.15Alpha20190924", "update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-0.5.15Alpha20190924-an+fx.xpi" + }, + { + "version": "0.5.15Alpha20191111", + "update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-0.5.15Alpha20191111-an+fx.xpi" } ] }