Home | History | Annotate | Download | only in location
      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