summaryrefslogtreecommitdiff
path: root/data/extensions/jsr@javascriptrestrictor/wrappingS-SENSOR-MAGNET.js
blob: c713834c84d72a42c4489ef9b392c8d681635acb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
/** \file
 * \brief Wrappers for the Magnetometer Sensor
 *
 * \see https://www.w3.org/TR/magnetometer/
 *
 *  \author Copyright (C) 2021  Radek Hranicky
 *
 *  \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
  *
  * MOTIVATION
  *
  * Magnetometer is a platform sensor available under the Generic Sensor API.
  * Magnetometer measures strength and direction of the magnetic field at device's
  * location. The interface offers sensor readings using three properties: x, y, and z.
  * Each returns a number that describes the magnetic field aroud the particular axis.
  * The numbers have a double precision and can be positive or negative, depending
  * on the orientation of the field. The total strength of the magnetic field (M)
  * can be calculated as M = sqrt(x^2 + z^2 + y^2). The unit is in microtesla (µT).
  *
  * The Earth's magnetic field ranges between approximately 25 and 65 µT. Concrete
  * values depend on location, altitude, weather, interference made by other electric.
  * devices, etc. While we consider it is unlikely that someone determines the precise
  * location of the device from the  Mangetometer values, its data can be used for
  * fingerprinting. For instance, it can be determined wheter the device is moving or not.
  * In case of a stationary device, we can make a fingerprint from the device's orientation.
  * Another fingerprintable value is the average total strength of the field, which
  * should remain stable if the device is at the same position and in the same environment.
  *
  *
  * WRAPPING
  *
  * To protect the device, we are wrapping the x, y, z getters of the
  * `Magnetometer.prototype` object. Instead of using the original data, we use
  * artificially generated values that look like actual sensor readings.
  *
  * At every moment, our wrapper stores information about the previous reading. Each
  * rewrapped getter first checks the `timestamp` value of the sensor object. If there
  * is no difference from the previous reading's timestamp, the wrapper returns the
  * last measured value. Otherwise, it provides a new fake reading.
  *
  * We designed our fake field generator to fulfill the following properties:
  *
  * - The randomness of the generator should be high enough to prevent attackers from
  *   deducing the sensor values.
  * - Multiple scripts from the same website that access readings with the same
  *   timestamp must get the same results. And thus:
  * - The readings are deterministic - e.g., for a given website and time, we must
  *   be able to say what values to return.
  *
  * For every "random" draw, we use the Mulberry32 sen_prng that is seeded with a value
  * generated from the `domainHash` which ensures deterministic behavior for the given
  * website. First, we choose the desired total strength `M` of the magnetic field at
  * our simulated location. This is a pseudo-random number from 25 to 60 uT, like on
  * the Earth.
  *
  * We support two variants of settings the initial axes orientaton:
  * - A pseudorandom draw (RANDOM_AXES_ORIENTATION = true) - the original implementation
  * - Calculation from the faked device rotation (shared by other wrappers) - improved version
  *
  * For both methods, the orientation is defined by a number from -1 to 1 for each axis:
  * `baseX`, `baseY`, and `baseZ`. By modifying the above-shown formula, we calculate
  * the `multiplier` that needs to be applied to the base values to get the desired field.
  * The calculation is done as follows:
  * - mult = (M * sqrt(baseX^2 + baseY^2 + baseZ^2) / (baseX^2 + baseY^2 + baseZ^2))
  * Now, we know that for axis `x`, the value should fluctuate around `baseX * mult`, etc.
  *
  * How much the field changes over time is specified by the fluctuation factor (0;1]
  * that can also be configured. For instance, 0.2 means that the magnetic field on
  * the axis may change from the base value by 20% in both positive and negative way.
  *
  * The fluctuation is simulated by using a series of **sine** functions for each axis.
  * Each sine has a unique amplitude, phase shift, and period. The number of sines per
  * axis is chosen pseudorandomly based on the wrapper settings. For initial experiments,
  * we used around 20 to 30 sines for each axis. The optimal configuration is in question.
  * More sines give less predictable results, but also increase the computing complexity
  * that could have a negative impact on the browser's performance.
  *
  * For the given timestamp `t`, we make the sum of all sine values at the point `x=t`.
  * The result is then shifted over the y-axis by adding `base[X|Y|Z] * multiplier` to
  * the sum. The initial configuration of the fake field generator was chosen intuitively
  * to resemble the results of the real measurements. Currently, the generator uses at
  * least one sine with the period around 100 us (with 10% tolerance), which seems to be
  * the minimum sampling rate obtainable using the API on mobile devices. Then, at least
  * one sine around 1 s, around 10 s, 1 minute, and 1 hour. When more than 5 sines are
  * used, the cycle repeats using `modulo 5` and creates a new sine with the period around
  * 100 us, but this time the tolerance is 20%. The same follows for seconds, tens of
  * seconds, minutes, hours. The tolerance grows every 5 sines. For 11+ sines, the tolerance
  * is 30% up to the maximum (currently 50%). The amplitude of each sine is chosen pseudo-
  * randomly based on the **fluctuation factor** described above. The phase shift of each
  * sine is also pseudo-random number from [0;2PI).
  *
  * Based on the results, this heuristic returns belivable values that look like actual
  * sensor readings. Nevertheless, the generator uses a series of constants, whose optimal
  * values should be a subject of future research and improvements. Perphaps, a correlation
  * analysis with real mesurements could help in the future.
  *
  *
  * POSSIBLE IMPROVEMENTS
  * Non-stationary devices can be supported if the baseX,Y,Z is updated with each movement.
  * Do more experiments in real environments and possibly update the reference magnetic field
  * vector, or the sine generator, e.g. by simulating temporary pseudorandom electromagnetic
  * interferences, etc.
  */

  /*
   * Create private namespace
   */
(function() {
  /*
    * \brief Initialization of data for storing sensor readings
  */
  var init_data = `
    var currentReading = currentReading || {orig_x: null, orig_y: null, orig_z: null, timestamp: null,
                      fake_x: null, fake_y: null, fake_z: null};
    var previousReading = previousReading || {orig_x: null, orig_y: null, orig_z: null, timestamp: null,
                      fake_x: null, fake_y: null, fake_z: null};
    var emulateStationaryDevice = (typeof args === 'undefined') ? true : args[0];
    var debugMode = false;

    const TWOPI = 2 * Math.PI;
    `;

  /*
    * \brief Property getters of the original sensor object
  */
  var orig_getters = `
    var origGetX = Object.getOwnPropertyDescriptor(Magnetometer.prototype, "x").get;
    var origGetY = Object.getOwnPropertyDescriptor(Magnetometer.prototype, "y").get;
    var origGetZ = Object.getOwnPropertyDescriptor(Magnetometer.prototype, "z").get;
    var origGetTimestamp = Object.getOwnPropertyDescriptor(Sensor.prototype, "timestamp").get;
    `;

  /*
    * \brief Constructor of the sine configuration object
  */
  function SineCfg() {
    this.center = 0;
    this.amplitude = 1;
    this.shift = 0;
    this.period = 1;
  }

  /*
    * \brief Creates sine configurations based on the given settings
    *
    * \param Minimum number of sines
    * \param Maximum number of sines
    * \param Center 'y' value that the sine should spin around
    * \param Minimal fluctuation factor of a sine
    * \param Maximal fluctuation factor of a sine
    * \param Minimal period of a sine
    * \param Maximal period of a sine
  */
  function configureSines(cntMin, cntMax, center, flucMin, fluctMax, periodMin, periodMax) {
    // This is helping function for the field generator
    // Configures an array of sines for the given settings

    // How many sines we have?
    var cnt = Math.floor(sen_prng() * (cntMax - cntMin + 1) + cntMin);

    // max difference from base period
    const TOLERANCE_MAX = 0.5;

    // What is the typical amplitude for these sines?
    var sineAmplitude = center / cnt;

    var fluctMinMax = flucMin - fluctMax;
    let sines = [];
    let iteration = 0;
    let tolerance = 0.1;

    for (let i = 0; i < cnt; i++) {
      let s = new SineCfg();
      let fluctuationFactor = sen_prng() * (fluctMinMax) + fluctMax;

      s.center = center;
      s.amplitude = sineAmplitude * fluctuationFactor;
      s.shift = sen_prng() * TWOPI;

      let series = i % 5;

      switch(series) {

        case 0:
          iteration += 1;

          // increase tolerance for new iterations
          if (iteration > 1 && tolerance < TOLERANCE_MAX) {
            tolerance += 0.1;
          }

          // Minimal sampling rate (default: 100 miliseconds)
          s.period = sen_generateAround(periodMin, tolerance);
        break;
        case 1: // Seconds
          s.period = sen_generateAround(1000, tolerance);
        break;
        case 2: // Tens of seconds
          s.period = sen_generateAround(10000, tolerance);
        break;
        case 3: // Minutes
          s.period = sen_generateAround(60000, tolerance);
        break;
        case 4: // Hours
          s.period = sen_generateAround(3600000, tolerance);
        break;
      }
      sines.push(s);
    }
    return sines;
  }

  /*
    * \brief Fake magnetic field generator class
    *        (Modify the constants below to change the generator's behavior.)
  */
  class FieldGenerator {
    constructor() {
      // Specifies, how much the values may (pseudorandomly) oscillate,
      // i.e., how much the may relatively differ from the chosen center value
      // in both positiva and negative way
      this.FLUCTUATION_MIN = 0.20;
      this.FLUCTUATION_MAX = 0.45;
      this.AXES_OSCILLATE_DIFFERENTLY = true;

      this.NUMBER_OF_SINES_MIN = 25;
      this.NUMBER_OF_SINES_MAX = 30;

      // Shifts the phase of each axis randomly [0, 2*PI)
      this.RANDOM_PHASE_SHIFT = true;

      // Minimum sampling rate of the device(s)
      // Motivation: It does not have sense to waste computing resources
      // by oscillating in periods smaller than this value
      this.MIN_SAMPLING_RATE = 100; // [ms]

      // Period configuration
      this.PERIOD_MIN = this.MIN_SAMPLING_RATE;
      this.PERIOD_MAX = 60000 // 1 minute

      // Defines whether the axes orientation is generated pseudorandomly
      // true = A PRNG is used to draw the orientation of x/y/z axes
      // false = orientation is calculated from the Earth's reference
      //         coordinate system and the (faked) orientation of the
      //         phone defined by the global rotation matrix (orient.rotMat)
      this.RANDOM_AXES_ORIENTATION = false;

      let m = generateBaseField();

      // Base of each axis
      var baseX = 0;
      var baseY = 0;
      var baseZ = 0;

      // Calculate the axes base
      if (this.RANDOM_AXES_ORIENTATION) {
        /*
         * Pseudorandom axes orientation
         *
         * The generateRandomAxisBase() is used to draw a number between
         * -1 and 1 for each axis base.
         */
        baseX = generateRandomAxisBase();
        baseY = generateRandomAxisBase();
        baseZ = generateRandomAxisBase();
      } else {
        /*
         * Calculation of axes orientation from the device's rotation
         *
         * The magnetic field vector is oriented towards the Earth's magnetic
         * north and towards the center of the earth.
         */
        let referenceMagVec = [0, 0.4, -0.6];

        /*
         * Actual field's strengths in all directions, based on the orientation:
         * (Tested on Samsung Galaxy S21 Ultra [And12] and Xiaomi Redmi 9 [And11])
         *
         * Legend:
         * -- ... highly negative
         * -  ... negative
         * 0  ... zero
         * +  ... positive
         * ++ ... highly positive
         *
         * +-------+-------+------+---+---+---+
         * |  yaw  | pitch | roll | x | y | z |
         * +-------+-------+------+---+---+---+
         * |  0       0       0     0   +  -- |
         * |  PI      0       0     0   -  -- |
         * |  PI/2    0       0     -   0  -- |
         * | -PI/2    0       0     +   0  -- |
         * +----------------------------------+
         */

        // The vector is rotated using the device's fake rotation matrix
        var deviceMagVec  = multVectRot(referenceMagVec, orient.rotMat);

        if (debugMode) {
        }

        // The orientation is taken from the elements of the vector
        baseX = deviceMagVec[0];
        baseY = deviceMagVec[1];
        baseZ = deviceMagVec[2];
      }

      var baseX2 = Math.pow(baseX,2)
      var baseY2 = Math.pow(baseY,2)
      var baseZ2 = Math.pow(baseZ,2)

      // The total magnetic field strength is calculated as:
      //   m = sqrt(x^2, y^2, z^2)
      // where x,y,z are strengs in individual directions (axes).
      //
      // For x,y,z, the algorithm generates a sine-based fluctuation around
      // a center value for each axis. For axis x, it is calculated as:
      //   x = baseX * multiplier
      //
      // At this moment, we have calculate the basis (-1,1) for each axis.
      // Now, we calculate the multiplier:
      //
      //                   m + sqrt(baseX^2 + baseY^2 + baseZ^2)
      // multiplier = +/- -------------------------------------
      //                       baseX^2 + baseY^2 + baseZ^2
      //
      // Values at axis X will oscillate around: baseX * multiplier, etc.

      let mult = (m * Math.sqrt(baseX2 + baseY2 + baseZ2))
                    / (baseX2 + baseY2 + baseZ2);

      this.baseField = m,
      this.multiplier = mult,
      this.x = {
        base: baseX,
        center: baseX * mult,
        sines: [],
        value: null
      };
      this.y = {
        base: baseY,
        center: baseY * mult,
        sines: [],
        value: null
      };
      this.z = {
        base: baseZ,
        center: baseZ * mult,
        sines: [],
        value: null
      };

      this.x.sines = configureSines(this.NUMBER_OF_SINES_MIN, this.NUMBER_OF_SINES_MAX, this.x.center,
                                    this.FLUCTUATION_MIN, this.FLUCTUATION_MAX, this.PERIOD_MIN, this.PERIOD_MAX);
      this.y.sines = configureSines(this.NUMBER_OF_SINES_MIN, this.NUMBER_OF_SINES_MAX, this.y.center,
                                    this.FLUCTUATION_MIN, this.FLUCTUATION_MAX, this.PERIOD_MIN, this.PERIOD_MAX);
      this.z.sines = configureSines(this.NUMBER_OF_SINES_MIN, this.NUMBER_OF_SINES_MAX, this.z.center,
                                    this.FLUCTUATION_MIN, this.FLUCTUATION_MAX, this.PERIOD_MIN, this.PERIOD_MAX);
    }

    // Updates the x/y/z values based on timestamp
    update(t) {
      // Simulate the magnetic field fluctuation based on settings
      // Center is added only once - we want to y-shift the result, not individial sines
      this.x.value = this.x.center + this.x.sines.reduce(function (val, s) {
        return val + (Math.sin(t * (TWOPI/s.period) + s.shift) * s.amplitude);
      }, 0);
      this.y.value = this.y.center + this.y.sines.reduce(function (val, s) {
        return val + (Math.sin(t * (TWOPI/s.period) + s.shift) * s.amplitude);
      }, 0);
      this.z.value = this.z.center + this.z.sines.reduce(function (val, s) {
        return val + (Math.sin(t * (TWOPI/s.period) + s.shift) * s.amplitude);
      }, 0);
    }
  }

  /*
    * \brief Pseudorandomly draws the desired total magnetic field around the device
  */
  function generateBaseField() {
    const FIELD_MIN = 25;
    const FIELD_MAX = 60;
    return sen_prng() * (FIELD_MIN - FIELD_MAX) + FIELD_MAX;
  }

  /*
    * \brief Pseudorandomly draws the orientation of X, Y, Z axes
  */
  function generateRandomAxisBase() {
    // Returns a number in (-1,1)
    var v = sen_prng(); // Random in [0,1)
    v *= Math.round(sen_prng()) ? 1 : -1; // 50% change for positive / negative
    return v;
  }

  /*
    * \brief Updates the stored (both real and fake) sensor readings
    *        according to the data from the sensor object.
    *
    * \param The sensor object
  */
  function updateReadings(sensorObject) {
    // We need the original reading's timestamp to see if it differs
    // from the previous sample. If so, we need to update the faked x,y,z
    let previousTimestamp = previousReading.timestamp;
    let currentTimestamp = origGetTimestamp.call(sensorObject);

    if (debugMode) {
      // [!] Debug mode: overriding timestamp
      // This allows test suites to set a custom timestamp externally
      // by modifying the property of the Magnetometer object directly.
      currentTimestamp = sensorObject.timestamp;
    }

    if (currentTimestamp === previousTimestamp) {
      // No new reading, nothing to update
      return;
    }

    // Rotate the readings: previous <- current
    previousReading = JSON.parse(JSON.stringify(currentReading));

    // Update current reading
    // NOTE: Original values are also stored for possible future use
    //       in improvements of the magnetic field generator
    currentReading.orig_x = origGetX.call(sensorObject);
    currentReading.orig_y = origGetY.call(sensorObject);
    currentReading.orig_z = origGetZ.call(sensorObject);
    currentReading.timestamp = currentTimestamp;

    fieldGenerator.update(currentTimestamp);
    currentReading.fake_x = fieldGenerator.x.value;
    currentReading.fake_y = fieldGenerator.y.value;
    currentReading.fake_z = fieldGenerator.z.value;

    if (debugMode) {
    }
  }

  /*
    * \brief Initializes the related generators
  */
  var generators = `
    // Initialize the field generator, if not initialized before
    var fieldGenerator = fieldGenerator || new FieldGenerator();
    `;

  var helping_functions = sensorapi_prng_functions + device_orientation_functions
          + SineCfg + configureSines + FieldGenerator
          + generateBaseField + generateRandomAxisBase + updateReadings;
  var hc = init_data + orig_getters + helping_functions + generators;

  var wrappers = [
    {
      parent_object: "Magnetometer.prototype",
      parent_object_property: "x",
      wrapped_objects: [],
      helping_code: hc,
      post_wrapping_code: [
        {
          code_type: "object_properties",
          parent_object: "Magnetometer.prototype",
          parent_object_property: "x",
          wrapped_objects: [],
          /**  \brief replaces Sensor.prototype.x getter to return a faked value
           */
          wrapped_properties: [
            {
              property_name: "get",
              property_value: `
              function() {
               updateReadings(this);
               return currentReading.fake_x;
             }`,
            },
          ],
        }
      ],
    },
    {
      parent_object: "Magnetometer.prototype",
      parent_object_property: "y",
      wrapped_objects: [],
      helping_code: hc,
      post_wrapping_code: [
        {
          code_type: "object_properties",
          parent_object: "Magnetometer.prototype",
          parent_object_property: "y",
          wrapped_objects: [],
          /**  \brief replaces Sensor.prototype.y getter to return a faked value
           */
          wrapped_properties: [
            {
              property_name: "get",
              property_value: `
              function() {
               updateReadings(this);
               return currentReading.fake_y;
             }`,
            },
          ],
        }
      ],
    },
    {
      parent_object: "Magnetometer.prototype",
      parent_object_property: "z",
      wrapped_objects: [],
      helping_code: hc,
      post_wrapping_code: [
        {
          code_type: "object_properties",
          parent_object: "Magnetometer.prototype",
          parent_object_property: "z",
          wrapped_objects: [],
          /**  \brief replaces Sensor.prototype.z getter to return a faked value
           */
          wrapped_properties: [
            {
              property_name: "get",
              property_value: `
              function() {
               updateReadings(this);
               return currentReading.fake_z;
             }`,
            },
          ],
        }
      ],
    },
  ]
    add_wrappers(wrappers);
})()