• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.location.fudger;
18 
19 import static com.android.internal.location.geometry.S2CellIdUtils.LAT_INDEX;
20 import static com.android.internal.location.geometry.S2CellIdUtils.LNG_INDEX;
21 
22 import android.annotation.FlaggedApi;
23 import android.annotation.Nullable;
24 import android.location.Location;
25 import android.location.LocationResult;
26 import android.location.flags.Flags;
27 import android.os.SystemClock;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.internal.location.geometry.S2CellIdUtils;
32 
33 import java.security.SecureRandom;
34 import java.time.Clock;
35 import java.util.Random;
36 
37 /**
38  * Contains the logic to obfuscate (fudge) locations for coarse applications. The goal is just to
39  * prevent applications with only the coarse location permission from receiving a fine location.
40  */
41 public class LocationFudger {
42 
43     // minimum accuracy a coarsened location can have
44     private static final float MIN_ACCURACY_M = 200.0f;
45 
46     // how often random offsets are updated
47     @VisibleForTesting
48     static final long OFFSET_UPDATE_INTERVAL_MS = 60 * 60 * 1000;
49 
50     // the percentage that we change the random offset at every interval. 0.0 indicates the random
51     // offset doesn't change. 1.0 indicates the random offset is completely replaced every interval
52     private static final double CHANGE_PER_INTERVAL = 0.03;  // 3% change
53 
54     // weights used to move the random offset. the goal is to iterate on the previous offset, but
55     // keep the resulting standard deviation the same. the variance of two gaussian distributions
56     // summed together is equal to the sum of the variance of each distribution. so some quick
57     // algebra results in the following sqrt calculation to weight in a new offset while keeping the
58     // final standard deviation unchanged.
59     private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL;
60     private static final double OLD_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT);
61 
62     // this number actually varies because the earth is not round, but 111,000 meters is considered
63     // generally acceptable
64     private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111_000;
65 
66     // we pick a value 1 meter away from 90.0 degrees in order to keep cosine(MAX_LATITUDE) to a
67     // non-zero value, so that we avoid divide by zero errors
68     private static final double MAX_LATITUDE =
69             90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR);
70 
71     // The average edge length in km of an S2 cell, indexed by S2 levels 0 to
72     // 13. Level 13 is the highest level used for coarsening.
73     // This approximation assumes the S2 cells are squares.
74     // For density-based coarsening, we use the edge to set the accuracy of the
75     // coarsened location.
76     // The values are from http://s2geometry.io/resources/s2cell_statistics.html
77     // We take square root of the average area.
78     private static final float[] S2_CELL_AVG_EDGE_PER_LEVEL = new float[] {
79             9220.14f, 4610.07f, 2305.04f, 1152.52f, 576.26f, 288.13f, 144.06f,
80             72.03f, 36.02f, 20.79f, 9f, 5.05f, 2.25f, 1.13f, 0.57f};
81 
82     private final float mAccuracyM;
83     private final Clock mClock;
84     private final Random mRandom;
85 
86     @GuardedBy("this")
87     private double mLatitudeOffsetM;
88     @GuardedBy("this")
89     private double mLongitudeOffsetM;
90     @GuardedBy("this")
91     private long mNextUpdateRealtimeMs;
92 
93     @GuardedBy("this")
94     @Nullable private Location mCachedFineLocation;
95     @GuardedBy("this")
96     @Nullable private Location mCachedCoarseLocation;
97 
98     @GuardedBy("this")
99     @Nullable private LocationResult mCachedFineLocationResult;
100     @GuardedBy("this")
101     @Nullable private LocationResult mCachedCoarseLocationResult;
102 
103     @GuardedBy("this")
104     @Nullable private LocationFudgerCache mLocationFudgerCache = null;
105 
LocationFudger(float accuracyM)106     public LocationFudger(float accuracyM) {
107         this(accuracyM, SystemClock.elapsedRealtimeClock(), new SecureRandom());
108     }
109 
110     @VisibleForTesting
LocationFudger(float accuracyM, Clock clock, Random random)111     LocationFudger(float accuracyM, Clock clock, Random random) {
112         mClock = clock;
113         mRandom = random;
114         mAccuracyM = Math.max(accuracyM, MIN_ACCURACY_M);
115 
116         resetOffsets();
117     }
118 
119     /**
120      * Provides the optional {@link LocationFudgerCache} for coarsening based on population density.
121      */
122     @FlaggedApi(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS)
setLocationFudgerCache(LocationFudgerCache cache)123     public void setLocationFudgerCache(LocationFudgerCache cache) {
124         synchronized (this) {
125             mLocationFudgerCache = cache;
126         }
127     }
128 
129     /**
130      * Resets the random offsets completely.
131      */
resetOffsets()132     public void resetOffsets() {
133         mLatitudeOffsetM = nextRandomOffset();
134         mLongitudeOffsetM = nextRandomOffset();
135         mNextUpdateRealtimeMs = mClock.millis() + OFFSET_UPDATE_INTERVAL_MS;
136     }
137 
138     /**
139      * Coarsens a LocationResult by coarsening every location within the location result with
140      * {@link #createCoarse(Location)}.
141      */
createCoarse(LocationResult fineLocationResult)142     public LocationResult createCoarse(LocationResult fineLocationResult) {
143         synchronized (this) {
144             if (fineLocationResult == mCachedFineLocationResult
145                     || fineLocationResult == mCachedCoarseLocationResult) {
146                 return mCachedCoarseLocationResult;
147             }
148         }
149 
150         LocationResult coarseLocationResult = fineLocationResult.map(this::createCoarse);
151 
152         synchronized (this) {
153             mCachedFineLocationResult = fineLocationResult;
154             mCachedCoarseLocationResult = coarseLocationResult;
155         }
156 
157         return coarseLocationResult;
158     }
159 
160     /**
161      * Create a coarse location using two technique, random offsets and snap-to-grid.
162      *
163      * First we add a random offset to mitigate against detecting grid transitions. Without a random
164      * offset it is possible to detect a user's position quite accurately when they cross a grid
165      * boundary. The random offset changes very slowly over time, to mitigate against taking many
166      * location samples and averaging them out. Second we snap-to-grid (quantize). This has the nice
167      * property of producing stable results, and mitigating against taking many samples to average
168      * out a random offset.
169      */
createCoarse(Location fine)170     public Location createCoarse(Location fine) {
171         synchronized (this) {
172             if (fine == mCachedFineLocation || fine == mCachedCoarseLocation) {
173                 return mCachedCoarseLocation;
174             }
175         }
176 
177         // update the offsets in use
178         updateOffsets();
179 
180         Location coarse = new Location(fine);
181 
182         // clear any fields that could leak more detailed location information
183         coarse.removeBearing();
184         coarse.removeSpeed();
185         coarse.removeAltitude();
186         coarse.setExtras(null);
187 
188         double latitude = wrapLatitude(coarse.getLatitude());
189         double longitude = wrapLongitude(coarse.getLongitude());
190 
191         // add offsets - update longitude first using the non-offset latitude
192         longitude += wrapLongitude(metersToDegreesLongitude(mLongitudeOffsetM, latitude));
193         latitude += wrapLatitude(metersToDegreesLatitude(mLatitudeOffsetM));
194 
195         // We copy a reference to the cache, so even if mLocationFudgerCache is concurrently set
196         // to null, we can continue executing the condition below.
197         LocationFudgerCache cacheCopy = null;
198         synchronized (this) {
199             cacheCopy = mLocationFudgerCache;
200         }
201         double[] coarsened = new double[] {0.0, 0.0};
202         // TODO(b/381204398): To ensure a safe rollout, two algorithms co-exist. The first is the
203         // new density-based algorithm, while the second is the traditional coarsening algorithm.
204         // Once rollout is done, clean up the unused algorithm.
205         // The new algorithm is applied if and only if (1) the flag is on, (2) the cache has been
206         // set, and (3) the cache has successfully queried the provider for the default coarsening
207         // value.
208         float accuracy = mAccuracyM;
209         if (Flags.populationDensityProvider() && Flags.densityBasedCoarseLocations()
210                 && cacheCopy != null) {
211             if (cacheCopy.hasDefaultValue()) {
212                 // New algorithm that snaps to the center of a S2 cell.
213                 int level = cacheCopy.getCoarseningLevel(latitude, longitude);
214                 coarsened = snapToCenterOfS2Cell(latitude, longitude, level);
215                 accuracy = getS2CellApproximateEdge(level);
216             } else {
217                 // Try to fetch the default value. The answer won't come in time, but will be used
218                 // for the next location to coarsen.
219                 cacheCopy.onDefaultCoarseningLevelNotSet();
220                 // Previous algorithm that snaps to a grid of width mAccuracyM.
221                 coarsened = snapToGrid(latitude, longitude);
222             }
223         } else {
224             // Previous algorithm that snaps to a grid of width mAccuracyM.
225             coarsened = snapToGrid(latitude, longitude);
226         }
227 
228         coarse.setLatitude(coarsened[LAT_INDEX]);
229         coarse.setLongitude(coarsened[LNG_INDEX]);
230         coarse.setAccuracy(Math.max(accuracy, coarse.getAccuracy()));
231 
232         synchronized (this) {
233             mCachedFineLocation = fine;
234             mCachedCoarseLocation = coarse;
235         }
236 
237         return coarse;
238     }
239 
240     // Returns the average edge length in meters of an S2 cell at the given
241     // level. This is computed as if the S2 cell were a square. We do not need
242     // an exact value, only a rough approximation.
243     @VisibleForTesting
getS2CellApproximateEdge(int level)244     protected float getS2CellApproximateEdge(int level) {
245         if (level < 0) {
246             level = 0;
247         } else if (level >= S2_CELL_AVG_EDGE_PER_LEVEL.length) {
248             level = S2_CELL_AVG_EDGE_PER_LEVEL.length - 1;
249         }
250         return S2_CELL_AVG_EDGE_PER_LEVEL[level] * 1000;
251     }
252 
253     // quantize location by snapping to a grid. this is the primary means of obfuscation. it
254     // gives nice consistent results and is very effective at hiding the true location (as
255     // long as you are not sitting on a grid boundary, which the random offsets mitigate).
256     //
257     // note that we quantize the latitude first, since the longitude quantization depends on
258     // the latitude value and so leaks information about the latitude
snapToGrid(double latitude, double longitude)259     private double[] snapToGrid(double latitude, double longitude) {
260         double[] center = new double[] {0.0, 0.0};
261         double latGranularity = metersToDegreesLatitude(mAccuracyM);
262         center[LAT_INDEX] = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity);
263         double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude);
264         center[LNG_INDEX] = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity);
265         return center;
266     }
267 
268     @VisibleForTesting
snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level)269     protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) {
270         long leafCell = S2CellIdUtils.fromLatLngDegrees(latDegrees, lngDegrees);
271         long coarsenedCell = S2CellIdUtils.getParent(leafCell, level);
272         double[] center = new double[] {0.0, 0.0};
273         S2CellIdUtils.toLatLngDegrees(coarsenedCell, center);
274         return center;
275     }
276 
277     /**
278      * Update the random offsets over time.
279      *
280      * If the random offset was reset for every location fix then an application could more easily
281      * average location results over time, especially when the location is near a grid boundary. On
282      * the other hand if the random offset is constant then if an application finds a way to reverse
283      * engineer the offset they would be able to detect location at grid boundaries very accurately.
284      * So we choose a random offset and then very slowly move it, to make both approaches very hard.
285      * The random offset does not need to be large, because snap-to-grid is the primary obfuscation
286      * mechanism. It just needs to be large enough to stop information leakage as we cross grid
287      * boundaries.
288      */
updateOffsets()289     private synchronized void updateOffsets() {
290         long now = mClock.millis();
291         if (now < mNextUpdateRealtimeMs) {
292             return;
293         }
294 
295         mLatitudeOffsetM = (OLD_WEIGHT * mLatitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset());
296         mLongitudeOffsetM = (OLD_WEIGHT * mLongitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset());
297         mNextUpdateRealtimeMs = now + OFFSET_UPDATE_INTERVAL_MS;
298     }
299 
nextRandomOffset()300     private double nextRandomOffset() {
301         return mRandom.nextGaussian() * (mAccuracyM / 4.0);
302     }
303 
wrapLatitude(double lat)304     private static double wrapLatitude(double lat) {
305         if (lat > MAX_LATITUDE) {
306             lat = MAX_LATITUDE;
307         }
308         if (lat < -MAX_LATITUDE) {
309             lat = -MAX_LATITUDE;
310         }
311         return lat;
312     }
313 
wrapLongitude(double lon)314     private static double wrapLongitude(double lon) {
315         lon %= 360.0;  // wraps into range (-360.0, +360.0)
316         if (lon >= 180.0) {
317             lon -= 360.0;
318         }
319         if (lon < -180.0) {
320             lon += 360.0;
321         }
322         return lon;
323     }
324 
metersToDegreesLatitude(double distance)325     private static double metersToDegreesLatitude(double distance) {
326         return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR;
327     }
328 
329     // requires latitude since longitudinal distances change with distance from equator.
metersToDegreesLongitude(double distance, double lat)330     private static double metersToDegreesLongitude(double distance, double lat) {
331         // Needed to convert from longitude distance to longitude degree.
332         // X meters near the poles is more degrees than at the equator.
333         double cosLat = Math.cos(Math.toRadians(lat));
334         // If we are right on top of the pole, the degree is always 0.
335         // We return a very small value instead to avoid divide by zero errors
336         // later on.
337         if (cosLat == 0.0) {
338             return 0.0001;
339         }
340         return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat;
341     }
342 }
343