trisquel-icecat/icecat/extensions/gnu/jsr@javascriptrestrictor/wrappingS-GEO.js

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);
})();