From 44361238215c5ddc66564f38e73a6e06f9ceef49 Mon Sep 17 00:00:00 2001 From: ihranicky Date: Feb 14 2022 14:52:05 +0000 Subject: Merge devel0.7 branch This functionality is planned for 0.7 release. --- diff --git a/.gitignore b/.gitignore index f00d902..6461ab5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,7 @@ !.gitignore !.reuse *.zip -chrome_JSR/ -firefox_JSR/ +build/ ipv4.csv common/ipv4.dat ipv6.csv diff --git a/.reuse/dep5 b/.reuse/dep5 index 8d6fd6d..5f75fb5 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,7 +1,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: jsrestrictor +Upstream-Name: JShelter Upstream-Contact: Libor Polcak -Source: https://github.com/polcak/jsrestrictor +Source: https://pagure.io/JShelter/webextension # Files that we think are not copyrightable Files: .gitignore .gitmodules tests/common_files/webbrowser_drivers/DOWNLOAD WEB DRIVERS HERE.md tests/unit_tests/package-lock.json tests/unit_tests/package.json diff --git a/Makefile b/Makefile index d3995ca..c8c20c2 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,8 @@ DEBUG=0 all: firefox chrome .PHONY: firefox chrome clean get_csv docs -firefox: firefox_JSR.zip -chrome: chrome_JSR.zip +firefox: jshelter_firefox.zip +chrome: jshelter_chrome.zip COMMON_FILES = $(shell find common/) \ LICENSES/ \ @@ -34,33 +34,36 @@ submodules: git submodule init git submodule update -%_JSR.zip: $(COMMON_FILES) get_csv submodules - @rm -rf $*_JSR/ $@ - @cp -r common/ $*_JSR/ - @cp -r $*/* $*_JSR/ - @cp -r LICENSES $*_JSR/ - @./fix_manifest.sh $*_JSR/manifest.json - @cp common/wrappingX* $*_JSR/ - @nscl/include.sh $*_JSR +jshelter_%.zip: $(COMMON_FILES) get_csv submodules + @mkdir -p build/ + @rm -rf build/$*/ $@ + @cp -r common/ build/$*/ + @cp -r $*/* build/$*/ + @cp -r LICENSES build/$*/ + @./fix_manifest.sh build/$*/manifest.json + @cp common/wrappingX* build/$*/ + @nscl/include.sh build/$* @if [ $(DEBUG) -eq 0 ]; \ then \ - find $*_JSR/ -type f -name "*.js" -exec sed -i '/console\.debug/d' {} + ; \ + find build/$*/ -type f -name "*.js" -exec sed -i '/console\.debug/d' {} + ; \ fi - @rm -f $*_JSR/.*.sw[pno] - @rm -f $*_JSR/img/makeicons.sh - @find $*_JSR/ -name '*.license' -delete - @cd $*_JSR/ && zip -q -r ../$@ ./* --exclude \*.sw[pno] - @echo "LOG-WARNING: Number of lines in $*_JSR with console.log:" - @grep -re 'console.log' $*_JSR | wc -l + @rm -f build/$*/.*.sw[pno] + @rm -f build/$*/img/makeicons.sh + @find build/$*/ -name '*.license' -delete + @cd build/$*/ && zip -q -r ../../$@ ./* --exclude \*.sw[pno] + @echo "LOG-WARNING: Number of lines in build/$* with console.log:" + @grep -re 'console.log' build/$* | wc -l debug: DEBUG=1 debug: all - + +doxygen: + PROJECT_NAME="${PROJECT_NAME}" doxygen < doxyfile + clean: - rm -rf firefox_JSR.zip - rm -rf firefox_JSR - rm -rf chrome_JSR.zip - rm -rf chrome_JSR + rm -rf build/ + rm -rf jsheleter_firefox.zip + rm -rf jshelter_chrome.zip rm -rf common/ipv4.dat rm -rf common/ipv6.dat rm -rf common/wrappingX* diff --git a/README.md b/README.md deleted file mode 120000 index e892330..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -docs/index.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0de383b --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +> **Disclaimer**: This is a research project under development, see the [issue page](https://pagure.io/JShelter/webextension/issues) and the [webextension home page](https://JShelter.org/) for more details about the current status. + +## What is JShelter? + +JShelter is a browser extension to give back control over what your browser is doing. A JavaScript-enabled web page can access much of the browser's functionality, with little control over this process available to the user: malicious websites can uniquely identify you through fingerprinting and use other tactics for tracking your activity. JShelter aims to improve the privacy and security of your web browsing. + +## How does it work? + +Like a firewall that controls network connections, JShelter controls the APIs provided by the browser, restricting the data that they gather and send out to websites. JShelter adds a safety layer that allows the user to choose if a certain action should be forbidden on a site, or if it should be allowed with restrictions, such as reducing the precision of geolocation to the city area. This layer can also aid as a countermeasure against attacks targeting the browser, operating system or hardware. + + +## How can I get started? + +JavaScript Restrictor (JSR) is a browser extension with support for multiple browsers: [Firefox](https://addons.mozilla.org/firefox/addon/javascript-restrictor/), [Google Chrome](https://chrome.google.com/webstore/detail/javascript-restrictor/ammoloihpcbognfddfjcljgembpibcmb), and [Opera](https://addons.opera.com/extensions/details/javascript-restrictor/). The extension also works with Brave, Microsoft Edge, and most likely any Chromium-based browser. [Let us know](https://pagure.io/JShelter/webextension/issues) if you want to add the extension to additional stores. + +See our [website](https://JShelter.org/) for additional information and documentation. + +## Contributing + +If you have any questions or you have spotted a bug, please [let us know](https://pagure.io/JShelter/webextension/issues). + +If you would like to give us [feedback](https://pagure.io/JShelter/webextension/issues), we would really appreciate it. + +## License Information + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either [version +3](https://www.gnu.org/licenses/gpl-3.0) of the License, or (at your option) any later version. diff --git a/chrome/manifest.json b/chrome/manifest.json index 2b34f21..943f275 100644 --- a/chrome/manifest.json +++ b/chrome/manifest.json @@ -36,7 +36,7 @@ "256": "img/icon-256.png", "512": "img/icon-512.png" }, - "default_title": "JavaScript Restrictor", + "default_title": "JShelter", "default_popup": "popup.html" }, "content_scripts": [ @@ -59,7 +59,7 @@ } ], "description": "Extension for increasing security and privacy level of the user.", - "homepage_url": "https://polcak.github.io/jsrestrictor/", + "homepage_url": "https://JShelter.org", "icons": { "16": "img/icon-16.png", "32": "img/icon-32.png", @@ -71,7 +71,7 @@ "512": "img/icon-512.png" }, "manifest_version": 2, - "name": "JavaScript Restrictor", + "name": "JShelter", "options_page": "options.html", "permissions": [ "storage", @@ -82,6 +82,6 @@ "notifications", "browsingData" ], - "short_name": "JSR", + "short_name": "JShelter", "version": "0.6.4" } diff --git a/common/background.js b/common/background.js index dfb52e3..531e857 100644 --- a/common/background.js +++ b/common/background.js @@ -29,7 +29,7 @@ function updateBadge(level) { browser.browserAction.setBadgeText({text: "" + level["level_id"]}); } -// get active tab and pass it +// get active tab and pass it var queryInfo = { active: true, currentWindow: true @@ -47,8 +47,12 @@ function tabUpdate(tabid, changeInfo) { updateBadge(current_level); } // get level for activated tab -function tabActivate(activeInfo) { - current_level = tab_levels[activeInfo.tabId] || {level_id: "?"}; +async function tabActivate(activeInfo) { + let {tabId} = activeInfo; + if (!(tabId in tab_levels)) { + tabUpdate(tabId, await browser.tabs.get(tabId)); + } + current_level = tab_levels[tabId] || {level_id: "?"}; updateBadge(current_level); } // on tab reload or tab change, update badge @@ -64,9 +68,11 @@ browser.tabs.onActivated.addListener(tabActivate); // change tab * browser.runtime.getBackgroundPage() does not work as expected. See * also https://bugzilla.mozilla.org/show_bug.cgi?id=1329304. */ -function connected(port) { +async function connected(port) { if (port.name === "port_from_popup") { /// We always send back current level + let [tab] = await browser.tabs.query(queryInfo); + tabUpdate(tab.id, tab.url); port.postMessage(current_level); port.onMessage.addListener(function(msg) { port.postMessage(current_level); diff --git a/common/document_start.js b/common/document_start.js index be376e9..b03081d 100644 --- a/common/document_start.js +++ b/common/document_start.js @@ -24,10 +24,10 @@ // var wrappersPort; -var injectionConfigured = false; -function configureInjection({code, wrappers, domainHash, sessionHash, fpdOn}) { - if (injectionConfigured) return; // one shot - injectionConfigured = true; +var pageConfiguration = null; +function configureInjection({currentLevel, code, wrappers, domainHash, sessionHash, fpdOn}) { + if (pageConfiguration) return; // one shot + pageConfiguration = {currentLevel, fpdOn}; if (!code) return true; // nothing to wrap, bail out! if (!fpdOn) { code = code.replace(/\/\/ FPD_S[\s\S]*?\/\/ FPD_E/, ''); // remove fpd wrappers from injected code diff --git a/common/fp_config/groups-lvl_default.json b/common/fp_config/groups-lvl_default.json index 79a8d4c..d86d7b5 100644 --- a/common/fp_config/groups-lvl_default.json +++ b/common/fp_config/groups-lvl_default.json @@ -1,5 +1,6 @@ { "name":"FingerprintingActivity", + "description":"Definition of fingerprinting behavior by FPD module.", "criteria":[ { "value":3, @@ -9,6 +10,7 @@ "groups":[ { "name":"BrowserProperties", + "description":"Fingerprinting methods based on simple information gathering by accessing certain APIs.", "criteria":[ { "value":7, @@ -60,7 +62,7 @@ }, { "name":"NavigatorNetwork", - "description":"Information about connection to internet.", + "description":"Information about internet connection.", "criteria":[ { "value":3, @@ -162,6 +164,7 @@ }, { "name":"AlgorithmicMethods", + "description":"Fingerprinting methods based on specific procedures, calculations or processing.", "criteria":[ { "value":1, @@ -336,7 +339,7 @@ }, { "name":"WebRTCFingerprint", - "description":"Extraction of rendered image into fingerprint.", + "description":"Leakage of public or local IP address.", "criteria":[ { "percentage":100, @@ -378,6 +381,7 @@ }, { "name":"CrawlFpInspector", + "description":"APIs often abused for fingerprinting according to FP-Inspector study.", "criteria":[ { "percentage":10, diff --git a/common/fp_detect_background.js b/common/fp_detect_background.js index 23155c1..368301f 100644 --- a/common/fp_detect_background.js +++ b/common/fp_detect_background.js @@ -27,13 +27,13 @@ * its sharing. To learn more about Browser Fingerprinting topic, see study "Browser Fingerprinting: A survey" available * here: https://arxiv.org/pdf/1905.01051.pdf * - * The FPD module uses JSR wrapping technique to inject logic that allows log API calls and accesses for every visited web page - * and its frames. Logged JS APIs can be specified in wrappers-lvl_X.json file, where X represents corresponding JSR level. + * The FPD module uses wrapping technique to inject logic that allows log API calls and accesses for every visited web page + * and its frames. Logged JS APIs can be specified in wrappers-lvl_X.json file, where X represents corresponding JShelter level. * * Detector of fingeprinting activity is based on chosen heuristics that can be defined in form of API groups. Groups represents * a set of APIs that have similar but specific purpose. Access to group is triggered when a certain amount APIs is accessed. * Hierarchy of groups creates a tree structure, where access to root group means fingerprinting activity. Groups can be configured in - * groups-lvl_X.json file, where X represents corresponding JSR level. + * groups-lvl_X.json file, where X represents corresponding JShelter level. * * The FPD evaluate API groups with every request made in scope of certain browser tab. When FPD detects fingerprinting activity, * blocking of subsequent requests is issued. Local browsing data of fingerprinting origin are cleared to prevent caching extracted @@ -629,6 +629,21 @@ browser.runtime.onMessage.addListener(function (record, sender) { fpdWhitelist = result.fpdWhitelist; }); break; + case "fpd-fetch-hits": { + let {tabId} = record; + // filter by tabId; + let hits = Object.create(null); + for ([resource, tabRecords] of Object.entries(fpDb)) { + let total = 0; + if (tabRecords[tabId]) { + for (let stat of Object.values(tabRecords[tabId])) { // by type + total += stat.total; + } + } + hits[resource] = total; + } + return Promise.resolve(hits); + } } } }); @@ -704,6 +719,9 @@ function refreshDb(tabId) { if (fpDb[resource].hasOwnProperty(tabId)) { delete fpDb[resource][tabId]; } + if (Object.keys(fpDb[resource]).length == 0) { + delete fpDb[resource]; + } } if (latestEvals[tabId]) { delete latestEvals[tabId]; diff --git a/common/helpers.js b/common/helpers.js index d1cdcc2..34d7fd3 100644 --- a/common/helpers.js +++ b/common/helpers.js @@ -118,3 +118,13 @@ function strToUint(str, length){ } return "0b"+ret; }; + +/** + * \brief Asynchronously sleep for given number of milliseconds + * \param ms Number of milliseconds to sleep + */ +async function async_sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/common/level_cache.js b/common/level_cache.js index bcb6194..ecdff38 100644 --- a/common/level_cache.js +++ b/common/level_cache.js @@ -61,11 +61,11 @@ function getContentConfiguration(url, frameId, tabId) { */ level = getCurrentLevelJSON(TabCache.get(tabId).url); } - let [{is_default, wrappers}, code] = level; + let [{wrappers}, code] = level; let {domainHash} = Hashes.getFor(url); let fpdOn = isFpdOn(tabId); resolve({ - is_default, + currentLevel: level[0], code, wrappers, domainHash, diff --git a/common/levels.js b/common/levels.js index fd81719..a6f35ce 100644 --- a/common/levels.js +++ b/common/levels.js @@ -25,7 +25,7 @@ /** * Wrapping groups * - * Used to control the built-in levels and options GUI. + * Used to control the built-in levels and GUI (e.g. level tweaks). */ var wrapping_groups = { empty_level: { /// Automatically populated @@ -33,17 +33,13 @@ var wrapping_groups = { level_id: "", level_description: "", }, - option_map: {}, ///Automatically populated - associated_params: {}, ///Automatically populated + group_map: {}, ///Automatically populated + group_names: [], ///Automatically populated get_wrappers: function(level) { wrappers = []; for (group of wrapping_groups.groups) { - if (level[group.id] === true) { - let arg_names = wrapping_groups.associated_params[group.id]; - let arg_values = arg_names.reduce(function(prev, name) { - prev.push(level[name]); - return prev; - }, []); + if ((level[group.id] !== undefined) && level[group.id] !== 0) { + let arg_values = group.params[level[group.id] - 1].config; group.wrappers.forEach((w) => wrappers.push([w, ...arg_values])); } } @@ -52,36 +48,24 @@ var wrapping_groups = { groups: [ { name: "time_precision", - description: "Limit the precision of high resolution time stamps (Date, Performance, events, Gamepad API, Web VR API)", - description2: ["If you enable Geolocation API wrapping below, timestamps provided by the Geolocation API will be wrapped as well"], - options: [ - { - description: "Manipulate time to", - ui_elem: "select", - name: "precision", - default: 1, - data_type: "Number", - options: [ - { - value: 2, - description: "Hundredths of a second (1.230)", - }, - { - value: 1, - description: "Tenths of a second (1.200)", - }, - { - value: 0, - description: "Full seconds (1.000)", - }, - ], - }, - { - ui_elem: "input-checkbox", - name: "randomize", - description: "Apply additional randomization after rounding (note that the random noise is influenced by the selected precision and consequently is more effective with lower time precision)", - data_type: "Boolean", - default: false, + label: "Time precision", + description: "Prevent attacks and fingerprinting techniques relying on precise time measurement (or make them harder).", + description2: ["Limit the precision of high resolution time stamps (Date, Performance, events, Gamepad API, Web VR API). Timestamps provided by the Geolocation API are wrapped as well if you enable Geolocation API wrapping"], + params: [ + { + short: "Poor", + description: "Round time to hundredths of a second (1.230)", + config: [2, false], + }, + { + short: "Low", + description: "Round time to tenths of a second (1.200)", + config: [1, false], + }, + { + short: "High", + description: "Randomize decimal digits with noise (1.451)", + config: [0, true], }, ], wrappers: [ @@ -103,30 +87,24 @@ var wrapping_groups = { }, { name: "htmlcanvaselement", - description: "Protect against canvas fingerprinting", + label: "Localy rendered images", + description: "Protect against canvas fingerprinting.", description2: [ "Functions canvas.toDataURL(), canvas.toBlob(), CanvasRenderingContext2D.getImageData(), OffscreenCanvas.convertToBlob() return modified image data to prevent fingerprinting", "CanvasRenderingContext2D.isPointInStroke() and CanvasRenderingContext2D.isPointInPath() are modified to lie with probability" ], - options: [ - { - description: "farbling type", - ui_elem: "select", - name: "method", - default: 0, - data_type: "Number", - options: [ - { - value: 0, - description: "Alter image data based on domain and session hashes", - }, - { - value: 1, - description: "Replace by white image", - } - ], - } - ], + params: [ + { + short: "White lie", + description: "Alter image data based on domain hash", + config: [0], + }, + { + short: "Strict", + description: "Replace by white image", + config: [1], + }, + ], wrappers: [ // H-C "CanvasRenderingContext2D.prototype.getImageData", @@ -139,28 +117,22 @@ var wrapping_groups = { }, { name: "audiobuffer", - description: "Protect against audio fingerprinting", + label: "Locally generated audio and audio card information", + description: "Protect against audio fingerprinting, spoof details of your audio card.", description2: [ "Functions AudioBuffer.getChannelData(), AudioBuffer.copyFromChannel(), AnalyserNode.getByteTimeDomainData(), AnalyserNode.getFloatTimeDomainData(), AnalyserNode.getByteFrequencyData() and AnalyserNode.getFloatFrequencyData() are modified to alter audio data based on domain key" ], - options: [ - { - description: "farbling type", - ui_elem: "select", - name: "method", - default: 0, - data_type: "Number", - options: [ - { - value: 0, - description: "Add amplitude noise based on domain hash", - }, - { - value: 1, - description: "Replace by white noise based on domain hash", - } - ], - } + params: [ + { + short: "White lie", + description: "Add amplitude noise based on domain hash", + config: [0], + }, + { + short: "Strict", + description: "Replace by white noise based on domain hash", + config: [1], + }, ], wrappers: [ // AUDIO @@ -174,29 +146,25 @@ var wrapping_groups = { }, { name: "webgl", - description: "Protect against WEBGL fingerprinting", + label: "Localy rendered images and graphic card information", + description: "Protect against WEBGL fingerprinting, spoof details of your graphic card.", description2: [ "Function WebGLRenderingContext.getParameter() returns modified/bottom values for certain parameters", "WebGLRenderingContext functions .getFramebufferAttachmentParameter(), .getActiveAttrib(), .getActiveUniform(), .getAttribLocation(), .getBufferParameter(), .getProgramParameter(), .getRenderbufferParameter(), .getShaderParameter(), .getShaderPrecisionFormat(), .getTexParameter(), .getUniformLocation(), .getVertexAttribOffset(), .getSupportedExtensions() and .getExtension() return modified values", "Function WebGLRenderingContext.readPixels() returns modified image data to prevent fingerprinting" ], - options: [{ - description: "farbling type", - ui_elem: "select", - name: "method", - default: 0, - data_type: "Number", - options: [ - { - value: 0, - description: "Generate random numbers/strings based on domain hash, modified canvas", - }, - { - value: 1, - description: "Return bottom values (null, empty string), empty canvas", - } - ], - }], + params: [ + { + short: "White lie", + description: "Generate random numbers/strings based on domain hash, modified canvas", + config: [0], + }, + { + short: "Strict", + description: "Return bottom values (null, empty string), empty canvas", + config: [1], + }, + ], wrappers: [ // WEBGL "WebGLRenderingContext.prototype.getParameter", @@ -235,29 +203,26 @@ var wrapping_groups = { }, { name: "plugins", + label: "Installed browser plugins", description: "Protect against plugin fingerprinting", description2: [], - options: [{ - description: "farbling type", - ui_elem: "select", - name: "method", - default: 0, - data_type: "Number", - options: [ - { - value: 0, - description: "Edit current and add two fake plugins", - }, - { - value: 1, - description: "Return two fake plugins", - }, - { - value: 2, - description: "Return empty" - } - ], - }], + params: [ + { + short: "White lie", + description: "Edit current and add two fake plugins", + config: [0], + }, + { + short: "Fake", + description: "Return two fake plugins", + config: [1], + }, + { + short: "Empty", + description: "Return empty", + config: [2], + }, + ], wrappers: [ // NP "Navigator.prototype.plugins", // also modifies "Navigator.prototype.mimeTypes", @@ -265,31 +230,28 @@ var wrapping_groups = { }, { name: "enumerateDevices", + label: "Connected cameras and microphones", description: "Prevent fingerprinting based on the multimedia devices connected to the computer", description2: [ "Function MediaDevices.enumerateDevices() is modified to return empty or modified result" ], - options: [{ - description: "farbling type", - ui_elem: "select", - name: "method", - default: 0, - data_type: "Number", - options: [ - { - value: 0, - description: "Randomize order", - }, - { - value: 1, - description: "Add 0-4 fake devices and randomize order", - }, - { - value: 2, - description: "Return empty promise" - } - ], - }], + params: [ + { + short: "White lie", + description: "Randomize order", + config: [0], + }, + { + short: "Add fake", + description: "Add 0-4 fake devices and randomize order", + config: [1], + }, + { + short: "Empty", + description: "Return empty", + config: [2], + }, + ], wrappers: [ // MCS "MediaDevices.prototype.enumerateDevices", @@ -297,31 +259,28 @@ var wrapping_groups = { }, { name: "hardware", - description: "Spoof hardware information to the most popular HW", + label: "Device memory and CPU", + description: "Spoof hardware information on the amount of RAM and CPU count.", description2: [ "Getters navigator.deviceMemory and navigator.hardwareConcurrency return modified values", ], - options: [{ - description: "farbling type", - ui_elem: "select", - name: "method", - default: 0, - data_type: "Number", - options: [ - { - value: 0, - description: "Return random valid value between minimum and real value", - }, - { - value: 1, - description: "Return random valid value between minimum and 8.0", - }, - { - value: 2, - description: "Return 4 for navigator.deviceMemory and 2 for navigator.hardwareConcurrency" - } - ], - }], + params: [ + { + short: "Low", + description: "Return random valid value between minimum and real value", + config: [0], + }, + { + short: "Medium", + description: "Return random valid value between minimum and 8", + config: [1], + }, + { + short: "High", + description: "Return 4 for navigator.deviceMemory and 2 for navigator.hardwareConcurrency", + config: [2], + }, + ], wrappers: [ // HTML-LS "Navigator.prototype.hardwareConcurrency", @@ -331,25 +290,19 @@ var wrapping_groups = { }, { name: "xhr", - description: "Filter XMLHttpRequest requests", - description2: [], - options: [ - { - ui_elem: "input-radio", - name: "behaviour", - data_type: "Boolean", - options: [ - { - value: "block", - description: "Block all XMLHttpRequest.", - default: false, - }, - { - value: "ask", - description: "Ask before executing an XHR request.", - default: true, - }, - ], + label: "XMLHttpRequest requests (XHR)", + description: "Filter reliable XHR requests to server.", + description2: ["Note that these requests are broadly employed for benign purposes and also note that Fetch, SSE, WebRTC, and WebSockets APIs are not blocked. All provide similar and some even better means of communication with server. For practical usage, we recommend activating Fingerprint Detector instead of XHR wrappers. JShelter keeps the wrapper as it is useful for some users mainly for experimental reasons."], + params: [ + { + short: "Ask", + description: "Ask before executing an XHR request", + config: [false, true], + }, + { + short: "Block", + description: "Block all XMLHttpRequests", + config: [true, false], }, ], wrappers: [ @@ -360,15 +313,19 @@ var wrapping_groups = { }, { name: "arrays", - description: "Protect against ArrayBuffer exploitation", + label: "ArrayBuffer", + description: "Protect against ArrayBuffer exploitation, for example, to prevent side channel attacks on memory layout (or make them harder).", description2: [], - options: [ + params: [ + { + short: "Shift", + description: "Shift indexes to make memory page boundaries detection harder", + config: [false], + }, { - ui_elem: "input-checkbox", - name: "mapping", - description: "Use random mapping of array indexing to memory.", - data_type: "Boolean", - default: false, + short: "Randomize", + description: "Use random mapping of array indexing to memory", + config: [true], }, ], wrappers: [ @@ -386,25 +343,19 @@ var wrapping_groups = { }, { name: "shared_array", - description: "Protect against SharedArrayBuffer exploitation:", + label: "SharedArrayBuffer", + description: "Protect against SharedArrayBuffer exploitation, for example, to prevent side channel attacks on memory layout (or make them harder).", description2: [], - options: [ - { - ui_elem: "input-radio", - name: "approach", - data_type: "Boolean", - options: [ - { - value: "block", - description: "Block SharedArrayBuffer.", - default: true, - }, - { - value: "polyfill", - description: "Randomly slow messages to prevent high resolution timers.", - default: false, - }, - ], + params: [ + { + short: "Medium", + description: "Randomly slow messages to prevent high resolution timers", + config: [false], + }, + { + short: "Strict", + description: "Block SharedArrayBuffer", + config: [true], }, ], wrappers: [ @@ -414,25 +365,19 @@ var wrapping_groups = { }, { name: "webworker", - description: "Protect against WebWorker exploitation", + label: "WebWorker", + description: "Protect against WebWorker exploitation, for example, to provide high resolution timers", description2: [], - options: [ - { - ui_elem: "input-radio", - name: "approach", - data_type: "Boolean", - options: [ - { - value: "polyfill", - description: "Remove real parallelism, use WebWorker polyfill.", - default: true, - }, - { - value: "slow", - description: "Randomly slow messages to prevent high resolution timers.", - default: false, - }, - ], + params: [ + { + short: "Medium", + description: "Randomly slow messages to prevent high resolution timers", + config: [false], + }, + { + short: "Strict", + description: "Remove real parallelism, use WebWorker polyfill", + config: [true], }, ], wrappers: [ @@ -441,45 +386,39 @@ var wrapping_groups = { }, { name: "geolocation", - description: "Geolocation API wrapping", + label: "Physical location (geolocation)", + description: "Limit the information on real-world position provided by Geolocation API.", description2: [], - options: [ - { - description: "Location obfuscation", - ui_elem: "select", - name: "locationObfuscationType", - default: 0, - data_type: "Number", - options: [ - { - value: 0, - description: "Turn location services off", - }, - //{ - // value: 1, - // description: "Use the position below", - //}, - { - value: 2, - description: "Use accuracy of hundreds of meters", - }, - { - value: 3, - description: "Use accuracy of kilometers", - }, - { - value: 4, - description: "Use accuracy of tens of kilometers", - }, - { - value: 5, - description: "Use accuracy of hundreds of kilometers", - }, - { - value: -1, - description: "Provide accurate data (use when you really need to provide exact location)", - }, - ], + params: [ + { + short: "Poor", + description: "Provide accurate data (use when you really need to provide exact location and you want to protect geolocation timestamps)", + config: [-1], + }, + { + short: "Very low", + description: "Use accuracy of hundreds of meters", + config: [2], + }, + { + short: "Low", + description: "Use accuracy of kilometers", + config: [3], + }, + { + short: "Medium", + description: "Use accuracy of tens of kilometers", + config: [4], + }, + { + short: "High", + description: "Use accuracy of hundreds of kilometers", + config: [5], + }, + { + short: "Strict", + description: "Turn location services off", + config: [0], }, ], wrappers: [ @@ -496,15 +435,14 @@ var wrapping_groups = { }, { name: "physical_environment", - description: "Wrapping APIs for scanning properties of the physical environment", + label: "Physical environement sensors", + description: "Limit the information provided by physical environment sensors like Magnetometer or Accelerometer.", description2: [], - options: [ + params: [ { - name: "emulateStationaryDevice", - description: "Emulate stationary device", - data_type: "Boolean", - ui_elem: "input-checkbox", - default: true, + short: "High", + description: "Emulate stationary device", + config: [true], }, ], wrappers: [ @@ -531,10 +469,16 @@ var wrapping_groups = { }, { name: "gamepads", - description: "Prevent websites from learning information on local gamepads", + label: "Gamepads", + description: "Prevent websites from accessing and learning information on local gamepads.", description2: [], - default: true, - options: [], + params: [ + { + short: "Strict", + description: "Hide all gamepads", + config: [true], + }, + ], wrappers: [ // GAMEPAD "Navigator.prototype.getGamepads", @@ -542,10 +486,16 @@ var wrapping_groups = { }, { name: "vr", - description: "Prevent websites from learning information on local Virtual Reality displays", + label: "Virtual and augmented reality devices", + description: "Prevent websites from accessing and learning information on local virtual and augmented reality displays.", description2: [], - default: true, - options: [], + params: [ + { + short: "Strict", + description: "Hide all devices", + config: [], + }, + ], wrappers: [ // VR "Navigator.prototype.activeVRDisplays", @@ -555,10 +505,16 @@ var wrapping_groups = { }, { name: "analytics", - description: "Prevent sending analytics through Beacon API", - description2: [], - default: true, - options: [], + label: "Unreliable transfers to server (beacons)", + description: "Prevent unreliable transfers to server (beacons).", + description2: ["Such transfers are typically misused for analytics but occassionally may be used by e-shops or other pages.", "Prevent sending information through Beacon API."], + params: [ + { + short: "Disable", + description: "The wrapper performs no action", + config: [], + }, + ], wrappers: [ // BEACON "Navigator.prototype.sendBeacon", @@ -566,10 +522,16 @@ var wrapping_groups = { }, { name: "battery", + label: "Hardware battery", description: "Disable Battery status API", description2: [], - default: true, - options: [], + params: [ + { + short: "Disable", + description: "Disable the API", + config: [], + }, + ], wrappers: [ // BATTERY "Navigator.prototype.getBattery", @@ -578,10 +540,16 @@ var wrapping_groups = { }, { name: "windowname", - description: "Clear window.name value on the webpage loading", - description2: [], - default: true, - options: [], + label: "Persistent identifier of the browser tab", + description: "Clear window.name value on the webpage loading.", + description2: ["This API might be occasionally used for benign purposes.", "This API provides a possibility to detect cross-site browsing in one tab and broser session."], + params: [ + { + short: "Strict", + description: "Clear during page reload", + config: [], + }, + ], wrappers: [ // WINDOW-NAME "window.name", @@ -628,32 +596,11 @@ function are_all_api_unsupported(wrappers) { /// Automatically populate infered metadata in wrapping_groups. wrapping_groups.groups.forEach(function (group) { group.id = group.name; - group.data_type = "Boolean"; - group.ui_elem = "input-checkbox"; - wrapping_groups.empty_level[group.id] = are_all_api_unsupported(group.wrappers) ? true : Boolean(group.default); - wrapping_groups.option_map[group.id] = group - wrapping_groups.associated_params[group.id] = []; - group.options.forEach((function (gid, option) { - option.id = `${gid}_${option.name}`; - if (option.default !== undefined) { - wrapping_groups.empty_level[option.id] = option.default; - wrapping_groups.associated_params[group.id].push(option.id); - } - wrapping_groups.option_map[option.id] = option; - if (option.options !== undefined) { - option.options.forEach((function (oid, choice) { - choice.id = `${oid}_${choice.value}`; - if (choice.default !== undefined) { - wrapping_groups.empty_level[choice.id] = choice.default; - wrapping_groups.associated_params[group.id].push(choice.id); - } - if (choice.ui_elem === undefined && option.ui_elem !== undefined) { - choice.ui_elem = option.ui_elem; - } - wrapping_groups.option_map[choice.id] = choice; - }).bind(null, option.id)); - } - }).bind(null, group.id)); + if (!are_all_api_unsupported(group.wrappers)) { + wrapping_groups.group_names.push(group.name); + wrapping_groups.empty_level[group.id] = 0; + } + wrapping_groups.group_map[group.id] = group }); // ***************************************************************************** @@ -678,18 +625,13 @@ var level_1 = { "level_id": L1, "level_text": "Minimal", "level_description": "Minimal level of protection", - "time_precision": true, - "time_precision_precision": 2, - "time_precision_randomize": false, - "hardware": true, - "hardware_method": 0, - "battery": true, - "geolocation": true, - "geolocation_locationObfuscationType": 2, - "analytics": true, - "windowname": true, - "physical_environment": true, - "physical_environment_emulateStationaryDevice": true, + "time_precision": 1, + "hardware": 1, + "geolocation": 2, + "physical_environment": 1, + "analytics": 1, + "battery": 1, + "windowname": 1, }; var level_2 = { @@ -697,30 +639,20 @@ var level_2 = { "level_id": L2, "level_text": "Recommended", "level_description": "Recommended level of protection for most sites", - "time_precision": true, - "time_precision_precision": 1, - "time_precision_randomize": false, - "hardware": true, - "hardware_method": 0, - "battery": true, - "htmlcanvaselement": true, - "htmlcanvaselement_method": 0, - "audiobuffer": true, - "audiobuffer_method": 0, - "webgl": true, - "webgl_method": 0, - "plugins": true, - "plugins_method": 0, - "enumerateDevices": true, - "enumerateDevices_method": 1, - "geolocation": true, - "geolocation_locationObfuscationType": 3, - "gamepads": true, - "vr": true, - "analytics": true, - "windowname": true, - "physical_environment": true, - "physical_environment_emulateStationaryDevice": true, + "time_precision": 2, + "htmlcanvaselement": 1, + "audiobuffer": 1, + "webgl": 1, + "plugins": 1, + "enumerateDevices": 2, + "hardware": 1, + "geolocation": 3, + "physical_environment": 1, + "gamepads": 1, + "vr": 1, + "analytics": 1, + "battery": 1, + "windowname": 1, }; var level_3 = { @@ -728,41 +660,24 @@ var level_3 = { "level_id": L3, "level_text": "High", "level_description": "High level of protection", - "time_precision": true, - "time_precision_precision": 0, - "time_precision_randomize": true, - "hardware": true, - "hardware_method": 2, - "battery": true, - "htmlcanvaselement": true, - "htmlcanvaselement_method": 1, - "audiobuffer": true, - "audiobuffer_method": 1, - "webgl": true, - "webgl_method": 1, - "plugins": true, - "plugins_method": 2, - "enumerateDevices": true, - "enumerateDevices_method": 2, - "xhr": true, - "xhr_behaviour_block": false, - "xhr_behaviour_ask": true, - "arrays": true, - "arrays_mapping": true, - "shared_array": true, - "shared_array_approach_block": true, - "shared_array_approach_polyfill": false, - "webworker": true, - "webworker_approach_polyfill": true, - "webworker_approach_slow": false, - "geolocation": true, - "geolocation_locationObfuscationType": 0, - "gamepads": true, - "vr": true, - "analytics": true, - "windowname": true, - "physical_environment": true, - "physical_environment_emulateStationaryDevice": true, + "time_precision": 3, + "htmlcanvaselement": 2, + "audiobuffer": 2, + "webgl": 2, + "plugins": 3, + "enumerateDevices": 3, + "hardware": 3, + "xhr": 1, + "arrays": 2, + "shared_array": 2, + "webworker": 2, + "geolocation": 6, + "physical_environment": 1, + "gamepads": 1, + "vr": 1, + "analytics": 1, + "battery": 1, + "windowname": 1, }; const BUILTIN_LEVEL_NAMES = [L0, L1, L2, L3]; @@ -803,19 +718,26 @@ function updateLevels(res) { } var new_default_level = res["__default__"]; if (new_default_level === undefined || new_default_level === null || !(new_default_level in levels)) { - default_level = Object.create(levels[L2]); + default_level = Object.assign({}, levels[L2]); setDefaultLevel(L2); } else { - default_level = Object.create(levels[new_default_level]); + default_level = Object.assign({}, levels[new_default_level]); } default_level.is_default = true; var new_domains = res["domains"] || {}; - for (let d in new_domains) { - levid = levels[new_domains[d].level_id]; - if (levid !== undefined) { - domains[d] = levid; + for (let [d, {level_id, tweaks}] of Object.entries(new_domains)) { + let level = levels[level_id]; + if (level === undefined) { + domains[d] = default_level; } + else if (tweaks) { + // this domain has "tweaked" wrapper groups from other levels, let's merge them + level = Object.assign({}, level, tweaks); + level.tweaks = tweaks; + delete level.wrappers; // we will lazy instantiate them on demand in getCurrentLevelJSON() + } + domains[d] = level; } var orig_levels_updated_callbacks = levels_updated_callbacks; levels_updated_callbacks = []; @@ -836,11 +758,21 @@ function setDefaultLevel(level) { function saveDomainLevels() { tobesaved = {}; for (k in domains) { - let level_id = domains[k].level_id; + let {level_id, tweaks} = domains[k]; if (k[k.length - 1] === ".") { k = k.substring(0, k.length-1); } - tobesaved[k] = {level_id: level_id}; + if (tweaks) { + for (let [group, param] of Object.entries(tweaks)) { + if (param === (levels[level_id][group] || 0)) { + delete tweaks[group]; // remove redundant entries + } + } + if (Object.keys(tweaks).length === 0) { + tweaks = undefined; + } + } + tobesaved[k] = tweaks ? {level_id, tweaks} : {level_id}; } browser.storage.sync.set({domains: tobesaved}); } @@ -850,8 +782,23 @@ function getCurrentLevelJSON(url) { for (let domain of subDomains.reverse()) { if (domain in domains) { let l = domains[domain]; - return [l, wrapped_codes[l.level_id]]; + if (l.tweaks && !("wrapper_code" in l)) { + l.wrappers = wrapping_groups.get_wrappers(l); + l.wrapped_code = wrap_code(l) || ""; + } + return [l, l.tweaks ? l.wrapped_code : wrapped_codes[l.level_id]]; } } return [default_level, wrapped_codes[default_level.level_id]]; } + +function getTweaksForLevel(level_id, tweaks_obj) { + tweaks_obj = tweaks_obj || {}; // Make sure that tweaks_obj is an object + let working = Object.assign({}, wrapping_groups.empty_level, levels[level_id], tweaks_obj); + Object.keys(working).forEach(function(key) { + if (!wrapping_groups.group_names.includes(key)) { + delete working[key]; + } + }); + return working; +} diff --git a/common/options.css b/common/options.css index 5b60924..448557c 100644 --- a/common/options.css +++ b/common/options.css @@ -165,7 +165,7 @@ section.content { } .hidden_descr{ - display: none; + display: none !important; } .help { @@ -176,6 +176,25 @@ section.content { font-weight: bold; } +.domain { + font-weight: bold; + font-size: xx-large; +} + +.tweakgrid { + display: grid; + grid: auto / auto 1fr; + gap: 1em; + align-items: center; +} + +.explainer { + grid-column-start: 1; + grid-column-end: -1; + padding-bottom: 3px; +} + + #proxy-protection-config, #fingerprinting-protection-config { display:flex; @@ -292,3 +311,7 @@ section#sect-devel { fieldset { border: 0; } + +.more + .less { + display: none; +} diff --git a/common/options.html b/common/options.html index 21c1d5b..e633c11 100644 --- a/common/options.html +++ b/common/options.html @@ -14,27 +14,28 @@ SPDX-License-Identifier: GPL-3.0-or-later + - JavaScript Restrictor options + JShelter options