1 /*
2  * Copyright (C) 2023 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.app.ecm;
18 
19 import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION;
20 
21 import android.annotation.FlaggedApi;
22 import android.annotation.IntDef;
23 import android.annotation.RequiresPermission;
24 import android.annotation.SdkConstant;
25 import android.annotation.SystemApi;
26 import android.annotation.SystemService;
27 import android.annotation.TargetApi;
28 import android.app.AppOpsManager;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.PackageManager;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.os.Build;
34 import android.os.RemoteException;
35 import android.permission.flags.Flags;
36 import android.util.ArraySet;
37 
38 import androidx.annotation.NonNull;
39 
40 import java.lang.annotation.Retention;
41 
42 /**
43  * This class provides the core API for ECM (Enhanced Confirmation Mode). ECM is a feature that
44  * restricts access to protected **settings** (i.e., sensitive resources) by restricted **apps**
45  * (apps from from dangerous sources, such as sideloaded packages or packages downloaded from a web
46  * browser).
47  *
48  * <p>Specifically, this class provides the ability to:
49  *
50  * <ol>
51  *   <li>Check whether a setting is restricted from an app ({@link #isRestricted})
52  *   <li>Get an intent that will open the "Restricted setting" dialog ({@link
53  *       #createRestrictedSettingDialogIntent}) (a dialog that informs the user that the operation
54  *       they've attempted to perform is restricted)
55  *   <li>Check whether an app is eligible to have its restriction status cleared ({@link
56  *       #isClearRestrictionAllowed})
57  *   <li>Clear an app's restriction status (i.e., un-restrict it). ({@link #clearRestriction})
58  * </ol>
59  *
60  * <p>Methods of this class will generally accept an app (identified by a packageName and a user)
61  * and a "setting" (a string representing the "sensitive resource") as arguments. ECM's exact
62  * behavior will generally depend on what restriction state ECM considers each setting and app. For
63  * example:
64  *
65  * <ol>
66  *   <li>A setting may be considered by ECM to be either **protected** or **not protected**. In
67  *       general, this should be considered hardcoded into ECM's implementation: nothing can
68  *       "protect" or "unprotect" a setting.
69  *   <li>An app may be considered as being **not restricted** or **restricted**. A restricted app
70  *       will be restricted from accessing all protected settings. Whether ECM considers any
71  *       particular app restricted is an implementation detail of ECM. However, the user is able to
72  *       clear any restricted app's restriction status (i.e, un-restrict it), after which ECM will
73  *       consider the app **not restricted**.
74  * </ol>
75  *
76  * Why is ECM needed? Consider the following (pre-ECM) scenario:
77  *
78  * <ol>
79  *   <li>The user downloads and installs an apk file from a browser.
80  *   <li>The user opens Settings -> Accessibility
81  *   <li>The user tries to register the app as an accessibility service.
82  *   <li>The user is shown a permission prompt "Allow _ to have full control of your device?"
83  *   <li>The user clicks "Allow"
84  *   <li>The downloaded app now has full control of the device.
85  * </ol>
86  *
87  * The purpose of ECM is to add more friction to this scenario.
88  *
89  * <p>With ECM, this scenario becomes:
90  *
91  * <ol>
92  *   <li>The user downloads and installs an apk file from a browser.
93  *   <li>The user goes into Settings -> Accessibility.
94  *   <li>The user tries to register the app as an accessibility service.
95  *   <li>The user is presented with a "Restricted setting" dialog explaining that the attempted
96  *       action has been restricted. (No "allow" button is shown, but a link is given to a screen
97  *       with intentionally-obscure instructions on how to proceed.)
98  *   <li>The user must now navigate to Settings -> Apps -> [app]
99  *   <li>The user then must click on "..." (top-right corner hamburger menu), then click "Allow
100  *       restricted settings"
101  *   <li>The user goes (again) into Settings -> Accessibility and (again) tries to register the app
102  *       as an accessibility service.
103  *   <li>The user is shown a permission prompt "Allow _ to have full control of your device?"
104  *   <li>The user clicks "Allow"
105  *   <li>The downloaded app now has full control of the device.
106  * </ol>
107  *
108  * And, expanding on the above scenario, the role that this class plays is as follows:
109  *
110  * <ol>
111  *   <li>The user downloads and installs an apk file from a browser.
112  *   <li>The user goes into Settings -> Accessibility.
113  *       <p>**This screen then calls {@link #isRestricted}, which checks whether each app listed
114  *       on-screen is restricted from the accessibility service setting. It uses this to visually
115  *       "gray out" restricted apps.**
116  *   <li>The user tries to register the app as an accessibility service.
117  *       <p>**This screen then calls {@link #createRestrictedSettingDialogIntent} and starts the
118  *       intent. This opens the "Restricted setting" dialog.**
119  *   <li>The user is presented with a "Restricted setting" dialog explaining that the attempted
120  *       action is restricted. (No "allow" button is shown, but a link is given to a screen with
121  *       intentionally-obscure instructions on how to proceed.)
122  *       <p>**Upon opening, this dialog marks the app as eligible to have its restriction status
123  *       cleared.**
124  *   <li>The user must now navigate to Settings -> Apps -> [app].
125  *       <p>**This screen calls {@link #isClearRestrictionAllowed} to check whether the app is
126  *       eligible to have its restriction status cleared. If this returns {@code true}, this screen
127  *       should then show a "Allow restricted setting" button inside the top-right hamburger menu.**
128  *   <li>The user then must click on "..." (top-right corner hamburger menu), then click "Allow
129  *       restricted settings".
130  *       <p>**In response, this screen should now call {@link #clearRestriction}.**
131  *   <li>The user goes (again) into Settings -> Accessibility and (again) tries to register the app
132  *       as an accessibility service.
133  *   <li>The user is shown a permission prompt "Allow _ to have full control of your device?"
134  *   <li>The user clicks "Allow"
135  *   <li>The downloaded app now has full control of the device.
136  * </ol>
137  *
138  * @hide
139  */
140 @SystemApi
141 @FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
142 @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
143 @SystemService(Context.ECM_ENHANCED_CONFIRMATION_SERVICE)
144 public final class EnhancedConfirmationManager {
145     /*
146      * At the API level, we use the following terminology:
147      *
148      * - The capability of an app to access a setting may be considered (by ECM) to be *restricted*
149      * or *not restricted*.
150      * - A setting may be considered (by ECM) to be *protected* or *not protected*.
151      * - The state of an app may be considered (by ECM) to be *restricted* or *not restricted*
152      *
153      * In this implementation, however, the state of an app is considered either **guarded** or
154      * **not guarded**; these terms can generally be considered synonymous with **restricted** and
155      * **not restricted**. (Keeping in mind that, the capability of any app to access any
156      * non-protected setting will always be considered "not restricted", even if the state of the
157      * app is considered "restricted".). An app can also be in a third state: **guarded and
158      * acknowledged**, which corresponds with an app that is restricted and is eligible to have its
159      * restriction status cleared.
160      *
161      * Currently, the ECM state of any given app is stored in the OP_ACCESS_RESTRICTED_SETTINGS
162      * appop (though this may change in the future):
163      *
164      * - MODE_ALLOWED means the app is explicitly **not guarded**. (U- default)
165      * - MODE_ERRORED means the app is explicitly **guarded**. (Only settable in U-.)
166      * - MODE_IGNORED means the app is explicitly **guarded and acknowledged**. (An app enters this
167      *   state as soon as the "Restricted setting" dialog has been shown to the user. If an app is
168      *   in this state, Settings is now allowed to provide the user with the option to clear the
169      *   restriction.)
170      * - MODE_DEFAULT means the app's ECM state should be decided lazily. (V+ default) (That is,
171      *   each time a caller checks whether or not an app is considered guarded by ECM, we'll run an
172      *   heuristic to determine this.)
173      *
174      * Some notes on compatibility:
175      *
176      *   - On U-, MODE_ALLOWED is the default mode of OP_ACCESS_RESTRICTED_SETTINGS. On both U- and
177      *   V+, this is also the mode after the app's restriction has been cleared.
178      *   - In U-, the mode needed to be explicitly set (for example, by a browser that allows a
179      *   dangerous app to be installed) to MODE_ERRORED to indicate that an app is guarded. In V+,
180      *   we no longer allow an app to be placed into MODE_ERRORED, but for compatibility, we still
181      *   recognize MODE_ERRORED to indicate that an app is explicitly guarded.
182      * - In V+, the default mode is MODE_DEFAULT. Unlike U-, this potentially affects *all* apps,
183      *   not just the ones which have been explicitly marked as **guarded**.
184      *
185      * Regarding ECM "setting"s: a setting may be any abstract resource identified by a string. ECM
186      * may consider any particular setting **protected** or **not protected**. For now, the set of
187      * protected settings is hardcoded, but this may evolve in the future.
188      *
189      * TODO(b/320512579): These methods currently enforce UPDATE_APP_OPS_STATS,
190      * UPDATE_APP_OPS_STATS, and, for setter methods, MANAGE_APP_OPS_MODES. We should add
191      * RequiresPermission annotations, but we can't, because some of these permissions are hidden
192      * API. Either upgrade these to SystemApi or enforce a different permission, then add the
193      * appropriate RequiresPermission annotation.
194      */
195 
196     /**
197      * Shows the "Restricted setting" dialog. Opened when a setting is blocked.
198      */
199     @SdkConstant(BROADCAST_INTENT_ACTION)
200     public static final String ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG =
201             "android.app.ecm.action.SHOW_ECM_RESTRICTED_SETTING_DIALOG";
202 
203     /** A map of ECM states to their corresponding app op states */
204     @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
205     @IntDef(prefix = {"ECM_STATE_"}, value = {EcmState.ECM_STATE_NOT_GUARDED,
206             EcmState.ECM_STATE_GUARDED, EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED,
207             EcmState.ECM_STATE_IMPLICIT})
208     private @interface EcmState {
209         int ECM_STATE_NOT_GUARDED = AppOpsManager.MODE_ALLOWED;
210         int ECM_STATE_GUARDED = AppOpsManager.MODE_ERRORED;
211         int ECM_STATE_GUARDED_AND_ACKNOWLEDGED = AppOpsManager.MODE_IGNORED;
212         int ECM_STATE_IMPLICIT = AppOpsManager.MODE_DEFAULT;
213     }
214 
215     private static final String LOG_TAG = EnhancedConfirmationManager.class.getSimpleName();
216 
217     private static final ArraySet<String> PROTECTED_SETTINGS = new ArraySet<>();
218 
219     static {
220         PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
221         // TODO(b/310654015): Add other explicitly protected settings
222     }
223 
224     private final @NonNull Context mContext;
225     private final PackageManager mPackageManager;
226 
227     private final @NonNull IEnhancedConfirmationManager mService;
228 
229     /**
230      * @hide
231      */
EnhancedConfirmationManager(@onNull Context context, @NonNull IEnhancedConfirmationManager service)232     public EnhancedConfirmationManager(@NonNull Context context,
233             @NonNull IEnhancedConfirmationManager service) {
234         mContext = context;
235         mPackageManager = context.getPackageManager();
236         mService = service;
237     }
238 
239     /**
240      * Check whether a setting is restricted from an app.
241      *
242      * <p>This is {@code true} when the setting is a protected setting (i.e., a sensitive resource),
243      * and the app is restricted (i.e., considered dangerous), and the user has not yet cleared the
244      * app's restriction status (i.e., by clicking "Allow restricted settings" for this app).
245      *
246      * @param packageName package name of the application to check for
247      * @param settingIdentifier identifier of the resource to check to check for
248      * @return {@code true} if the setting is restricted from the app
249      * @throws NameNotFoundException if the provided package was not found
250      */
251     @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
isRestricted(@onNull String packageName, @NonNull String settingIdentifier)252     public boolean isRestricted(@NonNull String packageName, @NonNull String settingIdentifier)
253             throws NameNotFoundException {
254         try {
255             return mService.isRestricted(packageName, settingIdentifier,
256                     mContext.getUser().getIdentifier());
257         } catch (IllegalArgumentException e) {
258             throw new NameNotFoundException(packageName);
259         } catch (RemoteException e) {
260             throw e.rethrowFromSystemServer();
261         }
262     }
263 
264     /**
265      * Clear an app's restriction status (i.e., un-restrict it).
266      *
267      * <p>After this is called, the app will no longer be restricted from accessing any protected
268      * setting by ECM. This method should be called when the user clicks "Allow restricted settings"
269      * for the app.
270      *
271      * @param packageName package name of the application to remove protection from
272      * @throws NameNotFoundException if the provided package was not found
273      */
274     @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
clearRestriction(@onNull String packageName)275     public void clearRestriction(@NonNull String packageName) throws NameNotFoundException {
276         try {
277             mService.clearRestriction(packageName, mContext.getUser().getIdentifier());
278         } catch (IllegalArgumentException e) {
279             throw new NameNotFoundException(packageName);
280         } catch (RemoteException e) {
281             throw e.rethrowFromSystemServer();
282         }
283     }
284 
285     /**
286      * Check whether the provided app is eligible to have its restriction status cleared (i.e., the
287      * app is restricted, and the "Restricted setting" dialog has been presented to the user).
288      *
289      * <p>The Settings UI should use method this to check whether to present the user with the
290      * "Allow restricted settings" button.
291      *
292      * @param packageName package name of the application to check for
293      * @return {@code true} if the settings UI should present the user with the ability to clear
294      * restrictions from the provided app
295      * @throws NameNotFoundException if the provided package was not found
296      */
297     @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
isClearRestrictionAllowed(@onNull String packageName)298     public boolean isClearRestrictionAllowed(@NonNull String packageName)
299             throws NameNotFoundException {
300         try {
301             return mService.isClearRestrictionAllowed(packageName,
302                     mContext.getUser().getIdentifier());
303         } catch (IllegalArgumentException e) {
304             throw new NameNotFoundException(packageName);
305         } catch (RemoteException e) {
306             throw e.rethrowFromSystemServer();
307         }
308     }
309 
310     /**
311      * Mark the app as eligible to have its restriction status cleared.
312      *
313      * <p>This should be called from the "Restricted setting" dialog (which {@link
314      * #createRestrictedSettingDialogIntent} directs to) upon being presented to the user.
315      *
316      * @param packageName package name of the application which should be considered acknowledged
317      * @throws NameNotFoundException if the provided package was not found
318      */
319     @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
setClearRestrictionAllowed(@onNull String packageName)320     public void setClearRestrictionAllowed(@NonNull String packageName)
321             throws NameNotFoundException {
322         try {
323             mService.setClearRestrictionAllowed(packageName, mContext.getUser().getIdentifier());
324         } catch (IllegalArgumentException e) {
325             throw new NameNotFoundException(packageName);
326         } catch (RemoteException e) {
327             throw e.rethrowFromSystemServer();
328         }
329     }
330 
331     /**
332      * Gets an intent that will open the "Restricted setting" dialog for the specified package
333      * and setting.
334      *
335      * <p>The "Restricted setting" dialog is a dialog that informs the user that the operation
336      * they've attempted to perform is restricted, and provides them with a link explaining how to
337      * proceed.
338      *
339      * @param packageName package name of the restricted application
340      * @param settingIdentifier identifier of the restricted setting
341      * @throws NameNotFoundException if the provided package was not found
342      */
createRestrictedSettingDialogIntent(@onNull String packageName, @NonNull String settingIdentifier)343     public @NonNull Intent createRestrictedSettingDialogIntent(@NonNull String packageName,
344             @NonNull String settingIdentifier) throws NameNotFoundException {
345         Intent intent = new Intent(ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG);
346         intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
347         intent.putExtra(Intent.EXTRA_UID, getPackageUid(packageName));
348         intent.putExtra(Intent.EXTRA_SUBJECT, settingIdentifier);
349         return intent;
350     }
351 
getPackageUid(String packageName)352     private int getPackageUid(String packageName) throws NameNotFoundException {
353         return mPackageManager.getApplicationInfoAsUser(packageName, /* flags */ 0,
354                 mContext.getUser()).uid;
355     }
356 }
357