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 android.telephony; 18 19 import android.Manifest; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.UserIdInt; 23 import android.app.ActivityManager; 24 import android.app.AppOpsManager; 25 import android.content.Context; 26 import android.content.pm.PackageManager; 27 import android.location.LocationManager; 28 import android.os.Binder; 29 import android.os.Build; 30 import android.os.Process; 31 import android.os.UserHandle; 32 import android.util.Log; 33 import android.widget.Toast; 34 35 import com.android.internal.telephony.util.TelephonyUtils; 36 37 /** 38 * Helper for performing location access checks. 39 * @hide 40 */ 41 public final class LocationAccessPolicy { 42 private static final String TAG = "LocationAccessPolicy"; 43 private static final boolean DBG = false; 44 public static final int MAX_SDK_FOR_ANY_ENFORCEMENT = Build.VERSION_CODES.CUR_DEVELOPMENT; 45 46 public enum LocationPermissionResult { 47 ALLOWED, 48 /** 49 * Indicates that the denial is due to a transient device state 50 * (e.g. app-ops, location main switch) 51 */ 52 DENIED_SOFT, 53 /** 54 * Indicates that the denial is due to a misconfigured app (e.g. missing entry in manifest) 55 */ 56 DENIED_HARD, 57 } 58 59 /** Data structure for location permission query */ 60 public static class LocationPermissionQuery { 61 public final String callingPackage; 62 public final String callingFeatureId; 63 public final int callingUid; 64 public final int callingPid; 65 public final int minSdkVersionForCoarse; 66 public final int minSdkVersionForFine; 67 public final boolean logAsInfo; 68 public final String method; 69 LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId, int callingUid, int callingPid, int minSdkVersionForCoarse, int minSdkVersionForFine, boolean logAsInfo, String method)70 private LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId, 71 int callingUid, int callingPid, int minSdkVersionForCoarse, 72 int minSdkVersionForFine, boolean logAsInfo, String method) { 73 this.callingPackage = callingPackage; 74 this.callingFeatureId = callingFeatureId; 75 this.callingUid = callingUid; 76 this.callingPid = callingPid; 77 this.minSdkVersionForCoarse = minSdkVersionForCoarse; 78 this.minSdkVersionForFine = minSdkVersionForFine; 79 this.logAsInfo = logAsInfo; 80 this.method = method; 81 } 82 83 /** Builder for LocationPermissionQuery */ 84 public static class Builder { 85 private String mCallingPackage; 86 private String mCallingFeatureId; 87 private int mCallingUid; 88 private int mCallingPid; 89 private int mMinSdkVersionForCoarse = -1; 90 private int mMinSdkVersionForFine = -1; 91 private int mMinSdkVersionForEnforcement = -1; 92 private boolean mLogAsInfo = false; 93 private String mMethod; 94 95 /** 96 * Mandatory parameter, used for performing permission checks. 97 */ setCallingPackage(String callingPackage)98 public Builder setCallingPackage(String callingPackage) { 99 mCallingPackage = callingPackage; 100 return this; 101 } 102 103 /** 104 * Mandatory parameter, used for performing permission checks. 105 */ setCallingFeatureId(@ullable String callingFeatureId)106 public Builder setCallingFeatureId(@Nullable String callingFeatureId) { 107 mCallingFeatureId = callingFeatureId; 108 return this; 109 } 110 111 /** 112 * Mandatory parameter, used for performing permission checks. 113 */ setCallingUid(int callingUid)114 public Builder setCallingUid(int callingUid) { 115 mCallingUid = callingUid; 116 return this; 117 } 118 119 /** 120 * Mandatory parameter, used for performing permission checks. 121 */ setCallingPid(int callingPid)122 public Builder setCallingPid(int callingPid) { 123 mCallingPid = callingPid; 124 return this; 125 } 126 127 /** 128 * Apps that target at least this sdk version will be checked for coarse location 129 * permission. This method MUST be called before calling {@link #build()}. Otherwise, an 130 * {@link IllegalArgumentException} will be thrown. 131 * 132 * Additionally, if both the argument to this method and 133 * {@link #setMinSdkVersionForFine} are greater than {@link Build.VERSION_CODES#BASE}, 134 * you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to 135 * affirm that you do not want any location checks below a certain SDK version. 136 * Otherwise, {@link #build} will throw an {@link IllegalArgumentException}. 137 */ setMinSdkVersionForCoarse( int minSdkVersionForCoarse)138 public Builder setMinSdkVersionForCoarse( 139 int minSdkVersionForCoarse) { 140 mMinSdkVersionForCoarse = minSdkVersionForCoarse; 141 return this; 142 } 143 144 /** 145 * Apps that target at least this sdk version will be checked for fine location 146 * permission. This method MUST be called before calling {@link #build()}. 147 * Otherwise, an {@link IllegalArgumentException} will be thrown. 148 * 149 * Additionally, if both the argument to this method and 150 * {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE}, 151 * you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to 152 * affirm that you do not want any location checks below a certain SDK version. 153 * Otherwise, {@link #build} will throw an {@link IllegalArgumentException}. 154 */ setMinSdkVersionForFine( int minSdkVersionForFine)155 public Builder setMinSdkVersionForFine( 156 int minSdkVersionForFine) { 157 mMinSdkVersionForFine = minSdkVersionForFine; 158 return this; 159 } 160 161 /** 162 * If both the argument to {@link #setMinSdkVersionForFine} and 163 * {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE}, 164 * this method must be called with the min of the two to 165 * affirm that you do not want any location checks below a certain SDK version. 166 */ setMinSdkVersionForEnforcement(int minSdkVersionForEnforcement)167 public Builder setMinSdkVersionForEnforcement(int minSdkVersionForEnforcement) { 168 mMinSdkVersionForEnforcement = minSdkVersionForEnforcement; 169 return this; 170 } 171 172 /** 173 * Optional, for logging purposes only. 174 */ setMethod(String method)175 public Builder setMethod(String method) { 176 mMethod = method; 177 return this; 178 } 179 180 /** 181 * If called with {@code true}, log messages will only be printed at the info level. 182 */ setLogAsInfo(boolean logAsInfo)183 public Builder setLogAsInfo(boolean logAsInfo) { 184 mLogAsInfo = logAsInfo; 185 return this; 186 } 187 188 /** build LocationPermissionQuery */ build()189 public LocationPermissionQuery build() { 190 if (mMinSdkVersionForCoarse < 0 || mMinSdkVersionForFine < 0) { 191 throw new IllegalArgumentException("Must specify min sdk versions for" 192 + " enforcement for both coarse and fine permissions"); 193 } 194 if (mMinSdkVersionForFine > Build.VERSION_CODES.BASE 195 && mMinSdkVersionForCoarse > Build.VERSION_CODES.BASE) { 196 if (mMinSdkVersionForEnforcement != Math.min( 197 mMinSdkVersionForCoarse, mMinSdkVersionForFine)) { 198 throw new IllegalArgumentException("setMinSdkVersionForEnforcement must be" 199 + " called."); 200 } 201 } 202 203 if (mMinSdkVersionForFine < mMinSdkVersionForCoarse) { 204 throw new IllegalArgumentException("Since fine location permission includes" 205 + " access to coarse location, the min sdk level for enforcement of" 206 + " the fine location permission must not be less than the min sdk" 207 + " level for enforcement of the coarse location permission."); 208 } 209 210 return new LocationPermissionQuery(mCallingPackage, mCallingFeatureId, 211 mCallingUid, mCallingPid, mMinSdkVersionForCoarse, mMinSdkVersionForFine, 212 mLogAsInfo, mMethod); 213 } 214 } 215 } 216 logError(Context context, LocationPermissionQuery query, String errorMsg)217 private static void logError(Context context, LocationPermissionQuery query, String errorMsg) { 218 if (query.logAsInfo) { 219 Log.i(TAG, errorMsg); 220 return; 221 } 222 Log.e(TAG, errorMsg); 223 try { 224 if (TelephonyUtils.IS_DEBUGGABLE) { 225 Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show(); 226 } 227 } catch (Throwable t) { 228 // whatever, not important 229 } 230 } 231 appOpsModeToPermissionResult(int appOpsMode)232 private static LocationPermissionResult appOpsModeToPermissionResult(int appOpsMode) { 233 switch (appOpsMode) { 234 case AppOpsManager.MODE_ALLOWED: 235 return LocationPermissionResult.ALLOWED; 236 case AppOpsManager.MODE_ERRORED: 237 return LocationPermissionResult.DENIED_HARD; 238 default: 239 return LocationPermissionResult.DENIED_SOFT; 240 } 241 } 242 getAppOpsString(String manifestPermission)243 private static String getAppOpsString(String manifestPermission) { 244 switch (manifestPermission) { 245 case Manifest.permission.ACCESS_FINE_LOCATION: 246 return AppOpsManager.OPSTR_FINE_LOCATION; 247 case Manifest.permission.ACCESS_COARSE_LOCATION: 248 return AppOpsManager.OPSTR_COARSE_LOCATION; 249 default: 250 return null; 251 } 252 } 253 checkAppLocationPermissionHelper(Context context, LocationPermissionQuery query, String permissionToCheck)254 private static LocationPermissionResult checkAppLocationPermissionHelper(Context context, 255 LocationPermissionQuery query, String permissionToCheck) { 256 String locationTypeForLog = 257 Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck) 258 ? "fine" : "coarse"; 259 260 // Do the app-ops and the manifest check without any of the allow-overrides first. 261 boolean hasManifestPermission = checkManifestPermission(context, query.callingPid, 262 query.callingUid, permissionToCheck); 263 264 if (hasManifestPermission) { 265 // Only check the app op if the app has the permission. 266 int appOpMode = context.getSystemService(AppOpsManager.class) 267 .noteOpNoThrow(getAppOpsString(permissionToCheck), query.callingUid, 268 query.callingPackage, query.callingFeatureId, null); 269 if (appOpMode == AppOpsManager.MODE_ALLOWED) { 270 // If the app did everything right, return without logging. 271 return LocationPermissionResult.ALLOWED; 272 } else { 273 // If the app has the manifest permission but not the app-op permission, it means 274 // that it's aware of the requirement and the user denied permission explicitly. 275 // If we see this, don't let any of the overrides happen. 276 Log.i(TAG, query.callingPackage + " is aware of " + locationTypeForLog + " but the" 277 + " app-ops permission is specifically denied."); 278 return appOpsModeToPermissionResult(appOpMode); 279 } 280 } 281 282 int minSdkVersion = Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck) 283 ? query.minSdkVersionForFine : query.minSdkVersionForCoarse; 284 285 // If the app fails for some reason, see if it should be allowed to proceed. 286 if (minSdkVersion > MAX_SDK_FOR_ANY_ENFORCEMENT) { 287 String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog 288 + " because we're not enforcing API " + minSdkVersion + " yet." 289 + " Please fix this app because it will break in the future. Called from " 290 + query.method; 291 logError(context, query, errorMsg); 292 return null; 293 } else if (!isAppAtLeastSdkVersion(context, query.callingPackage, minSdkVersion)) { 294 String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog 295 + " because it doesn't target API " + minSdkVersion + " yet." 296 + " Please fix this app. Called from " + query.method; 297 logError(context, query, errorMsg); 298 return null; 299 } else { 300 // If we're not allowing it due to the above two conditions, this means that the app 301 // did not declare the permission in their manifest. 302 return LocationPermissionResult.DENIED_HARD; 303 } 304 } 305 306 /** Check if location permissions have been granted */ checkLocationPermission( Context context, LocationPermissionQuery query)307 public static LocationPermissionResult checkLocationPermission( 308 Context context, LocationPermissionQuery query) { 309 // Always allow the phone process, system server, and network stack to access location. 310 // This avoid breaking legacy code that rely on public-facing APIs to access cell location, 311 // and it doesn't create an info leak risk because the cell location is stored in the phone 312 // process anyway, and the system server already has location access. 313 if (query.callingUid == Process.PHONE_UID || query.callingUid == Process.SYSTEM_UID 314 || query.callingUid == Process.NETWORK_STACK_UID 315 || query.callingUid == Process.ROOT_UID) { 316 return LocationPermissionResult.ALLOWED; 317 } 318 319 // Check the system-wide requirements. If the location main switch is off and the caller is 320 // not in the allowlist of apps that always have loation access or the app's profile 321 // isn't in the foreground, return a soft denial. 322 if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid, 323 query.callingPackage)) { 324 return LocationPermissionResult.DENIED_SOFT; 325 } 326 327 // Do the check for fine, then for coarse. 328 if (query.minSdkVersionForFine < Integer.MAX_VALUE) { 329 LocationPermissionResult resultForFine = checkAppLocationPermissionHelper( 330 context, query, Manifest.permission.ACCESS_FINE_LOCATION); 331 if (resultForFine != null) { 332 return resultForFine; 333 } 334 } 335 336 if (query.minSdkVersionForCoarse < Integer.MAX_VALUE) { 337 LocationPermissionResult resultForCoarse = checkAppLocationPermissionHelper( 338 context, query, Manifest.permission.ACCESS_COARSE_LOCATION); 339 if (resultForCoarse != null) { 340 return resultForCoarse; 341 } 342 } 343 344 // At this point, we're out of location checks to do. If the app bypassed all the previous 345 // ones due to the SDK backwards compatibility schemes, allow it access. 346 return LocationPermissionResult.ALLOWED; 347 } 348 checkManifestPermission(Context context, int pid, int uid, String permissionToCheck)349 private static boolean checkManifestPermission(Context context, int pid, int uid, 350 String permissionToCheck) { 351 return context.checkPermission(permissionToCheck, pid, uid) 352 == PackageManager.PERMISSION_GRANTED; 353 } 354 checkSystemLocationAccess(@onNull Context context, int uid, int pid, @NonNull String callingPackage)355 private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid, 356 @NonNull String callingPackage) { 357 if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier()) 358 && !isLocationBypassAllowed(context, callingPackage)) { 359 if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")"); 360 return false; 361 } 362 // If the user or profile is current, permission is granted. 363 // Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission. 364 return isCurrentProfile(context, uid) || checkInteractAcrossUsersFull(context, pid, uid); 365 } 366 367 /** 368 * @return Whether location is enabled for the given user. 369 */ isLocationModeEnabled(@onNull Context context, @UserIdInt int userId)370 public static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) { 371 LocationManager locationManager = context.getSystemService(LocationManager.class); 372 if (locationManager == null) { 373 Log.w(TAG, "Couldn't get location manager, denying location access"); 374 return false; 375 } 376 return locationManager.isLocationEnabledForUser(UserHandle.of(userId)); 377 } 378 isLocationBypassAllowed(@onNull Context context, @NonNull String callingPackage)379 private static boolean isLocationBypassAllowed(@NonNull Context context, 380 @NonNull String callingPackage) { 381 for (String bypassPackage : getLocationBypassPackages(context)) { 382 if (callingPackage.equals(bypassPackage)) { 383 return true; 384 } 385 } 386 return false; 387 } 388 389 /** 390 * @return An array of packages that are always allowed to access location. 391 */ getLocationBypassPackages(@onNull Context context)392 public static @NonNull String[] getLocationBypassPackages(@NonNull Context context) { 393 return context.getResources().getStringArray( 394 com.android.internal.R.array.config_serviceStateLocationAllowedPackages); 395 } 396 checkInteractAcrossUsersFull( @onNull Context context, int pid, int uid)397 private static boolean checkInteractAcrossUsersFull( 398 @NonNull Context context, int pid, int uid) { 399 return checkManifestPermission(context, pid, uid, 400 Manifest.permission.INTERACT_ACROSS_USERS_FULL); 401 } 402 isCurrentProfile(@onNull Context context, int uid)403 private static boolean isCurrentProfile(@NonNull Context context, int uid) { 404 final long token = Binder.clearCallingIdentity(); 405 try { 406 if (UserHandle.getUserHandleForUid(uid).getIdentifier() 407 == ActivityManager.getCurrentUser()) { 408 return true; 409 } 410 ActivityManager activityManager = context.getSystemService(ActivityManager.class); 411 if (activityManager != null) { 412 return activityManager.isProfileForeground( 413 UserHandle.getUserHandleForUid(ActivityManager.getCurrentUser())); 414 } else { 415 return false; 416 } 417 } finally { 418 Binder.restoreCallingIdentity(token); 419 } 420 } 421 isAppAtLeastSdkVersion(Context context, String pkgName, int sdkVersion)422 private static boolean isAppAtLeastSdkVersion(Context context, String pkgName, int sdkVersion) { 423 try { 424 if (context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion 425 >= sdkVersion) { 426 return true; 427 } 428 } catch (PackageManager.NameNotFoundException e) { 429 // In case of exception, assume known app (more strict checking) 430 // Note: This case will never happen since checkPackage is 431 // called to verify validity before checking app's version. 432 } 433 return false; 434 } 435 } 436