diff --git a/location/java/android/location/LocationRequest.java b/location/java/android/location/LocationRequest.java index 3110196fd5dfc..ce49b413678e3 100644 --- a/location/java/android/location/LocationRequest.java +++ b/location/java/android/location/LocationRequest.java @@ -195,20 +195,6 @@ public final class LocationRequest implements Parcelable { return mSmallestDisplacement; } - /** @hide */ - public LocationRequest applyCoarsePermissionRestrictions() { - switch (mQuality) { - case ACCURACY_FINE: - mQuality = ACCURACY_BLOCK; - break; - } - // cap fastest interval to 6 seconds - if (mFastestInterval < 6 * 1000) mFastestInterval = 6 * 1000; - // cap requested interval to 1 minute - if (mInterval < 60 * 1000) mInterval = 60 * 1000; - return this; - } - private static void checkInterval(long millis) { if (millis < 0) { throw new IllegalArgumentException("invalid interval: " + millis); diff --git a/services/java/com/android/server/LocationManagerService.java b/services/java/com/android/server/LocationManagerService.java index e219e8d7d0ce5..054219b3854f5 100644 --- a/services/java/com/android/server/LocationManagerService.java +++ b/services/java/com/android/server/LocationManagerService.java @@ -47,7 +47,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; -import android.os.Parcelable; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; @@ -64,6 +63,7 @@ import com.android.internal.location.ProviderRequest; import com.android.server.location.GeocoderProxy; import com.android.server.location.GeofenceManager; import com.android.server.location.GpsLocationProvider; +import com.android.server.location.LocationFudger; import com.android.server.location.LocationProviderInterface; import com.android.server.location.LocationProviderProxy; import com.android.server.location.MockProvider; @@ -110,19 +110,6 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs private static final int MSG_LOCATION_CHANGED = 1; - // Accuracy in meters above which a location is considered coarse - private static final double COARSE_ACCURACY_M = 100.0; - private static final String EXTRA_COARSE_LOCATION = "coarseLocation"; - - private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111000; - - /** - * Maximum latitude of 1 meter from the pole. - * This keeps cosine(MAX_LATITUDE) to a non-zero value; - */ - private static final double MAX_LATITUDE = - 90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR); - // Location Providers may sometimes deliver location updates // slightly faster that requested - provide grace period so // we don't unnecessarily filter events that are otherwise on @@ -137,6 +124,7 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs private final Object mLock = new Object(); // --- fields below are final after init() --- + private LocationFudger mLocationFudger; private GeofenceManager mGeofenceManager; private PowerManager.WakeLock mWakeLock; private PackageManager mPackageManager; @@ -221,6 +209,7 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs loadProvidersLocked(); } mGeofenceManager = new GeofenceManager(mContext); + mLocationFudger = new LocationFudger(); // Register for Network (Wifi or Mobile) updates IntentFilter filter = new IntentFilter(); @@ -907,7 +896,22 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs String perm = checkPermission(); if (ACCESS_COARSE_LOCATION.equals(perm)) { - request.applyCoarsePermissionRestrictions(); + switch (request.getQuality()) { + case LocationRequest.ACCURACY_FINE: + request.setQuality(LocationRequest.ACCURACY_BLOCK); + break; + case LocationRequest.POWER_HIGH: + request.setQuality(LocationRequest.POWER_LOW); + break; + } + // throttle fastest interval + if (request.getFastestInterval() < LocationFudger.FASTEST_INTERVAL_MS) { + request.setFastestInterval(LocationFudger.FASTEST_INTERVAL_MS); + } + } + // throttle interval if its faster than the fastest interval + if (request.getInterval () < request.getFastestInterval()) { + request.setInterval(request.getFastestInterval()); } return perm; } @@ -1075,7 +1079,7 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs if (ACCESS_FINE_LOCATION.equals(perm)) { return location; } else { - return getCoarseLocationExtra(location); + return mLocationFudger.getOrCreate(location); } } } @@ -1291,11 +1295,8 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs LocationProviderInterface p = mProvidersByName.get(provider); if (p == null) return; - // Add the coarse location as an extra, if not already present - Location coarse = getCoarseLocationExtra(location); - if (coarse == null) { - coarse = addCoarseLocationExtra(location); - } + // Add the coarse location as an extra + Location coarse = mLocationFudger.getOrCreate(location); // Update last known locations Location lastLocation = mLastLocation.get(provider); @@ -1660,106 +1661,6 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs } } - private static double wrapLatitude(double lat) { - if (lat > MAX_LATITUDE) lat = MAX_LATITUDE; - if (lat < -MAX_LATITUDE) lat = -MAX_LATITUDE; - return lat; - } - - private static double wrapLongitude(double lon) { - if (lon >= 180.0) lon -= 360.0; - if (lon < -180.0) lon += 360.0; - return lon; - } - - private static double distanceToDegreesLatitude(double distance) { - return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR; - } - - /** - * Requires latitude since longitudinal distances change with distance from equator. - */ - private static double distanceToDegreesLongitude(double distance, double lat) { - return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(lat); - } - - /** - * Fudge a location into a coarse location. - *

Add a random offset, then quantize the result (snap-to-grid). - * Random offsets alone can be low-passed pretty easily. - * Snap-to-grid on its own is excellent unless you are sitting on a - * grid boundary and bouncing between quantizations. - * The combination is quite hard to reverse engineer. - *

The random offset used is smaller than the goal accuracy - * ({@link #COARSE_ACCURACY_M}), in order to give relatively stable - * results after quantization. - */ - private static Location createCoarse(Location fine) { - Location coarse = new Location(fine); - - coarse.removeBearing(); - coarse.removeSpeed(); - coarse.removeAltitude(); - - double lat = coarse.getLatitude(); - double lon = coarse.getLongitude(); - - // wrap - lat = wrapLatitude(lat); - lon = wrapLongitude(lon); - - if (coarse.getAccuracy() < COARSE_ACCURACY_M / 2) { - // apply a random offset - double fudgeDistance = COARSE_ACCURACY_M / 2.0 - coarse.getAccuracy(); - lat += (Math.random() - 0.5) * distanceToDegreesLatitude(fudgeDistance); - lon += (Math.random() - 0.5) * distanceToDegreesLongitude(fudgeDistance, lat); - } - - // wrap - lat = wrapLatitude(lat); - lon = wrapLongitude(lon); - - // quantize (snap-to-grid) - double latGranularity = distanceToDegreesLatitude(COARSE_ACCURACY_M); - double lonGranularity = distanceToDegreesLongitude(COARSE_ACCURACY_M, lat); - long latQuantized = Math.round(lat / latGranularity); - long lonQuantized = Math.round(lon / lonGranularity); - lat = latQuantized * latGranularity; - lon = lonQuantized * lonGranularity; - - // wrap again - lat = wrapLatitude(lat); - lon = wrapLongitude(lon); - - // apply - coarse.setLatitude(lat); - coarse.setLongitude(lon); - coarse.setAccuracy((float)COARSE_ACCURACY_M); - - return coarse; - } - - - private static Location getCoarseLocationExtra(Location location) { - Bundle extras = location.getExtras(); - if (extras == null) return null; - Parcelable parcel = extras.getParcelable(EXTRA_COARSE_LOCATION); - if (parcel == null) return null; - if (!(parcel instanceof Location)) return null; - Location coarse = (Location) parcel; - if (coarse.getAccuracy() < COARSE_ACCURACY_M) return null; - return coarse; - } - - private static Location addCoarseLocationExtra(Location location) { - Bundle extras = location.getExtras(); - if (extras == null) extras = new Bundle(); - Location coarse = createCoarse(location); - extras.putParcelable(EXTRA_COARSE_LOCATION, coarse); - location.setExtras(extras); - return coarse; - } - private void log(String log) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Slog.d(TAG, log); @@ -1819,6 +1720,9 @@ public class LocationManagerService extends ILocationManager.Stub implements Obs } } + pw.append(" fudger: "); + mLocationFudger.dump(fd, pw, args); + if (args.length > 0 && "short".equals(args[0])) { return; } diff --git a/services/java/com/android/server/location/LocationFudger.java b/services/java/com/android/server/location/LocationFudger.java new file mode 100644 index 0000000000000..57bc1c5a6dbb1 --- /dev/null +++ b/services/java/com/android/server/location/LocationFudger.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.security.SecureRandom; +import android.location.Location; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.Log; + + +/** + * Contains the logic to obfuscate (fudge) locations for coarse applications. + * + *

The goal is just to prevent applications with only + * the coarse location permission from receiving a fine location. + */ +public class LocationFudger { + private static final boolean D = false; + private static final String TAG = "LocationFudge"; + + private static final String EXTRA_COARSE_LOCATION = "coarseLocation"; + + /** + * This is the main control: Best location accuracy allowed for coarse applications. + */ + private static final float ACCURACY_METERS = 200.0f; + + /** + * The distance between grids for snap-to-grid. See {@link #createCoarse}. + */ + private static final double GRID_SIZE_METERS = ACCURACY_METERS; + + /** + * Standard deviation of the (normally distributed) random offset applied + * to coarse locations. It does not need to be as large as + * {@link #COARSE_ACCURACY_METERS} because snap-to-grid is the primary obfuscation + * method. See further details in the implementation. + */ + private static final double STANDARD_DEVIATION_METERS = GRID_SIZE_METERS / 4.0; + + /** + * This is the fastest interval that applications can receive coarse + * locations. + */ + public static final long FASTEST_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + + /** + * The duration until we change the random offset. + */ + private static final long CHANGE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour + + /** + * The percentage that we change the random offset at every interval. + * + *

0.0 indicates the random offset doesn't change. 1.0 + * indicates the random offset is completely replaced every interval. + */ + private static final double CHANGE_PER_INTERVAL = 0.03; // 3% change + + // Pre-calculated weights used to move the random offset. + // + // The goal is to iterate on the previous offset, but keep + // the resulting standard deviation the same. The variance of + // two gaussian distributions summed together is equal to the + // sum of the variance of each distribution. So some quick + // algebra results in the following sqrt calculation to + // weigh in a new offset while keeping the final standard + // deviation unchanged. + private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL; + private static final double PREVIOUS_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT); + + /** + * This number actually varies because the earth is not round, but + * 111,000 meters is considered generally acceptable. + */ + private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111000; + + /** + * Maximum latitude. + * + *

We pick a value 1 meter away from 90.0 degrees in order + * to keep cosine(MAX_LATITUDE) to a non-zero value, so that we avoid + * divide by zero fails. + */ + private static final double MAX_LATITUDE = 90.0 - + (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR); + + private final Object mLock = new Object(); + private final SecureRandom mRandom = new SecureRandom(); + + // all fields below protected by mLock + private double mOffsetLatitudeMeters; + private double mOffsetLongitudeMeters; + private long mNextInterval; + + public LocationFudger() { + mOffsetLatitudeMeters = nextOffset(); + mOffsetLongitudeMeters = nextOffset(); + mNextInterval = SystemClock.elapsedRealtime() + CHANGE_INTERVAL_MS; + } + + /** + * Get the cached coarse location, or generate a new one and cache it. + */ + public Location getOrCreate(Location location) { + Bundle extras = location.getExtras(); + if (extras == null) { + return addCoarseLocationExtra(location); + } + Parcelable parcel = extras.getParcelable(EXTRA_COARSE_LOCATION); + if (parcel == null) { + return addCoarseLocationExtra(location); + } + if (!(parcel instanceof Location)) { + return addCoarseLocationExtra(location); + } + Location coarse = (Location) parcel; + if (coarse.getAccuracy() < ACCURACY_METERS) { + return addCoarseLocationExtra(location); + } + return coarse; + } + + private Location addCoarseLocationExtra(Location location) { + Bundle extras = location.getExtras(); + if (extras == null) extras = new Bundle(); + Location coarse = createCoarse(location); + extras.putParcelable(EXTRA_COARSE_LOCATION, coarse); + location.setExtras(extras); + return coarse; + } + + /** + * Create a coarse location. + * + *

Two techniques are used: random offsets and snap-to-grid. + * + *

First we add a random offset. This mitigates against detecting + * grid transitions. Without a random offset it is possible to detect + * a users position very accurately when they cross a grid boundary. + * The random offset changes very slowly over time, to mitigate against + * taking many location samples and averaging them out. + * + *

Second we snap-to-grid (quantize). This has the nice property of + * producing stable results, and mitigating against taking many samples + * to average out a random offset. + */ + private Location createCoarse(Location fine) { + Location coarse = new Location(fine); + + // clean all the optional information off the location, because + // this can leak detailed location information + coarse.removeBearing(); + coarse.removeSpeed(); + coarse.removeAltitude(); + coarse.setExtras(null); + + double lat = coarse.getLatitude(); + double lon = coarse.getLongitude(); + + // wrap + lat = wrapLatitude(lat); + lon = wrapLongitude(lon); + + // Step 1) apply a random offset + // + // The goal of the random offset is to prevent the application + // from determining that the device is on a grid boundary + // when it crosses from one grid to the next. + // + // We apply the offset even if the location already claims to be + // inaccurate, because it may be more accurate than claimed. + synchronized (mLock) { + updateRandomOffsetLocked(); + // perform lon first whilst lat is still within bounds + lon += metersToDegreesLongitude(mOffsetLongitudeMeters, lat); + lat += metersToDegreesLatitude(mOffsetLatitudeMeters); + if (D) Log.d(TAG, String.format("applied offset of %.0f, %.0f (meters)", + mOffsetLongitudeMeters, mOffsetLatitudeMeters)); + } + + // wrap + lat = wrapLatitude(lat); + lon = wrapLongitude(lon); + + // Step 2) Snap-to-grid (quantize) + // + // This is the primary means of obfuscation. It gives nice consistent + // results and is very effective at hiding the true location + // (as long as you are not sitting on a grid boundary, which + // step 1 mitigates). + // + // Note we quantize the latitude first, since the longitude + // quantization depends on the latitude value and so leaks information + // about the latitude + double latGranularity = metersToDegreesLatitude(GRID_SIZE_METERS); + lat = Math.round(lat / latGranularity) * latGranularity; + double lonGranularity = metersToDegreesLongitude(GRID_SIZE_METERS, lat); + lon = Math.round(lon / lonGranularity) * lonGranularity; + + // wrap again + lat = wrapLatitude(lat); + lon = wrapLongitude(lon); + + // apply + coarse.setLatitude(lat); + coarse.setLongitude(lon); + coarse.setAccuracy(Math.max(ACCURACY_METERS, coarse.getAccuracy())); + + if (D) Log.d(TAG, "fudged " + fine + " to " + coarse); + return coarse; + } + + /** + * Update the random offset over time. + * + *

If the random offset was new for every location + * fix then an application can more easily average location results + * over time, + * especially when the location is near a grid boundary. On the + * other hand if the random offset is constant then if an application + * found a way to reverse engineer the offset they would be able + * to detect location at grid boundaries very accurately. So + * we choose a random offset and then very slowly move it, to + * make both approaches very hard. + * + *

The random offset does not need to be large, because snap-to-grid + * is the primary obfuscation mechanism. It just needs to be large + * enough to stop information leakage as we cross grid boundaries. + */ + private void updateRandomOffsetLocked() { + long now = SystemClock.elapsedRealtime(); + if (now < mNextInterval) { + return; + } + + if (D) Log.d(TAG, String.format("old offset: %.0f, %.0f (meters)", + mOffsetLongitudeMeters, mOffsetLatitudeMeters)); + + // ok, need to update the random offset + mNextInterval = now + CHANGE_INTERVAL_MS; + + mOffsetLatitudeMeters *= PREVIOUS_WEIGHT; + mOffsetLatitudeMeters += NEW_WEIGHT * nextOffset(); + mOffsetLongitudeMeters *= PREVIOUS_WEIGHT; + mOffsetLongitudeMeters += NEW_WEIGHT * nextOffset(); + + if (D) Log.d(TAG, String.format("new offset: %.0f, %.0f (meters)", + mOffsetLongitudeMeters, mOffsetLatitudeMeters)); + } + + private double nextOffset() { + return mRandom.nextGaussian() * STANDARD_DEVIATION_METERS; + } + + private static double wrapLatitude(double lat) { + if (lat > MAX_LATITUDE) { + lat = MAX_LATITUDE; + } + if (lat < -MAX_LATITUDE) { + lat = -MAX_LATITUDE; + } + return lat; + } + + private static double wrapLongitude(double lon) { + lon %= 360.0; // wraps into range (-360.0, +360.0) + if (lon >= 180.0) { + lon -= 360.0; + } + if (lon < -180.0) { + lon += 360.0; + } + return lon; + } + + private static double metersToDegreesLatitude(double distance) { + return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR; + } + + /** + * Requires latitude since longitudinal distances change with distance from equator. + */ + private static double metersToDegreesLongitude(double distance, double lat) { + return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat)); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(String.format("offset: %.0f, %.0f (meters)", mOffsetLongitudeMeters, + mOffsetLatitudeMeters)); + } +}