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