summaryrefslogtreecommitdiff
path: root/data/extensions/jsr@javascriptrestrictor/wrappingS-GEO.js
diff options
context:
space:
mode:
authorRuben Rodriguez <ruben@trisquel.info>2022-09-08 20:18:54 -0400
committerRuben Rodriguez <ruben@trisquel.info>2022-09-08 20:18:54 -0400
commit5da28b0f8771834ae208d61431d632875e9f8e7d (patch)
tree688ecaff26197bad8abde617b4947b11d617309e /data/extensions/jsr@javascriptrestrictor/wrappingS-GEO.js
parent4a87716686104266a9cccc2d83cc249e312f3673 (diff)
Updated extensions:
* Upgraded Privacy Redirect to 1.1.49 and configured to use the 10 most reliable invidious instances * Removed ViewTube * Added torproxy@icecat.gnu based on 'Proxy toggle' extension * Added jShelter 0.11.1 * Upgraded LibreJS to 7.21.0 * Upgraded HTTPS Everywhere to 2021.7.13 * Upgraded SubmitMe to 1.9
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/wrappingS-GEO.js')
-rw-r--r--data/extensions/jsr@javascriptrestrictor/wrappingS-GEO.js383
1 files changed, 383 insertions, 0 deletions
diff --git a/data/extensions/jsr@javascriptrestrictor/wrappingS-GEO.js b/data/extensions/jsr@javascriptrestrictor/wrappingS-GEO.js
new file mode 100644
index 0000000..a23ba47
--- /dev/null
+++ b/data/extensions/jsr@javascriptrestrictor/wrappingS-GEO.js
@@ -0,0 +1,383 @@
+/** \file
+ * \brief This file contains wrappers for the Geolocation API
+ *
+ * \see https://www.w3.org/TR/geolocation-API/
+ *
+ * \author Copyright (C) 2019 Martin Timko
+ * \author Copyright (C) 2020 Libor Polcak
+ * \author Copyright (C) 2020 Peter Marko
+ * \author Copyright (C) 2021 Giorgio Maone
+ *
+ * \license SPDX-License-Identifier: GPL-3.0-or-later
+ */
+//
+// 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 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+//
+
+/** \file
+ * \ingroup wrappers
+ *
+ * The goal is to prevent leaks of user current position. The Geolocation API also provides access
+ * to high precision timestamps which can be used to various web attacks (see for example,
+ * http://www.jucs.org/jucs_21_9/clock_skew_based_computer,
+ * https://lirias.kuleuven.be/retrieve/389086).
+ *
+ * Although it is true that the user needs to specificaly approve access to location facilities,
+ * these wrappers aim on improving the control of the precision of the geolocation.
+ *
+ * The wrappers support the following controls:
+ *
+ * * Accurate data: the extension provides precise geolocation position but modifies the time
+ * precision in conformance with the Date and Performance wrappers.
+ * * Modified position: the extension modifies the time precision of the time stamps in
+ * conformance with the Date and Performance wrappers, and additionally, allows to limit the
+ * precision of the current position to hundered of meters, kilometers, tens, or hundereds of
+ * kilometers.
+ *
+ * When modifying position:
+ *
+ * * Repeated calls of `navigator.geolocation.getCurrentPosition()` return the same position
+ * without page load and typically return another position after page reload.
+ * * `navigator.geolocation.watchPosition()` does not change position.
+ */
+
+(function() {
+ var processOriginalGPSDataObject_globals = `
+ let geo_prng = alea(domainHash, "Geolocation");
+ let randomx = geo_prng();
+ let randomy = geo_prng();
+ /**
+ * Make sure that repeated calls shows the same position (BUT different objects, via cloning)
+ * to reduce fingerprintablity.
+ */
+ let previouslyReturnedCoords;
+ let clone = obj => Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
+
+ /**
+ * \brief Store the limit for the returned timestamps.
+ *
+ * This is used to avoid returning older readings compared to the previous readings.
+ * The timestamp needs to be the same as was last time or newser.
+ */
+ var geoTimestamp = Date.now();
+ `;
+ /**
+ * \brief Modifies the given PositionObject according to settings
+ *
+ * \param expectedMaxAge The maximal age of the returned time stamps as defined by the wrapped API
+ * (https://www.w3.org/TR/geolocation-API/#max-age)
+ * \param originalPositionObject the position object to be returned without this wrapper, see the
+ * Position interface (https://www.w3.org/TR/geolocation-API/#position)
+ *
+ * The function modifies the originalPositionObject and stores it for later readins. The returned
+ * position does not modify during the life time of a pages. The returned postion can be different
+ * after a page reload.
+ *
+ * The goal of the behavoiur is to prevent learning the current position so that different
+ * original postion can be mapped to the same position and the same position should generally
+ * yield a different outcome to prevent correlation of user activities.
+ *
+ * The algorithm works as follows:
+ * 1. The Earth surface is partitioned into squares (tiles) with the edge derived from the desired
+ * accuracy.
+ * 2. The position from the originalPositionObject is mapped to its tile and eight adjacent tiles.
+ * 3. A position in the current tile and the eight adjacent tiles is selected randomly.
+ *
+ * The returned timestamp is not older than 1 hour and it is the same as was during the last call
+ * or newer. Different calls to the function can yield different timestamps.
+ *
+ * \bug The tile-based approach does not work correctly near poles but:
+ * * The function returns fake locations near poles.
+ * * As there are not many people near poles, we do not believe this wrapping is useful near poles
+ * so we do not consider this bug as important.
+ */
+
+ function spoofCall(fakeData, originalPositionObject, successCallback) {
+ // proxying the original object lessens the fingerprintable weirdness
+ // (e.g. accessors on the instance rather than on the prototype)
+ fakeData = clone(fakeData);
+ let pos = new Proxy(originalPositionObject, {
+ get(target, key) {
+ return (key in fakeData) ? fakeData[key] : target[key];
+ },
+ getPrototypeOf(target) {
+ return Object.getPrototypeOf(target);
+ }
+ });
+ successCallback(pos);
+ }
+
+ function processOriginalGPSDataObject(expectedMaxAge, originalPositionObject) {
+ if (expectedMaxAge === undefined) {
+ expectedMaxAge = 0; // default value
+ }
+ // Set reasonable expectedMaxAge of 1 hour for later computation
+ expectedMaxAge = Math.min(3600000, expectedMaxAge);
+ geoTimestamp = Math.max(geoTimestamp, Date.now() - Math.random()*expectedMaxAge);
+
+ let spoofPos = coords => {
+ let pos = { timestamp: geoTimestamp };
+ if (coords) pos.coords = coords;
+ spoofCall(pos, originalPositionObject, successCallback);
+ };
+
+ if (provideAccurateGeolocationData) {
+ return spoofPos();
+ }
+ if (previouslyReturnedCoords) {
+ return spoofPos(clone(previouslyReturnedCoords));
+ }
+
+ const EQUATOR_LEN = 40074;
+ const HALF_MERIDIAN = 10002;
+ const DESIRED_ACCURACY_KM = desiredAccuracy*2;
+
+ var lat = originalPositionObject.coords.latitude;
+ var lon = originalPositionObject.coords.longitude;
+ // Compute (approximate) distance from 0 meridian [m]
+ var x = (lon * (EQUATOR_LEN * Math.cos((lat/90)*(Math.PI/2))) / 180);
+ // Compute (approximate) distance from equator [m]
+ var y = (lat / 90) * (HALF_MERIDIAN);
+
+ // Compute the coordinates of the left bottom corner of the tile in which the orig position is
+ var xmin = Math.floor(x / DESIRED_ACCURACY_KM) * DESIRED_ACCURACY_KM;
+ var ymin = Math.floor(y / DESIRED_ACCURACY_KM) * DESIRED_ACCURACY_KM;
+
+ // The position to be returned should be in the original tile and the 8 adjacent tiles:
+ // +----+----+----+
+ // | | | |
+ // +----+----+----+
+ // | |orig| |
+ // +----+----+----+
+ // | | | |
+ // +----+----+----+
+ var newx = xmin + randomx * 3 * DESIRED_ACCURACY_KM - DESIRED_ACCURACY_KM;
+ var newy = ymin + randomy * 3 * DESIRED_ACCURACY_KM - DESIRED_ACCURACY_KM;
+
+ if (Math.abs(newy) > (HALF_MERIDIAN)) {
+ newy = (HALF_MERIDIAN + HALF_MERIDIAN - Math.abs(newy)) * (newy < 0 ? -1 : 1);
+ newx = -newx;
+ }
+
+ var newLatitude = newy / HALF_MERIDIAN * 90;
+ var newLongitude = newx * 180 / (EQUATOR_LEN * Math.cos((newLatitude/90)*(Math.PI/2)));
+
+ while (newLongitude < -180) {
+ newLongitude += 360;
+ }
+ while (newLongitude > 180) {
+ newLongitude -= 360;
+ }
+
+ var newAccuracy = DESIRED_ACCURACY_KM * 1000 * 2.5; // in meters
+
+ previouslyReturnedCoords = {
+ latitude: newLatitude,
+ longitude: newLongitude,
+ altitude: null,
+ accuracy: newAccuracy,
+ altitudeAccuracy: null,
+ heading: null,
+ speed: null,
+ __proto__: originalPositionObject.coords.__proto__
+ };
+ spoofPos(previouslyReturnedCoords);
+ };
+ /**
+ * \brief process the parameters of the wrapping function
+ *
+ * Checks if the wrappers should be active, and the position modified. Transforms the desired
+ * precision into kilometers.
+ */
+ var setArgs = `
+ var enableGeolocation = (args[0] !== 0);
+ var provideAccurateGeolocationData = (args[0] === -1);
+ let desiredAccuracy = 0;
+ switch (args[0]) {
+ case 2:
+ desiredAccuracy = 0.1;
+ break;
+ case 3:
+ desiredAccuracy = 1;
+ break;
+ case 4:
+ desiredAccuracy = 10;
+ break;
+ case 5:
+ desiredAccuracy = 100;
+ break;
+ }
+ `;
+
+ var wrappers = [
+ {
+ parent_object: "Navigator.prototype",
+ parent_object_property: "geolocation",
+ wrapped_objects: [],
+ helping_code: setArgs,
+ post_wrapping_code: [
+ {
+ code_type: "delete_properties",
+ parent_object: "Navigator.prototype",
+ apply_if: "!enableGeolocation",
+ delete_properties: ["geolocation"],
+ }
+ ],
+ },
+ {
+ parent_object: "window",
+ parent_object_property: "Geolocation",
+ wrapped_objects: [],
+ helping_code: setArgs,
+ post_wrapping_code: [
+ {
+ code_type: "delete_properties",
+ parent_object: "window",
+ apply_if: "!enableGeolocation",
+ delete_properties: ["Geolocation"],
+ }
+ ],
+ },
+ {
+ parent_object: "window",
+ parent_object_property: "GeolocationCoordinates",
+ wrapped_objects: [],
+ helping_code: setArgs,
+ post_wrapping_code: [
+ {
+ code_type: "delete_properties",
+ parent_object: "window",
+ apply_if: "!enableGeolocation",
+ delete_properties: ["GeolocationCoordinates"],
+ }
+ ],
+ },
+ {
+ parent_object: "window",
+ parent_object_property: "GeolocationPosition",
+ wrapped_objects: [],
+ helping_code: setArgs,
+ post_wrapping_code: [
+ {
+ code_type: "delete_properties",
+ parent_object: "window",
+ apply_if: "!enableGeolocation",
+ delete_properties: ["GeolocationPosition"],
+ }
+ ],
+ },
+ {
+ parent_object: "window",
+ parent_object_property: "GeolocationPositionError",
+ wrapped_objects: [],
+ helping_code: setArgs,
+ post_wrapping_code: [
+ {
+ code_type: "delete_properties",
+ parent_object: "window",
+ apply_if: "!enableGeolocation",
+ delete_properties: ["GeolocationPositionError"],
+ }
+ ],
+ },
+ {
+ parent_object: "Geolocation.prototype",
+ parent_object_property: "getCurrentPosition",
+
+ wrapped_objects: [
+ {
+ original_name: "Geolocation.prototype.getCurrentPosition",
+ callable_name: "originalGetCurrentPosition",
+ },
+ ],
+ helping_code: setArgs + processOriginalGPSDataObject_globals,
+ wrapping_function_args: "successCallback, errorCallback, origOptions",
+ /** \fn fake Geolocation.prototype.getCurrentPosition
+ * \brief Provide a fake geolocation position
+ */
+ wrapping_function_body: `
+ ${spoofCall}
+ ${processOriginalGPSDataObject}
+ var options = {
+ enableHighAccuracy: false,
+ };
+ if (origOptions) try {
+ if ("timeout" in origOptions) {
+ options.timeout = origOptions.timeout;
+ }
+ if ("maximumAge" in origOptions) {
+ option.maximumAge = origOptions.maximumAge;
+ }
+ }
+ catch { /* Undefined or another error */}
+ originalGetCurrentPosition.call(this, processOriginalGPSDataObject.bind(null, options.maximumAge), errorCallback, options);
+ `,
+ },
+ {
+ parent_object: "Geolocation.prototype",
+ parent_object_property: "watchPosition",
+ wrapped_objects: [
+ {
+ original_name: "Geolocation.prototype.watchPosition",
+ wrapped_name: "originalWatchPosition",
+ },
+ ],
+ helping_code: setArgs + processOriginalGPSDataObject_globals + "let watchPositionCounter = 0;",
+ wrapping_function_args: "successCallback, errorCallback, origOptions",
+ /** \fn fake Geolocation.prototype.watchPosition
+ * Geolocation.prototype.watchPosition intended use concerns tracking user position changes.
+ * JShelter provides four modes of operaion:
+ * * current position approximation: Always return the same data, the same as getCurrentPosition()
+ * * accurate data: Return exact position but fake timestamp
+ */
+ wrapping_function_body: `
+ if (provideAccurateGeolocationData) {
+ function wrappedSuccessCallback(originalPositionObject) {
+ geoTimestamp = Date.now(); // Limit the timestamp accuracy by calling possibly wrapped function
+ return spoofCall({ timestamp: geoTimestamp }, originalPositionObject, succesCallback);
+ }
+ originalWatchPosition.call(this, wrappedSuccessCallback, errorCallback, origOptions);
+ }
+ else {
+ // Re-use the wrapping of n.g.getCurrentPosition()
+ Geolocation.prototype.getCurrentPosition(successCallback, errorCallback, origOptions);
+ watchPositionCounter++;
+ return watchPositionCounter;
+ }
+ `,
+ },
+ {
+ parent_object: "Geolocation.prototype",
+ parent_object_property: "clearWatch",
+ wrapped_objects: [
+ {
+ original_name: "Geolocation.prototype.clearWatch",
+ wrapped_name: "originalClearWatch",
+ },
+ ],
+ helping_code: setArgs,
+ wrapping_function_args: "id",
+ /** \fn fake_or_original Geolocation.prototype.clearWatch
+ * If the Geolocation API provides correct data, call the original implementation,
+ * otherwise do nothing as the watchPosition object was not created.
+ */
+ wrapping_function_body: `
+ if (provideAccurateGeolocationData) {
+ originalClearWatch.call(Geolocation.prototype, id);
+ }
+ `,
+ }
+ ]
+ add_wrappers(wrappers);
+})();