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 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 */ 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 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 */ 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 */ 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 304 private double nextOffsetLocked() { 305 return mRandom.nextGaussian() * mStandardDeviationInMeters; 306 } 307 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 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 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 */ 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 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 */ 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 */ 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 */ 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