diff --git a/core/java/android/ext/SettingsIntents.java b/core/java/android/ext/SettingsIntents.java index ab42fdfa0038..5f62b664f442 100644 --- a/core/java/android/ext/SettingsIntents.java +++ b/core/java/android/ext/SettingsIntents.java @@ -9,6 +9,7 @@ public class SettingsIntents { public static final String APP_NATIVE_DEBUGGING = "android.settings.OPEN_APP_NATIVE_DEBUGGING_SETTINGS"; public static final String APP_MEMTAG = "android.settings.OPEN_APP_MEMTAG_SETTINGS"; public static final String APP_HARDENED_MALLOC = "android.settings.OPEN_APP_HARDENED_MALLOC_SETTINGS"; + public static final String APP_CLIPBOARD = "android.settings.OPEN_APP_CLIPBOARD_SETTINGS"; public static Intent getAppIntent(String action, String pkgName) { var i = new Intent(action); diff --git a/core/java/android/ext/settings/ExtSettings.java b/core/java/android/ext/settings/ExtSettings.java index 4e00270fb2f7..92a3536d1d10 100644 --- a/core/java/android/ext/settings/ExtSettings.java +++ b/core/java/android/ext/settings/ExtSettings.java @@ -137,6 +137,10 @@ public boolean validateValue(String val) { public static final BoolSysProperty ALLOW_NATIVE_DEBUG_BY_DEFAULT = new BoolSysProperty( "persist.native_debug", defaultBool(R.bool.setting_default_allow_native_debugging)); + public static final BoolSetting DENY_CLIPBOARD_READ_BY_DEFAULT = new BoolSetting( + Setting.Scope.PER_USER, "deny_clipboard_read", + defaultBool(R.bool.setting_default_deny_clipboard_read)); + // AppCompatConfig specifies which hardening features are compatible/incompatible with a // specific app. // This setting controls whether incompatible hardening features would be disabled by default diff --git a/core/java/android/ext/settings/app/AswDenyClipboardRead.java b/core/java/android/ext/settings/app/AswDenyClipboardRead.java new file mode 100644 index 000000000000..6807047a5a97 --- /dev/null +++ b/core/java/android/ext/settings/app/AswDenyClipboardRead.java @@ -0,0 +1,37 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; +import android.ext.settings.ExtSettings; + +/** @hide */ +public class AswDenyClipboardRead extends AppSwitch { + public static final AswDenyClipboardRead I = new AswDenyClipboardRead(); + + private AswDenyClipboardRead() { + gosPsFlag = GosPackageState.FLAG_DENY_CLIPBOARD_READ; + gosPsFlagNonDefault = GosPackageState.FLAG_DENY_CLIPBOARD_READ_NON_DEFAULT; + gosPsFlagSuppressNotif = GosPackageState.FLAG_DENY_CLIPBOARD_READ_SUPPRESS_NOTIF; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + si.immutabilityReason = IR_IS_SYSTEM_APP; + return false; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + si.defaultValueReason = DVR_DEFAULT_SETTING; + return ExtSettings.DENY_CLIPBOARD_READ_BY_DEFAULT.get(ctx, userId); + } +} diff --git a/core/res/res/values/config_ext.xml b/core/res/res/values/config_ext.xml index 104593554d27..78eced9f5c81 100644 --- a/core/res/res/values/config_ext.xml +++ b/core/res/res/values/config_ext.xml @@ -38,4 +38,7 @@ app.grapheneos.apps + + false + diff --git a/core/res/res/values/string_ext.xml b/core/res/res/values/string_ext.xml index 19a4e2aa1a2d..391850465360 100644 --- a/core/res/res/values/string_ext.xml +++ b/core/res/res/values/string_ext.xml @@ -46,4 +46,7 @@ hardened_malloc detected an error in %1$s + %1$s tried to read clipboard + + diff --git a/services/core/java/com/android/server/clipboard/ClipboardAccessHelper.java b/services/core/java/com/android/server/clipboard/ClipboardAccessHelper.java new file mode 100644 index 000000000000..5b0f888ca755 --- /dev/null +++ b/services/core/java/com/android/server/clipboard/ClipboardAccessHelper.java @@ -0,0 +1,41 @@ +package com.android.server.clipboard; + +import android.content.ClipData; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.PackageManagerInternal; +import android.ext.SettingsIntents; +import android.ext.settings.app.AswDenyClipboardRead; +import android.os.Process; + +import com.android.internal.R; +import com.android.server.LocalServices; +import com.android.server.ext.AppSwitchNotification; +import com.android.server.pm.pkg.GosPackageStatePm; + +public class ClipboardAccessHelper { + static final ClipData dummyClip = ClipData.newPlainText(null, ""); + + static boolean isReadBlockedForPackage(Context ctx, String pkgName, int userId) { + var pmi = LocalServices.getService(PackageManagerInternal.class); + ApplicationInfo appInfo = pmi.getApplicationInfo(pkgName, 0, Process.SYSTEM_UID, userId); + if (appInfo == null) { + return true; + } + GosPackageStatePm ps = pmi.getGosPackageState(pkgName, userId); + return AswDenyClipboardRead.I.get(ctx, userId, appInfo, ps); + } + + static void maybeNotifyAccessDenied(Context ctx, String pkgName, int userId) { + var pmi = LocalServices.getService(PackageManagerInternal.class); + ApplicationInfo appInfo = pmi.getApplicationInfo(pkgName, 0, Process.SYSTEM_UID, userId); + if (appInfo == null) { + return; + } + var n = AppSwitchNotification.create(ctx, appInfo, SettingsIntents.APP_CLIPBOARD); + n.titleRes = R.string.notif_clipboard_read_deny_title; + n.gosPsFlagSuppressNotif = GosPackageState.FLAG_DENY_CLIPBOARD_READ_SUPPRESS_NOTIF; + n.maybeShow(); + } +} diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 56a94ec06ad4..154a0c924ada 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -682,7 +682,12 @@ public ClipData getPrimaryClip( if (clipboard == null) { return null; } - showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard); + boolean isReadBlocked = isReadBlockedForPkg(intendingUid, pkg, userId, clipboard); + showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard, + isReadBlocked); + if (isReadBlocked) { + return clipboard.primaryClip != null ? ClipboardAccessHelper.dummyClip : null; + } notifyTextClassifierLocked(clipboard, pkg, intendingUid); if (clipboard.primaryClip != null) { scheduleAutoClear(userId, intendingUid, intendingDeviceId); @@ -710,8 +715,13 @@ public ClipDescription getPrimaryClipDescription( } synchronized (mLock) { Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId); - return (clipboard != null && clipboard.primaryClip != null) - ? clipboard.primaryClip.getDescription() : null; + ClipDescription clipDesc = null; + if (clipboard != null && clipboard.primaryClip != null) { + clipDesc = isReadBlockedForPkg(intendingUid, callingPackage, userId, clipboard) + ? ClipboardAccessHelper.dummyClip.getDescription() + : clipboard.primaryClip.getDescription(); + } + return clipDesc; } } @@ -809,7 +819,8 @@ public boolean hasClipboardText( } synchronized (mLock) { Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId); - if (clipboard != null && clipboard.primaryClip != null) { + if (clipboard != null && clipboard.primaryClip != null + && !isReadBlockedForPkg(intendingUid, callingPackage, userId, clipboard)) { CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); return text != null && text.length() > 0; } @@ -838,7 +849,8 @@ public String getPrimaryClipSource( } synchronized (mLock) { Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId); - if (clipboard != null && clipboard.primaryClip != null) { + if (clipboard != null && clipboard.primaryClip != null + && !isReadBlockedForPkg(intendingUid, callingPackage, userId, clipboard)) { return clipboard.mPrimaryClipPackage; } return null; @@ -1047,6 +1059,14 @@ private void sendClipChangedBroadcast(Clipboard clipboard) { ListenerInfo li = (ListenerInfo) clipboard.primaryClipListeners.getBroadcastCookie(i); + if (isReadBlockedForPkg( + li.mUid, + li.mPackageName, + UserHandle.getUserId(li.mUid), + clipboard)) { + continue; + } + if (clipboardAccessAllowed( AppOpsManager.OP_READ_CLIPBOARD, li.mPackageName, @@ -1438,7 +1458,7 @@ private boolean isDefaultIme(int userId, String packageName) { */ @GuardedBy("mLock") private void showAccessNotificationLocked(String callingPackage, int uid, @UserIdInt int userId, - Clipboard clipboard) { + Clipboard clipboard, boolean isReadBlocked) { if (clipboard.primaryClip == null) { return; } @@ -1472,6 +1492,18 @@ private void showAccessNotificationLocked(String callingPackage, int uid, @UserI clipboard.deviceId) == uid) { return; } + + // Shows a system notification instead, when clipboard access is blocked. + if (isReadBlocked) { + String message = + getContext().getString(R.string.notif_clipboard_read_deny_title, callingPackage); + Slog.d(TAG, message); + Binder.withCleanCallingIdentity( + () -> ClipboardAccessHelper.maybeNotifyAccessDenied(getContext(), + callingPackage, userId)); + return; + } + // Don't notify if already notified for this uid and clip. if (clipboard.mNotifiedUids.get(uid)) { return; @@ -1615,4 +1647,9 @@ private TextClassificationManager createTextClassificationManagerAsUser(@UserIdI Context context = getContext().createContextAsUser(UserHandle.of(userId), /* flags= */ 0); return context.getSystemService(TextClassificationManager.class); } + + private boolean isReadBlockedForPkg(int uid, String pkg, int userId, Clipboard clipboard) { + return !UserHandle.isSameApp(uid, clipboard.primaryClipUid) + && ClipboardAccessHelper.isReadBlockedForPackage(getContext(), pkg, userId); + } }