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