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