383 lines
13 KiB
JavaScript
383 lines
13 KiB
JavaScript
/** \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);
|
|
})();
|