1 /* 2 * Copyright (C) 2020 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.fudger; 18 19 import static com.android.internal.location.geometry.S2CellIdUtils.LAT_INDEX; 20 import static com.android.internal.location.geometry.S2CellIdUtils.LNG_INDEX; 21 22 import android.annotation.FlaggedApi; 23 import android.annotation.Nullable; 24 import android.location.Location; 25 import android.location.LocationResult; 26 import android.location.flags.Flags; 27 import android.os.SystemClock; 28 29 import com.android.internal.annotations.GuardedBy; 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.internal.location.geometry.S2CellIdUtils; 32 33 import java.security.SecureRandom; 34 import java.time.Clock; 35 import java.util.Random; 36 37 /** 38 * Contains the logic to obfuscate (fudge) locations for coarse applications. The goal is just to 39 * prevent applications with only the coarse location permission from receiving a fine location. 40 */ 41 public class LocationFudger { 42 43 // minimum accuracy a coarsened location can have 44 private static final float MIN_ACCURACY_M = 200.0f; 45 46 // how often random offsets are updated 47 @VisibleForTesting 48 static final long OFFSET_UPDATE_INTERVAL_MS = 60 * 60 * 1000; 49 50 // the percentage that we change the random offset at every interval. 0.0 indicates the random 51 // offset doesn't change. 1.0 indicates the random offset is completely replaced every interval 52 private static final double CHANGE_PER_INTERVAL = 0.03; // 3% change 53 54 // weights used to move the random offset. the goal is to iterate on the previous offset, but 55 // keep the resulting standard deviation the same. the variance of two gaussian distributions 56 // summed together is equal to the sum of the variance of each distribution. so some quick 57 // algebra results in the following sqrt calculation to weight in a new offset while keeping the 58 // final standard deviation unchanged. 59 private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL; 60 private static final double OLD_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT); 61 62 // this number actually varies because the earth is not round, but 111,000 meters is considered 63 // generally acceptable 64 private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111_000; 65 66 // we pick a value 1 meter away from 90.0 degrees in order to keep cosine(MAX_LATITUDE) to a 67 // non-zero value, so that we avoid divide by zero errors 68 private static final double MAX_LATITUDE = 69 90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR); 70 71 // The average edge length in km of an S2 cell, indexed by S2 levels 0 to 72 // 13. Level 13 is the highest level used for coarsening. 73 // This approximation assumes the S2 cells are squares. 74 // For density-based coarsening, we use the edge to set the accuracy of the 75 // coarsened location. 76 // The values are from http://s2geometry.io/resources/s2cell_statistics.html 77 // We take square root of the average area. 78 private static final float[] S2_CELL_AVG_EDGE_PER_LEVEL = new float[] { 79 9220.14f, 4610.07f, 2305.04f, 1152.52f, 576.26f, 288.13f, 144.06f, 80 72.03f, 36.02f, 20.79f, 9f, 5.05f, 2.25f, 1.13f, 0.57f}; 81 82 private final float mAccuracyM; 83 private final Clock mClock; 84 private final Random mRandom; 85 86 @GuardedBy("this") 87 private double mLatitudeOffsetM; 88 @GuardedBy("this") 89 private double mLongitudeOffsetM; 90 @GuardedBy("this") 91 private long mNextUpdateRealtimeMs; 92 93 @GuardedBy("this") 94 @Nullable private Location mCachedFineLocation; 95 @GuardedBy("this") 96 @Nullable private Location mCachedCoarseLocation; 97 98 @GuardedBy("this") 99 @Nullable private LocationResult mCachedFineLocationResult; 100 @GuardedBy("this") 101 @Nullable private LocationResult mCachedCoarseLocationResult; 102 103 @GuardedBy("this") 104 @Nullable private LocationFudgerCache mLocationFudgerCache = null; 105 LocationFudger(float accuracyM)106 public LocationFudger(float accuracyM) { 107 this(accuracyM, SystemClock.elapsedRealtimeClock(), new SecureRandom()); 108 } 109 110 @VisibleForTesting LocationFudger(float accuracyM, Clock clock, Random random)111 LocationFudger(float accuracyM, Clock clock, Random random) { 112 mClock = clock; 113 mRandom = random; 114 mAccuracyM = Math.max(accuracyM, MIN_ACCURACY_M); 115 116 resetOffsets(); 117 } 118 119 /** 120 * Provides the optional {@link LocationFudgerCache} for coarsening based on population density. 121 */ 122 @FlaggedApi(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS) setLocationFudgerCache(LocationFudgerCache cache)123 public void setLocationFudgerCache(LocationFudgerCache cache) { 124 synchronized (this) { 125 mLocationFudgerCache = cache; 126 } 127 } 128 129 /** 130 * Resets the random offsets completely. 131 */ resetOffsets()132 public void resetOffsets() { 133 mLatitudeOffsetM = nextRandomOffset(); 134 mLongitudeOffsetM = nextRandomOffset(); 135 mNextUpdateRealtimeMs = mClock.millis() + OFFSET_UPDATE_INTERVAL_MS; 136 } 137 138 /** 139 * Coarsens a LocationResult by coarsening every location within the location result with 140 * {@link #createCoarse(Location)}. 141 */ createCoarse(LocationResult fineLocationResult)142 public LocationResult createCoarse(LocationResult fineLocationResult) { 143 synchronized (this) { 144 if (fineLocationResult == mCachedFineLocationResult 145 || fineLocationResult == mCachedCoarseLocationResult) { 146 return mCachedCoarseLocationResult; 147 } 148 } 149 150 LocationResult coarseLocationResult = fineLocationResult.map(this::createCoarse); 151 152 synchronized (this) { 153 mCachedFineLocationResult = fineLocationResult; 154 mCachedCoarseLocationResult = coarseLocationResult; 155 } 156 157 return coarseLocationResult; 158 } 159 160 /** 161 * Create a coarse location using two technique, random offsets and snap-to-grid. 162 * 163 * First we add a random offset to mitigate against detecting grid transitions. Without a random 164 * offset it is possible to detect a user's position quite accurately when they cross a grid 165 * boundary. The random offset changes very slowly over time, to mitigate against taking many 166 * location samples and averaging them out. Second we snap-to-grid (quantize). This has the nice 167 * property of producing stable results, and mitigating against taking many samples to average 168 * out a random offset. 169 */ createCoarse(Location fine)170 public Location createCoarse(Location fine) { 171 synchronized (this) { 172 if (fine == mCachedFineLocation || fine == mCachedCoarseLocation) { 173 return mCachedCoarseLocation; 174 } 175 } 176 177 // update the offsets in use 178 updateOffsets(); 179 180 Location coarse = new Location(fine); 181 182 // clear any fields that could leak more detailed location information 183 coarse.removeBearing(); 184 coarse.removeSpeed(); 185 coarse.removeAltitude(); 186 coarse.setExtras(null); 187 188 double latitude = wrapLatitude(coarse.getLatitude()); 189 double longitude = wrapLongitude(coarse.getLongitude()); 190 191 // add offsets - update longitude first using the non-offset latitude 192 longitude += wrapLongitude(metersToDegreesLongitude(mLongitudeOffsetM, latitude)); 193 latitude += wrapLatitude(metersToDegreesLatitude(mLatitudeOffsetM)); 194 195 // We copy a reference to the cache, so even if mLocationFudgerCache is concurrently set 196 // to null, we can continue executing the condition below. 197 LocationFudgerCache cacheCopy = null; 198 synchronized (this) { 199 cacheCopy = mLocationFudgerCache; 200 } 201 double[] coarsened = new double[] {0.0, 0.0}; 202 // TODO(b/381204398): To ensure a safe rollout, two algorithms co-exist. The first is the 203 // new density-based algorithm, while the second is the traditional coarsening algorithm. 204 // Once rollout is done, clean up the unused algorithm. 205 // The new algorithm is applied if and only if (1) the flag is on, (2) the cache has been 206 // set, and (3) the cache has successfully queried the provider for the default coarsening 207 // value. 208 float accuracy = mAccuracyM; 209 if (Flags.populationDensityProvider() && Flags.densityBasedCoarseLocations() 210 && cacheCopy != null) { 211 if (cacheCopy.hasDefaultValue()) { 212 // New algorithm that snaps to the center of a S2 cell. 213 int level = cacheCopy.getCoarseningLevel(latitude, longitude); 214 coarsened = snapToCenterOfS2Cell(latitude, longitude, level); 215 accuracy = getS2CellApproximateEdge(level); 216 } else { 217 // Try to fetch the default value. The answer won't come in time, but will be used 218 // for the next location to coarsen. 219 cacheCopy.onDefaultCoarseningLevelNotSet(); 220 // Previous algorithm that snaps to a grid of width mAccuracyM. 221 coarsened = snapToGrid(latitude, longitude); 222 } 223 } else { 224 // Previous algorithm that snaps to a grid of width mAccuracyM. 225 coarsened = snapToGrid(latitude, longitude); 226 } 227 228 coarse.setLatitude(coarsened[LAT_INDEX]); 229 coarse.setLongitude(coarsened[LNG_INDEX]); 230 coarse.setAccuracy(Math.max(accuracy, coarse.getAccuracy())); 231 232 synchronized (this) { 233 mCachedFineLocation = fine; 234 mCachedCoarseLocation = coarse; 235 } 236 237 return coarse; 238 } 239 240 // Returns the average edge length in meters of an S2 cell at the given 241 // level. This is computed as if the S2 cell were a square. We do not need 242 // an exact value, only a rough approximation. 243 @VisibleForTesting getS2CellApproximateEdge(int level)244 protected float getS2CellApproximateEdge(int level) { 245 if (level < 0) { 246 level = 0; 247 } else if (level >= S2_CELL_AVG_EDGE_PER_LEVEL.length) { 248 level = S2_CELL_AVG_EDGE_PER_LEVEL.length - 1; 249 } 250 return S2_CELL_AVG_EDGE_PER_LEVEL[level] * 1000; 251 } 252 253 // quantize location by snapping to a grid. this is the primary means of obfuscation. it 254 // gives nice consistent results and is very effective at hiding the true location (as 255 // long as you are not sitting on a grid boundary, which the random offsets mitigate). 256 // 257 // note that we quantize the latitude first, since the longitude quantization depends on 258 // the latitude value and so leaks information about the latitude snapToGrid(double latitude, double longitude)259 private double[] snapToGrid(double latitude, double longitude) { 260 double[] center = new double[] {0.0, 0.0}; 261 double latGranularity = metersToDegreesLatitude(mAccuracyM); 262 center[LAT_INDEX] = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity); 263 double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude); 264 center[LNG_INDEX] = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity); 265 return center; 266 } 267 268 @VisibleForTesting snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level)269 protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) { 270 long leafCell = S2CellIdUtils.fromLatLngDegrees(latDegrees, lngDegrees); 271 long coarsenedCell = S2CellIdUtils.getParent(leafCell, level); 272 double[] center = new double[] {0.0, 0.0}; 273 S2CellIdUtils.toLatLngDegrees(coarsenedCell, center); 274 return center; 275 } 276 277 /** 278 * Update the random offsets over time. 279 * 280 * If the random offset was reset for every location fix then an application could more easily 281 * average location results over time, especially when the location is near a grid boundary. On 282 * the other hand if the random offset is constant then if an application finds a way to reverse 283 * engineer the offset they would be able to detect location at grid boundaries very accurately. 284 * So we choose a random offset and then very slowly move it, to make both approaches very hard. 285 * The random offset does not need to be large, because snap-to-grid is the primary obfuscation 286 * mechanism. It just needs to be large enough to stop information leakage as we cross grid 287 * boundaries. 288 */ updateOffsets()289 private synchronized void updateOffsets() { 290 long now = mClock.millis(); 291 if (now < mNextUpdateRealtimeMs) { 292 return; 293 } 294 295 mLatitudeOffsetM = (OLD_WEIGHT * mLatitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset()); 296 mLongitudeOffsetM = (OLD_WEIGHT * mLongitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset()); 297 mNextUpdateRealtimeMs = now + OFFSET_UPDATE_INTERVAL_MS; 298 } 299 nextRandomOffset()300 private double nextRandomOffset() { 301 return mRandom.nextGaussian() * (mAccuracyM / 4.0); 302 } 303 wrapLatitude(double lat)304 private static double wrapLatitude(double lat) { 305 if (lat > MAX_LATITUDE) { 306 lat = MAX_LATITUDE; 307 } 308 if (lat < -MAX_LATITUDE) { 309 lat = -MAX_LATITUDE; 310 } 311 return lat; 312 } 313 wrapLongitude(double lon)314 private static double wrapLongitude(double lon) { 315 lon %= 360.0; // wraps into range (-360.0, +360.0) 316 if (lon >= 180.0) { 317 lon -= 360.0; 318 } 319 if (lon < -180.0) { 320 lon += 360.0; 321 } 322 return lon; 323 } 324 metersToDegreesLatitude(double distance)325 private static double metersToDegreesLatitude(double distance) { 326 return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR; 327 } 328 329 // requires latitude since longitudinal distances change with distance from equator. metersToDegreesLongitude(double distance, double lat)330 private static double metersToDegreesLongitude(double distance, double lat) { 331 // Needed to convert from longitude distance to longitude degree. 332 // X meters near the poles is more degrees than at the equator. 333 double cosLat = Math.cos(Math.toRadians(lat)); 334 // If we are right on top of the pole, the degree is always 0. 335 // We return a very small value instead to avoid divide by zero errors 336 // later on. 337 if (cosLat == 0.0) { 338 return 0.0001; 339 } 340 return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat; 341 } 342 } 343