/** \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 . // /** \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); })();