LocationFudgerpublic class LocationFudger extends Object 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. |
Fields Summary |
---|
private static final boolean | D | private static final String | TAG | private static final float | DEFAULT_ACCURACY_IN_METERSDefault coarse accuracy in meters. | private static final float | MINIMUM_ACCURACY_IN_METERSMinimum coarse accuracy in meters. | private static final String | COARSE_ACCURACY_CONFIG_NAMESecure settings key for coarse accuracy. | public static final long | FASTEST_INTERVAL_MSThis is the fastest interval that applications can receive coarse
locations. | private static final long | CHANGE_INTERVAL_MSThe duration until we change the random offset. | private static final double | CHANGE_PER_INTERVALThe 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 | NEW_WEIGHT | private static final double | PREVIOUS_WEIGHT | private static final int | APPROXIMATE_METERS_PER_DEGREE_AT_EQUATORThis number actually varies because the earth is not round, but
111,000 meters is considered generally acceptable. | private static final double | MAX_LATITUDEMaximum 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 final Object | mLock | private final SecureRandom | mRandom | private final android.database.ContentObserver | mSettingsObserverUsed to monitor coarse accuracy secure setting for changes. | private final android.content.Context | mContextUsed to resolve coarse accuracy setting. | private double | mOffsetLatitudeMeters | private double | mOffsetLongitudeMeters | private long | mNextInterval | private float | mAccuracyInMetersBest location accuracy allowed for coarse applications.
This value should only be set by {@link #setAccuracyInMetersLocked(float)}. | private double | mGridSizeInMetersThe distance between grids for snap-to-grid. See {@link #createCoarse}.
This value should only be set by {@link #setAccuracyInMetersLocked(float)}. | private double | mStandardDeviationInMetersStandard 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.
This value should only be set by {@link #setAccuracyInMetersLocked(float)}. |
Constructors Summary |
---|
public LocationFudger(android.content.Context context, android.os.Handler handler)
mContext = context;
mSettingsObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
setAccuracyInMeters(loadCoarseAccuracy());
}
};
mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
COARSE_ACCURACY_CONFIG_NAME), false, mSettingsObserver);
float accuracy = loadCoarseAccuracy();
synchronized (mLock) {
setAccuracyInMetersLocked(accuracy);
mOffsetLatitudeMeters = nextOffsetLocked();
mOffsetLongitudeMeters = nextOffsetLocked();
mNextInterval = SystemClock.elapsedRealtime() + CHANGE_INTERVAL_MS;
}
|
Methods Summary |
---|
private android.location.Location | addCoarseLocationExtraLocked(android.location.Location location)
Location coarse = createCoarseLocked(location);
location.setExtraLocation(Location.EXTRA_COARSE_LOCATION, coarse);
return coarse;
| private android.location.Location | createCoarseLocked(android.location.Location fine)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.
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.
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(mGridSizeInMeters);
lat = Math.round(lat / latGranularity) * latGranularity;
double lonGranularity = metersToDegreesLongitude(mGridSizeInMeters, 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(mAccuracyInMeters, coarse.getAccuracy()));
if (D) Log.d(TAG, "fudged " + fine + " to " + coarse);
return coarse;
| public void | dump(java.io.FileDescriptor fd, java.io.PrintWriter pw, java.lang.String[] args)
pw.println(String.format("offset: %.0f, %.0f (meters)", mOffsetLongitudeMeters,
mOffsetLatitudeMeters));
| public android.location.Location | getOrCreate(android.location.Location location)Get the cached coarse location, or generate a new one and cache it.
synchronized (mLock) {
Location coarse = location.getExtraLocation(Location.EXTRA_COARSE_LOCATION);
if (coarse == null) {
return addCoarseLocationExtraLocked(location);
}
if (coarse.getAccuracy() < mAccuracyInMeters) {
return addCoarseLocationExtraLocked(location);
}
return coarse;
}
| private float | loadCoarseAccuracy()Loads the coarse accuracy value from secure settings.
String newSetting = Settings.Secure.getString(mContext.getContentResolver(),
COARSE_ACCURACY_CONFIG_NAME);
if (D) {
Log.d(TAG, "loadCoarseAccuracy: newSetting = \"" + newSetting + "\"");
}
if (newSetting == null) {
return DEFAULT_ACCURACY_IN_METERS;
}
try {
return Float.parseFloat(newSetting);
} catch (NumberFormatException e) {
return DEFAULT_ACCURACY_IN_METERS;
}
| private static double | metersToDegreesLatitude(double distance)
return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR;
| private static double | metersToDegreesLongitude(double distance, double lat)Requires latitude since longitudinal distances change with distance from equator.
return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat));
| private double | nextOffsetLocked()
return mRandom.nextGaussian() * mStandardDeviationInMeters;
| private void | setAccuracyInMeters(float accuracyInMeters)Same as setAccuracyInMetersLocked without the pre-lock requirement.
synchronized (mLock) {
setAccuracyInMetersLocked(accuracyInMeters);
}
| private void | setAccuracyInMetersLocked(float accuracyInMeters)This is the main control: call this to set the best location accuracy
allowed for coarse applications and all derived values.
mAccuracyInMeters = Math.max(accuracyInMeters, MINIMUM_ACCURACY_IN_METERS);
if (D) {
Log.d(TAG, "setAccuracyInMetersLocked: new accuracy = " + mAccuracyInMeters);
}
mGridSizeInMeters = mAccuracyInMeters;
mStandardDeviationInMeters = mGridSizeInMeters / 4.0;
| private void | updateRandomOffsetLocked()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.
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 * nextOffsetLocked();
mOffsetLongitudeMeters *= PREVIOUS_WEIGHT;
mOffsetLongitudeMeters += NEW_WEIGHT * nextOffsetLocked();
if (D) Log.d(TAG, String.format("new offset: %.0f, %.0f (meters)",
mOffsetLongitudeMeters, mOffsetLatitudeMeters));
| 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;
|
|