blob: 41f32075fb77bd1f3c1f12f05354cbbf033d3569 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility;
import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.appwidget.AppWidgetManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArraySet;
import android.util.Slog;
import android.view.accessibility.AccessibilityEvent;
import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.wm.ActivityTaskManagerInternal;
import libcore.util.EmptyArray;
/**
* This class provides APIs of accessibility security policies for accessibility manager
* to grant accessibility capabilities or events access right to accessibility service.
*/
public class AccessibilitySecurityPolicy {
private static final int OWN_PROCESS_ID = android.os.Process.myPid();
private static final String LOG_TAG = "AccessibilitySecurityPolicy";
private final Context mContext;
private final PackageManager mPackageManager;
private final UserManager mUserManager;
private final AppOpsManager mAppOpsManager;
private AppWidgetManagerInternal mAppWidgetService;
private static final int KEEP_SOURCE_EVENT_TYPES = AccessibilityEvent.TYPE_VIEW_CLICKED
| AccessibilityEvent.TYPE_VIEW_FOCUSED
| AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
| AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
| AccessibilityEvent.TYPE_VIEW_LONG_CLICKED
| AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
| AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
| AccessibilityEvent.TYPE_WINDOWS_CHANGED
| AccessibilityEvent.TYPE_VIEW_SELECTED
| AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
| AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
| AccessibilityEvent.TYPE_VIEW_SCROLLED
| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
| AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;
/**
* Methods that should find their way into separate modules, but are still in AMS
* TODO (b/111889696): Refactoring UserState to AccessibilityUserManager.
*/
public interface AccessibilityUserManager {
/**
* Returns current userId maintained in accessibility manager service
*/
int getCurrentUserIdLocked();
// TODO: Should include resolveProfileParentLocked, but that was already in SecurityPolicy
}
private final AccessibilityUserManager mAccessibilityUserManager;
private AccessibilityWindowManager mAccessibilityWindowManager;
private final ActivityTaskManagerInternal mAtmInternal;
/**
* Constructor for AccessibilityManagerService.
*/
public AccessibilitySecurityPolicy(@NonNull Context context,
@NonNull AccessibilityUserManager a11yUserManager) {
mContext = context;
mAccessibilityUserManager = a11yUserManager;
mPackageManager = mContext.getPackageManager();
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
}
/**
* Setup AccessibilityWindowManager. This isn't part of the constructor because the
* window manager and security policy both call each other.
*/
public void setAccessibilityWindowManager(@NonNull AccessibilityWindowManager awm) {
mAccessibilityWindowManager = awm;
}
/**
* Setup AppWidgetManger during boot phase.
*/
public void setAppWidgetManager(@NonNull AppWidgetManagerInternal appWidgetManager) {
mAppWidgetService = appWidgetManager;
}
/**
* Check if an accessibility event can be dispatched. Events should be dispatched only if they
* are dispatched from items that services can see.
*
* @param userId The userId to check
* @param event The event to check
* @return {@code true} if the event can be dispatched
*/
public boolean canDispatchAccessibilityEventLocked(int userId,
@NonNull AccessibilityEvent event) {
final int eventType = event.getEventType();
switch (eventType) {
// All events that are for changes in a global window
// state should *always* be dispatched.
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
case AccessibilityEvent.TYPE_ANNOUNCEMENT:
// All events generated by the user touching the
// screen should *always* be dispatched.
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
case AccessibilityEvent.TYPE_GESTURE_DETECTION_START:
case AccessibilityEvent.TYPE_GESTURE_DETECTION_END:
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START:
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:
// Also always dispatch the event that assist is reading context.
case AccessibilityEvent.TYPE_ASSIST_READING_CONTEXT:
// Also windows changing should always be dispatched.
case AccessibilityEvent.TYPE_WINDOWS_CHANGED: {
return true;
}
// All events for changes in window content should be
// dispatched *only* if this window is one of the windows
// the accessibility layer reports which are windows
// that a sighted user can touch.
default: {
return isRetrievalAllowingWindowLocked(userId, event.getWindowId());
}
}
}
/**
* Find a valid package name for an app to expose to accessibility
*
* @param packageName The package name the app wants to expose
* @param appId The app's id
* @param userId The app's user id
* @param pid The app's process pid that requested this
* @return A package name that is valid to report
*/
@Nullable
public String resolveValidReportedPackageLocked(
@Nullable CharSequence packageName, int appId, int userId, int pid) {
// Okay to pass no package
if (packageName == null) {
return null;
}
// The system gets to pass any package
if (appId == Process.SYSTEM_UID) {
return packageName.toString();
}
// Passing a package in your UID is fine
final String packageNameStr = packageName.toString();
final int resolvedUid = UserHandle.getUid(userId, appId);
if (isValidPackageForUid(packageNameStr, resolvedUid)) {
return packageName.toString();
}
// Appwidget hosts get to pass packages for widgets they host
if (mAppWidgetService != null && ArrayUtils.contains(mAppWidgetService
.getHostedWidgetPackages(resolvedUid), packageNameStr)) {
return packageName.toString();
}
// If app has the targeted permission to act as another package
if (mContext.checkPermission(Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY,
pid, resolvedUid) == PackageManager.PERMISSION_GRANTED) {
return packageName.toString();
}
// Otherwise, set the package to the first one in the UID
final String[] packageNames = mPackageManager.getPackagesForUid(resolvedUid);
if (ArrayUtils.isEmpty(packageNames)) {
return null;
}
// Okay, the caller reported a package it does not have access to.
// Instead of crashing the caller for better backwards compatibility
// we report the first package in the UID. Since most of the time apps
// don't use shared user id, this will yield correct results and for
// the edge case of using a shared user id we may report the wrong
// package but this is fine since first, this is a cheating app and
// second there is no way to get the correct package anyway.
return packageNames[0];
}
/**
* Get the packages that are valid for a uid. In some situations, like app widgets, there
* could be several valid packages
*
* @param targetPackage A package that is known to be valid for this id
* @param targetUid The whose packages should be checked
* @return An array of all valid package names. An empty array means any package is OK
*/
@NonNull
public String[] computeValidReportedPackages(
@NonNull String targetPackage, int targetUid) {
if (UserHandle.getAppId(targetUid) == Process.SYSTEM_UID) {
// Empty array means any package is Okay
return EmptyArray.STRING;
}
// IMPORTANT: The target package is already vetted to be in the target UID
String[] uidPackages = new String[]{targetPackage};
// Appwidget hosts get to pass packages for widgets they host
if (mAppWidgetService != null) {
final ArraySet<String> widgetPackages = mAppWidgetService
.getHostedWidgetPackages(targetUid);
if (widgetPackages != null && !widgetPackages.isEmpty()) {
final String[] validPackages = new String[uidPackages.length
+ widgetPackages.size()];
System.arraycopy(uidPackages, 0, validPackages, 0, uidPackages.length);
final int widgetPackageCount = widgetPackages.size();
for (int i = 0; i < widgetPackageCount; i++) {
validPackages[uidPackages.length + i] = widgetPackages.valueAt(i);
}
return validPackages;
}
}
return uidPackages;
}
/**
* Reset the event source for events that should not carry one
*
* @param event The event potentially to modify
*/
public void updateEventSourceLocked(@NonNull AccessibilityEvent event) {
if ((event.getEventType() & KEEP_SOURCE_EVENT_TYPES) == 0) {
event.setSource(null);
}
}
/**
* Check if a service can have access to a window
*
* @param userId The id of the user running the service
* @param service The service requesting access
* @param windowId The window it wants access to
*
* @return Whether ot not the service may retrieve info from the window
*/
public boolean canGetAccessibilityNodeInfoLocked(int userId,
@NonNull AbstractAccessibilityServiceConnection service, int windowId) {
return canRetrieveWindowContentLocked(service)
&& isRetrievalAllowingWindowLocked(userId, windowId);
}
/**
* Check if a service can have access the list of windows
*
* @param service The service requesting access
*
* @return Whether ot not the service may retrieve the window list
*/
public boolean canRetrieveWindowsLocked(
@NonNull AbstractAccessibilityServiceConnection service) {
return canRetrieveWindowContentLocked(service) && service.mRetrieveInteractiveWindows;
}
/**
* Check if a service can have access the content of windows on the screen
*
* @param service The service requesting access
*
* @return Whether ot not the service may retrieve the content
*/
public boolean canRetrieveWindowContentLocked(
@NonNull AbstractAccessibilityServiceConnection service) {
return (service.getCapabilities()
& AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0;
}
/**
* Check if a service can control magnification
*
* @param service The service requesting access
*
* @return Whether ot not the service may control magnification
*/
public boolean canControlMagnification(
@NonNull AbstractAccessibilityServiceConnection service) {
return (service.getCapabilities()
& AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION) != 0;
}
/**
* Check if a service can perform gestures
*
* @param service The service requesting access
*
* @return Whether ot not the service may perform gestures
*/
public boolean canPerformGestures(@NonNull AccessibilityServiceConnection service) {
return (service.getCapabilities()
& AccessibilityServiceInfo.CAPABILITY_CAN_PERFORM_GESTURES) != 0;
}
/**
* Check if a service can capture gestures from the fingerprint sensor
*
* @param service The service requesting access
*
* @return Whether ot not the service may capture gestures from the fingerprint sensor
*/
public boolean canCaptureFingerprintGestures(@NonNull AccessibilityServiceConnection service) {
return (service.getCapabilities()
& AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES) != 0;
}
/**
* Checks if a service can take screenshot.
*
* @param service The service requesting access
*
* @return Whether ot not the service may take screenshot
*/
public boolean canTakeScreenshotLocked(
@NonNull AbstractAccessibilityServiceConnection service) {
return (service.getCapabilities()
& AccessibilityServiceInfo.CAPABILITY_CAN_TAKE_SCREENSHOT) != 0;
}
/**
* Returns the parent userId of the profile according to the specified userId.
*
* @param userId The userId to check
* @return the parent userId of the profile, or self if no parent exist
*/
public int resolveProfileParentLocked(int userId) {
if (userId != mAccessibilityUserManager.getCurrentUserIdLocked()) {
final long identity = Binder.clearCallingIdentity();
try {
UserInfo parent = mUserManager.getProfileParent(userId);
if (parent != null) {
return parent.getUserHandle().getIdentifier();
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
return userId;
}
/**
* Returns the parent userId of the profile according to the specified userId. Enforcing
* permissions check if specified userId is not caller's userId.
*
* @param userId The userId to check
* @return the parent userId of the profile, or self if no parent exist
* @throws SecurityException if caller cannot interact across users
* @throws IllegalArgumentException if specified invalid userId
*/
public int resolveCallingUserIdEnforcingPermissionsLocked(int userId) {
final int callingUid = Binder.getCallingUid();
final int currentUserId = mAccessibilityUserManager.getCurrentUserIdLocked();
if (callingUid == 0
|| callingUid == Process.SYSTEM_UID
|| callingUid == Process.SHELL_UID) {
if (userId == UserHandle.USER_CURRENT
|| userId == UserHandle.USER_CURRENT_OR_SELF) {
return currentUserId;
}
return resolveProfileParentLocked(userId);
}
final int callingUserId = UserHandle.getUserId(callingUid);
if (callingUserId == userId) {
return resolveProfileParentLocked(userId);
}
final int callingUserParentId = resolveProfileParentLocked(callingUserId);
if (callingUserParentId == currentUserId && (userId == UserHandle.USER_CURRENT
|| userId == UserHandle.USER_CURRENT_OR_SELF)) {
return currentUserId;
}
if (!hasPermission(Manifest.permission.INTERACT_ACROSS_USERS)
&& !hasPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
throw new SecurityException("Call from user " + callingUserId + " as user "
+ userId + " without permission INTERACT_ACROSS_USERS or "
+ "INTERACT_ACROSS_USERS_FULL not allowed.");
}
if (userId == UserHandle.USER_CURRENT
|| userId == UserHandle.USER_CURRENT_OR_SELF) {
return currentUserId;
}
return resolveProfileParentLocked(userId);
}
/**
* Returns false if caller is not SYSTEM and SHELL, and tried to interact across users.
*
* @param userId The userId to interact.
* @return false if caller cannot interact across users.
*/
public boolean isCallerInteractingAcrossUsers(int userId) {
final int callingUid = Binder.getCallingUid();
return (Binder.getCallingPid() == android.os.Process.myPid()
|| callingUid == Process.SHELL_UID
|| userId == UserHandle.USER_CURRENT
|| userId == UserHandle.USER_CURRENT_OR_SELF);
}
private boolean isValidPackageForUid(String packageName, int uid) {
final long token = Binder.clearCallingIdentity();
try {
return uid == mPackageManager.getPackageUidAsUser(
packageName, UserHandle.getUserId(uid));
} catch (PackageManager.NameNotFoundException e) {
return false;
} finally {
Binder.restoreCallingIdentity(token);
}
}
private boolean isRetrievalAllowingWindowLocked(int userId, int windowId) {
// The system gets to interact with any window it wants.
if (Binder.getCallingUid() == Process.SYSTEM_UID) {
return true;
}
if (Binder.getCallingUid() == Process.SHELL_UID) {
if (!isShellAllowedToRetrieveWindowLocked(userId, windowId)) {
return false;
}
}
if (mAccessibilityWindowManager.resolveParentWindowIdLocked(windowId)
== mAccessibilityWindowManager.getActiveWindowId(userId)) {
return true;
}
return mAccessibilityWindowManager.findA11yWindowInfoByIdLocked(windowId) != null;
}
private boolean isShellAllowedToRetrieveWindowLocked(int userId, int windowId) {
long token = Binder.clearCallingIdentity();
try {
IBinder windowToken = mAccessibilityWindowManager
.getWindowTokenForUserAndWindowIdLocked(userId, windowId);
if (windowToken == null) {
return false;
}
int windowOwnerUserId = mAccessibilityWindowManager.getWindowOwnerUserId(windowToken);
if (windowOwnerUserId == UserHandle.USER_NULL) {
return false;
}
return !mUserManager.hasUserRestriction(
UserManager.DISALLOW_DEBUGGING_FEATURES, UserHandle.of(windowOwnerUserId));
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Enforcing permission check to caller.
*
* @param permission The permission to check
* @param function The function name to check
*/
public void enforceCallingPermission(@NonNull String permission, @Nullable String function) {
if (OWN_PROCESS_ID == Binder.getCallingPid()) {
return;
}
if (!hasPermission(permission)) {
throw new SecurityException("You do not have " + permission
+ " required to call " + function + " from pid="
+ Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
}
}
/**
* Permission check to caller.
*
* @param permission The permission to check
* @return true if caller has permission
*/
public boolean hasPermission(@NonNull String permission) {
return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED;
}
/**
* Checks if accessibility service could register into the system.
*
* @param serviceInfo The ServiceInfo
* @return True if it could register into the system
*/
public boolean canRegisterService(@NonNull ServiceInfo serviceInfo) {
if (!android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE.equals(
serviceInfo.permission)) {
Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
serviceInfo.packageName, serviceInfo.name).flattenToShortString()
+ ": it does not require the permission "
+ android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE);
return false;
}
int servicePackageUid = serviceInfo.applicationInfo.uid;
if (mAppOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE,
servicePackageUid, serviceInfo.packageName) != AppOpsManager.MODE_ALLOWED) {
Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
serviceInfo.packageName, serviceInfo.name).flattenToShortString()
+ ": disallowed by AppOps");
return false;
}
return true;
}
/**
* Checks if accessibility service could execute accessibility operations.
*
* @param service The accessibility service connection
* @return True if it could execute accessibility operations
*/
public boolean checkAccessibilityAccess(AbstractAccessibilityServiceConnection service) {
final String packageName = service.getComponentName().getPackageName();
final ResolveInfo resolveInfo = service.getServiceInfo().getResolveInfo();
if (resolveInfo == null) {
// For InteractionBridge and UiAutomation
return true;
}
final int uid = resolveInfo.serviceInfo.applicationInfo.uid;
final long identityToken = Binder.clearCallingIdentity();
try {
// For the caller is system, just block the data to a11y services.
if (OWN_PROCESS_ID == Binder.getCallingPid()) {
return mAppOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_ACCESS_ACCESSIBILITY,
uid, packageName) == AppOpsManager.MODE_ALLOWED;
}
return mAppOpsManager.noteOp(AppOpsManager.OPSTR_ACCESS_ACCESSIBILITY,
uid, packageName) == AppOpsManager.MODE_ALLOWED;
} finally {
Binder.restoreCallingIdentity(identityToken);
}
}
/**
* Enforcing permission check to IPC caller or grant it if it's not through IPC.
*
* @param permission The permission to check
*/
public void enforceCallingOrSelfPermission(@NonNull String permission) {
if (mContext.checkCallingOrSelfPermission(permission)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Caller does not hold permission "
+ permission);
}
}
/**
* Enforcing permission check to IPC caller or grant it if it's recents.
*
* @param permission The permission to check
*/
public void enforceCallerIsRecentsOrHasPermission(@NonNull String permission, String func) {
mAtmInternal.enforceCallerIsRecentsOrHasPermission(permission, func);
}
}