• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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;
18 
19 import java.io.FileDescriptor;
20 import java.io.PrintWriter;
21 import java.security.SecureRandom;
22 import android.content.Context;
23 import android.database.ContentObserver;
24 import android.location.Location;
25 import android.location.LocationManager;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.Parcelable;
29 import android.os.SystemClock;
30 import android.provider.Settings;
31 import android.util.Log;
32 
33 
34 /**
35  * Contains the logic to obfuscate (fudge) locations for coarse applications.
36  *
37  * <p>The goal is just to prevent applications with only
38  * the coarse location permission from receiving a fine location.
39  */
40 public class LocationFudger {
41     private static final boolean D = false;
42     private static final String TAG = "LocationFudge";
43 
44     /**
45      * Default coarse accuracy in meters.
46      */
47     private static final float DEFAULT_ACCURACY_IN_METERS = 2000.0f;
48 
49     /**
50      * Minimum coarse accuracy in meters.
51      */
52     private static final float MINIMUM_ACCURACY_IN_METERS = 200.0f;
53 
54     /**
55      * Secure settings key for coarse accuracy.
56      */
57     private static final String COARSE_ACCURACY_CONFIG_NAME = "locationCoarseAccuracy";
58 
59     /**
60      * This is the fastest interval that applications can receive coarse
61      * locations.
62      */
63     public static final long FASTEST_INTERVAL_MS = 10 * 60 * 1000;  // 10 minutes
64 
65     /**
66      * The duration until we change the random offset.
67      */
68     private static final long CHANGE_INTERVAL_MS = 60 * 60 * 1000;  // 1 hour
69 
70     /**
71      * The percentage that we change the random offset at every interval.
72      *
73      * <p>0.0 indicates the random offset doesn't change. 1.0
74      * indicates the random offset is completely replaced every interval.
75      */
76     private static final double CHANGE_PER_INTERVAL = 0.03;  // 3% change
77 
78     // Pre-calculated weights used to move the random offset.
79     //
80     // The goal is to iterate on the previous offset, but keep
81     // the resulting standard deviation the same. The variance of
82     // two gaussian distributions summed together is equal to the
83     // sum of the variance of each distribution. So some quick
84     // algebra results in the following sqrt calculation to
85     // weigh in a new offset while keeping the final standard
86     // deviation unchanged.
87     private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL;
88     private static final double PREVIOUS_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT);
89 
90     /**
91      * This number actually varies because the earth is not round, but
92      * 111,000 meters is considered generally acceptable.
93      */
94     private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111000;
95 
96     /**
97      * Maximum latitude.
98      *
99      * <p>We pick a value 1 meter away from 90.0 degrees in order
100      * to keep cosine(MAX_LATITUDE) to a non-zero value, so that we avoid
101      * divide by zero fails.
102      */
103     private static final double MAX_LATITUDE = 90.0 -
104             (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR);
105 
106     private final Object mLock = new Object();
107     private final SecureRandom mRandom = new SecureRandom();
108 
109     /**
110      * Used to monitor coarse accuracy secure setting for changes.
111      */
112     private final ContentObserver mSettingsObserver;
113 
114     /**
115      * Used to resolve coarse accuracy setting.
116      */
117     private final Context mContext;
118 
119     // all fields below protected by mLock
120     private double mOffsetLatitudeMeters;
121     private double mOffsetLongitudeMeters;
122     private long mNextInterval;
123 
124     /**
125      * Best location accuracy allowed for coarse applications.
126      * This value should only be set by {@link #setAccuracyInMetersLocked(float)}.
127      */
128     private float mAccuracyInMeters;
129 
130     /**
131      * The distance between grids for snap-to-grid. See {@link #createCoarse}.
132      * This value should only be set by {@link #setAccuracyInMetersLocked(float)}.
133      */
134     private double mGridSizeInMeters;
135 
136     /**
137      * Standard deviation of the (normally distributed) random offset applied
138      * to coarse locations. It does not need to be as large as
139      * {@link #COARSE_ACCURACY_METERS} because snap-to-grid is the primary obfuscation
140      * method. See further details in the implementation.
141      * This value should only be set by {@link #setAccuracyInMetersLocked(float)}.
142      */
143     private double mStandardDeviationInMeters;
144 
LocationFudger(Context context, Handler handler)145     public LocationFudger(Context context, Handler handler) {
146         mContext = context;
147         mSettingsObserver = new ContentObserver(handler) {
148             @Override
149             public void onChange(boolean selfChange) {
150                 setAccuracyInMeters(loadCoarseAccuracy());
151             }
152         };
153         mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
154                 COARSE_ACCURACY_CONFIG_NAME), false, mSettingsObserver);
155 
156         float accuracy = loadCoarseAccuracy();
157         synchronized (mLock) {
158             setAccuracyInMetersLocked(accuracy);
159             mOffsetLatitudeMeters = nextOffsetLocked();
160             mOffsetLongitudeMeters = nextOffsetLocked();
161             mNextInterval = SystemClock.elapsedRealtime() + CHANGE_INTERVAL_MS;
162         }
163     }
164 
165     /**
166      * Get the cached coarse location, or generate a new one and cache it.
167      */
getOrCreate(Location location)168     public Location getOrCreate(Location location) {
169         synchronized (mLock) {
170             Location coarse = location.getExtraLocation(Location.EXTRA_COARSE_LOCATION);
171             if (coarse == null) {
172                 return addCoarseLocationExtraLocked(location);
173             }
174             if (coarse.getAccuracy() < mAccuracyInMeters) {
175                 return addCoarseLocationExtraLocked(location);
176             }
177             return coarse;
178         }
179     }
180 
addCoarseLocationExtraLocked(Location location)181     private Location addCoarseLocationExtraLocked(Location location) {
182         Location coarse = createCoarseLocked(location);
183         location.setExtraLocation(Location.EXTRA_COARSE_LOCATION, coarse);
184         return coarse;
185     }
186 
187     /**
188      * Create a coarse location.
189      *
190      * <p>Two techniques are used: random offsets and snap-to-grid.
191      *
192      * <p>First we add a random offset. This mitigates against detecting
193      * grid transitions. Without a random offset it is possible to detect
194      * a users position very accurately when they cross a grid boundary.
195      * The random offset changes very slowly over time, to mitigate against
196      * taking many location samples and averaging them out.
197      *
198      * <p>Second we snap-to-grid (quantize). This has the nice property of
199      * producing stable results, and mitigating against taking many samples
200      * to average out a random offset.
201      */
createCoarseLocked(Location fine)202     private Location createCoarseLocked(Location fine) {
203         Location coarse = new Location(fine);
204 
205         // clean all the optional information off the location, because
206         // this can leak detailed location information
207         coarse.removeBearing();
208         coarse.removeSpeed();
209         coarse.removeAltitude();
210         coarse.setExtras(null);
211 
212         double lat = coarse.getLatitude();
213         double lon = coarse.getLongitude();
214 
215         // wrap
216         lat = wrapLatitude(lat);
217         lon = wrapLongitude(lon);
218 
219         // Step 1) apply a random offset
220         //
221         // The goal of the random offset is to prevent the application
222         // from determining that the device is on a grid boundary
223         // when it crosses from one grid to the next.
224         //
225         // We apply the offset even if the location already claims to be
226         // inaccurate, because it may be more accurate than claimed.
227         updateRandomOffsetLocked();
228         // perform lon first whilst lat is still within bounds
229         lon += metersToDegreesLongitude(mOffsetLongitudeMeters, lat);
230         lat += metersToDegreesLatitude(mOffsetLatitudeMeters);
231         if (D) Log.d(TAG, String.format("applied offset of %.0f, %.0f (meters)",
232                 mOffsetLongitudeMeters, mOffsetLatitudeMeters));
233 
234         // wrap
235         lat = wrapLatitude(lat);
236         lon = wrapLongitude(lon);
237 
238         // Step 2) Snap-to-grid (quantize)
239         //
240         // This is the primary means of obfuscation. It gives nice consistent
241         // results and is very effective at hiding the true location
242         // (as long as you are not sitting on a grid boundary, which
243         // step 1 mitigates).
244         //
245         // Note we quantize the latitude first, since the longitude
246         // quantization depends on the latitude value and so leaks information
247         // about the latitude
248         double latGranularity = metersToDegreesLatitude(mGridSizeInMeters);
249         lat = Math.round(lat / latGranularity) * latGranularity;
250         double lonGranularity = metersToDegreesLongitude(mGridSizeInMeters, lat);
251         lon = Math.round(lon / lonGranularity) * lonGranularity;
252 
253         // wrap again
254         lat = wrapLatitude(lat);
255         lon = wrapLongitude(lon);
256 
257         // apply
258         coarse.setLatitude(lat);
259         coarse.setLongitude(lon);
260         coarse.setAccuracy(Math.max(mAccuracyInMeters, coarse.getAccuracy()));
261 
262         if (D) Log.d(TAG, "fudged " + fine + " to " + coarse);
263         return coarse;
264     }
265 
266     /**
267      * Update the random offset over time.
268      *
269      * <p>If the random offset was new for every location
270      * fix then an application can more easily average location results
271      * over time,
272      * especially when the location is near a grid boundary. On the
273      * other hand if the random offset is constant then if an application
274      * found a way to reverse engineer the offset they would be able
275      * to detect location at grid boundaries very accurately. So
276      * we choose a random offset and then very slowly move it, to
277      * make both approaches very hard.
278      *
279      * <p>The random offset does not need to be large, because snap-to-grid
280      * is the primary obfuscation mechanism. It just needs to be large
281      * enough to stop information leakage as we cross grid boundaries.
282      */
updateRandomOffsetLocked()283     private void updateRandomOffsetLocked() {
284         long now = SystemClock.elapsedRealtime();
285         if (now < mNextInterval) {
286             return;
287         }
288 
289         if (D) Log.d(TAG, String.format("old offset: %.0f, %.0f (meters)",
290                 mOffsetLongitudeMeters, mOffsetLatitudeMeters));
291 
292         // ok, need to update the random offset
293         mNextInterval = now + CHANGE_INTERVAL_MS;
294 
295         mOffsetLatitudeMeters *= PREVIOUS_WEIGHT;
296         mOffsetLatitudeMeters += NEW_WEIGHT * nextOffsetLocked();
297         mOffsetLongitudeMeters *= PREVIOUS_WEIGHT;
298         mOffsetLongitudeMeters += NEW_WEIGHT * nextOffsetLocked();
299 
300         if (D) Log.d(TAG, String.format("new offset: %.0f, %.0f (meters)",
301                 mOffsetLongitudeMeters, mOffsetLatitudeMeters));
302     }
303 
nextOffsetLocked()304     private double nextOffsetLocked() {
305         return mRandom.nextGaussian() * mStandardDeviationInMeters;
306     }
307 
wrapLatitude(double lat)308     private static double wrapLatitude(double lat) {
309          if (lat > MAX_LATITUDE) {
310              lat = MAX_LATITUDE;
311          }
312          if (lat < -MAX_LATITUDE) {
313              lat = -MAX_LATITUDE;
314          }
315          return lat;
316     }
317 
wrapLongitude(double lon)318     private static double wrapLongitude(double lon) {
319         lon %= 360.0;  // wraps into range (-360.0, +360.0)
320         if (lon >= 180.0) {
321             lon -= 360.0;
322         }
323         if (lon < -180.0) {
324             lon += 360.0;
325         }
326         return lon;
327     }
328 
metersToDegreesLatitude(double distance)329     private static double metersToDegreesLatitude(double distance) {
330         return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR;
331     }
332 
333     /**
334      * Requires latitude since longitudinal distances change with distance from equator.
335      */
metersToDegreesLongitude(double distance, double lat)336     private static double metersToDegreesLongitude(double distance, double lat) {
337         return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat));
338     }
339 
dump(FileDescriptor fd, PrintWriter pw, String[] args)340     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
341         pw.println(String.format("offset: %.0f, %.0f (meters)", mOffsetLongitudeMeters,
342                 mOffsetLatitudeMeters));
343     }
344 
345     /**
346      * This is the main control: call this to set the best location accuracy
347      * allowed for coarse applications and all derived values.
348      */
setAccuracyInMetersLocked(float accuracyInMeters)349     private void setAccuracyInMetersLocked(float accuracyInMeters) {
350         mAccuracyInMeters = Math.max(accuracyInMeters, MINIMUM_ACCURACY_IN_METERS);
351         if (D) {
352             Log.d(TAG, "setAccuracyInMetersLocked: new accuracy = " + mAccuracyInMeters);
353         }
354         mGridSizeInMeters = mAccuracyInMeters;
355         mStandardDeviationInMeters = mGridSizeInMeters / 4.0;
356     }
357 
358     /**
359      * Same as setAccuracyInMetersLocked without the pre-lock requirement.
360      */
setAccuracyInMeters(float accuracyInMeters)361     private void setAccuracyInMeters(float accuracyInMeters) {
362         synchronized (mLock) {
363             setAccuracyInMetersLocked(accuracyInMeters);
364         }
365     }
366 
367     /**
368      * Loads the coarse accuracy value from secure settings.
369      */
loadCoarseAccuracy()370     private float loadCoarseAccuracy() {
371         String newSetting = Settings.Secure.getString(mContext.getContentResolver(),
372                 COARSE_ACCURACY_CONFIG_NAME);
373         if (D) {
374             Log.d(TAG, "loadCoarseAccuracy: newSetting = \"" + newSetting + "\"");
375         }
376         if (newSetting == null) {
377             return DEFAULT_ACCURACY_IN_METERS;
378         }
379         try {
380             return Float.parseFloat(newSetting);
381         } catch (NumberFormatException e) {
382             return DEFAULT_ACCURACY_IN_METERS;
383         }
384     }
385 }
386