diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1e6f13b34a0fb..cf0faeb20faf8 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4366,6 +4366,7 @@ package android.content.pm { method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; field @Nullable public final byte[] contactScopes; + field @Nullable public final byte[] micSpoofingConfig; field @Nullable public final byte[] storageScopes; } @@ -4379,6 +4380,7 @@ package android.content.pm { method @NonNull public android.content.pm.GosPackageState.Editor setContactScopes(@Nullable byte[]); method @NonNull public android.content.pm.GosPackageState.Editor setFlagState(int, boolean); method @NonNull public android.content.pm.GosPackageState.Editor setKillUidAfterApply(boolean); + method @NonNull public android.content.pm.GosPackageState.Editor setMicSpoofingConfig(@Nullable byte[]); method @NonNull public android.content.pm.GosPackageState.Editor setNotifyUidAfterApply(boolean); method @NonNull public android.content.pm.GosPackageState.Editor setPackageFlagState(int, boolean); method @NonNull public android.content.pm.GosPackageState.Editor setStorageScopes(@Nullable byte[]); @@ -4386,6 +4388,7 @@ package android.content.pm { public interface GosPackageStateFlag { field public static final int CONTACT_SCOPES_ENABLED = 5; // 0x5 + field public static final int MIC_SPOOFING_ENABLED = 29; // 0x1d field public static final int STORAGE_SCOPES_ENABLED = 0; // 0x0 } @@ -5276,6 +5279,7 @@ package android.ext { field public static final int HAS_READ_MEDIA_IMAGES_DECLARATION = 1024; // 0x400 field public static final int HAS_READ_MEDIA_VIDEO_DECLARATION = 2048; // 0x800 field public static final int HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION = 4096; // 0x1000 + field public static final int HAS_RECORD_AUDIO_DECLARATION = 8388608; // 0x800000 field public static final int HAS_WRITE_CONTACTS_DECLARATION = 2097152; // 0x200000 field public static final int HAS_WRITE_EXTERNAL_STORAGE_DECLARATION = 32; // 0x20 } @@ -5392,6 +5396,23 @@ package android.ext.dcl { } +package android.ext.micspoofing { + + public final class MicSpoofingApi { + method @NonNull public static byte[] buildCustomPathConfig(@NonNull String); + method @NonNull public static byte[] buildDefaultConfig(); + method @NonNull public static android.content.Intent createConfigActivityIntent(@NonNull String); + method @Nullable public static String getCustomAudioPathForApp(@NonNull String, int); + method @Nullable public static String getPath(@Nullable byte[]); + method public static int getSourceMode(@Nullable byte[]); + method @Nullable public static android.os.ParcelFileDescriptor openCustomSourceFdForSelf(); + field public static final int MODE_CUSTOM_PATH = 1; // 0x1 + field public static final int MODE_DEFAULT = 0; // 0x0 + field public static final int VERSION = 1; // 0x1 + } + +} + package android.ext.settings { public class BoolSetting extends android.ext.settings.Setting { diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java index ba92e565b13ae..daa8b9a5386dc 100644 --- a/core/java/android/app/ActivityThreadHooks.java +++ b/core/java/android/app/ActivityThreadHooks.java @@ -1,9 +1,9 @@ package android.app; -import android.annotation.Nullable; import android.content.Context; import android.content.pm.GosPackageState; import android.content.pm.SrtPermissions; +import android.content.pm.spoofing.MicSpoofing; import android.ext.dcl.DynCodeLoading; import android.location.HookedLocationManager; import android.os.Bundle; @@ -74,6 +74,7 @@ static void onBind2(Context appContext, Bundle appBindArgs) { static void onGosPackageStateChanged(Context ctx, GosPackageState state, boolean fromBind) { StorageScopesAppHooks.maybeEnable(state); ContactScopes.maybeEnable(ctx, state); + MicSpoofing.onGosPackageStateChanged(state); } static Service instantiateService(String className) { diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 94bb499c77f74..f0e76ccff0b3f 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -35,6 +35,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo.ForegroundServiceType; import android.content.res.Configuration; +import android.ext.micspoofing.MicSpoofingApi; import android.os.Build; import android.os.IBinder; import android.os.RemoteException; @@ -774,6 +775,12 @@ public final void setForeground(boolean isForeground) { public final void startForeground(int id, Notification notification) { try { final ComponentName comp = new ComponentName(this, mClassName); + var fgsType = MicSpoofingApi + .maybeReplaceFgServiceType(this, comp, FOREGROUND_SERVICE_TYPE_MANIFEST); + if (fgsType != FOREGROUND_SERVICE_TYPE_MANIFEST) { + startForeground(id, notification, fgsType); + return; + } mActivityManager.setServiceForeground( comp, mToken, id, notification, 0, FOREGROUND_SERVICE_TYPE_MANIFEST); @@ -875,6 +882,8 @@ public final void startForeground(int id, @NonNull Notification notification, try { final ComponentName comp = new ComponentName(this, mClassName); + foregroundServiceType = MicSpoofingApi + .maybeReplaceFgServiceType(this, comp, foregroundServiceType); mActivityManager.setServiceForeground( comp, mToken, id, notification, 0, foregroundServiceType); diff --git a/core/java/android/content/pm/AppPermissionUtils.java b/core/java/android/content/pm/AppPermissionUtils.java index 6d3a18f6359eb..0197abceaef83 100644 --- a/core/java/android/content/pm/AppPermissionUtils.java +++ b/core/java/android/content/pm/AppPermissionUtils.java @@ -19,6 +19,7 @@ import android.annotation.NonNull; import android.annotation.SystemApi; import android.app.compat.gms.GmsCompat; +import android.content.pm.spoofing.MicSpoofing; import com.android.internal.app.ContactScopes; import com.android.internal.app.StorageScopesAppHooks; @@ -53,6 +54,10 @@ public static boolean shouldSpoofSelfCheck(String permName) { } } + if (MicSpoofing.shouldSpoofSelfPermissionCheck(permName)) { + return true; + } + return false; } @@ -70,6 +75,10 @@ public static boolean shouldSpoofSelfAppOpCheck(int op) { return true; } + if (MicSpoofing.shouldSpoofSelfAppOpCheck(op)) { + return true; + } + return false; } @@ -108,6 +117,13 @@ private static int getSpoofablePermissionDflag(GosPackageState ps, String perm, } } + if (ps.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED)) { + int permDflag = MicSpoofing.getSpoofablePermissionDflag(perm); + if (permDflag != 0) { + return permDflag; + } + } + return 0; } diff --git a/core/java/android/content/pm/GosPackageState.java b/core/java/android/content/pm/GosPackageState.java index 91daf0b273226..f28c6c22e6b46 100644 --- a/core/java/android/content/pm/GosPackageState.java +++ b/core/java/android/content/pm/GosPackageState.java @@ -37,6 +37,8 @@ public final class GosPackageState implements Parcelable { public final byte[] storageScopes; @Nullable public final byte[] contactScopes; + @Nullable + public final byte[] micSpoofingConfig; /** * These flags are lazily derived from persistent state. They are intentionally skipped from * equals() and hashCode(). derivedFlags are stored here for performance reasons, to avoid @@ -63,15 +65,17 @@ public final class GosPackageState implements Parcelable { /** @hide */ public GosPackageState(long flagStorage1, long packageFlagStorage, - @Nullable byte[] storageScopes, @Nullable byte[] contactScopes) { + @Nullable byte[] storageScopes, @Nullable byte[] contactScopes, + @Nullable byte[] micSpoofingConfig) { this.flagStorage1 = flagStorage1; this.packageFlagStorage = packageFlagStorage; this.storageScopes = storageScopes; this.contactScopes = contactScopes; + this.micSpoofingConfig = micSpoofingConfig; } private static GosPackageState createEmpty() { - return new GosPackageState(0L, 0L, null, null); + return new GosPackageState(0L, 0L, null, null, null); } private static final int TYPE_NONE = 0; @@ -94,6 +98,7 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeLong(this.packageFlagStorage); dest.writeByteArray(storageScopes); dest.writeByteArray(contactScopes); + dest.writeByteArray(micSpoofingConfig); dest.writeInt(derivedFlags); } @@ -106,7 +111,7 @@ public GosPackageState createFromParcel(Parcel in) { case TYPE_NONE: return NONE; }; var res = new GosPackageState(in.readLong(), in.readLong(), - in.createByteArray(), in.createByteArray()); + in.createByteArray(), in.createByteArray(), in.createByteArray()); res.derivedFlags = in.readInt(); return res; } @@ -119,7 +124,7 @@ public GosPackageState[] newArray(int size) { @Override public int hashCode() { - return Long.hashCode(flagStorage1) + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) + Long.hashCode(packageFlagStorage); + return Long.hashCode(flagStorage1) + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) + Long.hashCode(packageFlagStorage) + Arrays.hashCode(micSpoofingConfig); } @Override @@ -142,6 +147,9 @@ public boolean equals(Object obj) { if (packageFlagStorage != o.packageFlagStorage) { return false; } + if (!Arrays.equals(micSpoofingConfig, o.micSpoofingConfig)) { + return false; + } return true; } @@ -236,6 +244,7 @@ public static class Editor { private long packageFlagStorage; private byte[] storageScopes; private byte[] contactScopes; + private byte[] micSpoofingConfig; private int editorFlags; /** @hide */ @@ -246,6 +255,7 @@ public Editor(GosPackageState s, String packageName, int userId) { this.packageFlagStorage = s.packageFlagStorage; this.storageScopes = s.storageScopes; this.contactScopes = s.contactScopes; + this.micSpoofingConfig = s.micSpoofingConfig; } @NonNull @@ -304,6 +314,12 @@ public Editor setContactScopes(@Nullable byte[] contactScopes) { return this; } + @NonNull + public Editor setMicSpoofingConfig(@Nullable byte[] micSpoofingConfig) { + this.micSpoofingConfig = micSpoofingConfig; + return this; + } + @NonNull public Editor killUidAfterApply() { return setKillUidAfterApply(true); @@ -334,7 +350,7 @@ public Editor setNotifyUidAfterApply(boolean v) { public boolean apply() { try { return ActivityThread.getPackageManager().setGosPackageState(packageName, userId, - new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes), + new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes, micSpoofingConfig), editorFlags); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/content/pm/GosPackageStateFlag.java b/core/java/android/content/pm/GosPackageStateFlag.java index 21abe5f9398d0..0e344c860e9f6 100644 --- a/core/java/android/content/pm/GosPackageStateFlag.java +++ b/core/java/android/content/pm/GosPackageStateFlag.java @@ -36,6 +36,7 @@ public interface GosPackageStateFlag { /** @hide */ int PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE = 26; /** @hide */ int SUPPRESS_PLAY_INTEGRITY_API_NOTIF = 27; /** @hide */ int BLOCK_PLAY_INTEGRITY_API = 28; + /* SysApi */ int MIC_SPOOFING_ENABLED = 29; /** @hide */ @IntDef(value = { @@ -64,6 +65,7 @@ public interface GosPackageStateFlag { PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE, SUPPRESS_PLAY_INTEGRITY_API_NOTIF, BLOCK_PLAY_INTEGRITY_API, + MIC_SPOOFING_ENABLED, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/android/content/pm/spoofing/MicSpoofing.java b/core/java/android/content/pm/spoofing/MicSpoofing.java new file mode 100644 index 0000000000000..ad95e005e7b5f --- /dev/null +++ b/core/java/android/content/pm/spoofing/MicSpoofing.java @@ -0,0 +1,68 @@ +package android.content.pm.spoofing; + +import android.Manifest; +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.DerivedPackageFlag; +import android.util.Log; + +/** @hide */ +public class MicSpoofing { + + private static final String TAG = "MicSpoofing"; + private static final boolean VERBOSE_LOGGING = false; + + private static volatile boolean isEnabled; + private static int gosPackageStateDerivedFlags; + + private MicSpoofing() { + } + + public static void onGosPackageStateChanged(GosPackageState gosPackageState) { + boolean shouldBeEnabled = gosPackageState.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + if (shouldBeEnabled == isEnabled) return; + + if (shouldBeEnabled) { + gosPackageStateDerivedFlags = gosPackageState.derivedFlags; + } + + if (VERBOSE_LOGGING) { + Log.d(TAG, "Changing mic spoofing to " + shouldBeEnabled); + } + + isEnabled = shouldBeEnabled; + } + + public static boolean isEnabled() { + return isEnabled; + } + + public static boolean shouldSpoofSelfPermissionCheck(@NonNull String permissionName) { + if (!isEnabled) return false; + + return shouldSpoofPermissionCheckInner(getSpoofablePermissionDflag(permissionName)); + } + + public static boolean shouldSpoofSelfAppOpCheck(int op) { + if (!isEnabled) return false; + + return op == AppOpsManager.OP_RECORD_AUDIO && shouldSpoofPermissionCheckInner( + DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION); + } + + public static int getSpoofablePermissionDflag(@NonNull String permissionName) { + if (permissionName.equals(Manifest.permission.RECORD_AUDIO)) { + return DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION; + } + + return 0; + } + + private static boolean shouldSpoofPermissionCheckInner(int permissionDflag) { + if (permissionDflag == 0) return false; + + return (gosPackageStateDerivedFlags & permissionDflag) != 0; + } +} diff --git a/core/java/android/ext/DerivedPackageFlag.java b/core/java/android/ext/DerivedPackageFlag.java index 5258acf924327..97563d4c0deb8 100644 --- a/core/java/android/ext/DerivedPackageFlag.java +++ b/core/java/android/ext/DerivedPackageFlag.java @@ -27,6 +27,7 @@ public interface DerivedPackageFlag { int HAS_READ_CONTACTS_DECLARATION = 1 << 20; int HAS_WRITE_CONTACTS_DECLARATION = 1 << 21; int HAS_GET_ACCOUNTS_DECLARATION = 1 << 22; + int HAS_RECORD_AUDIO_DECLARATION = 1 << 23; /** @hide */ @IntDef(flag = true, value = { @@ -47,6 +48,7 @@ public interface DerivedPackageFlag { HAS_READ_CONTACTS_DECLARATION, HAS_WRITE_CONTACTS_DECLARATION, HAS_GET_ACCOUNTS_DECLARATION, + HAS_RECORD_AUDIO_DECLARATION, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/android/ext/micspoofing/MicSpoofingApi.java b/core/java/android/ext/micspoofing/MicSpoofingApi.java new file mode 100644 index 0000000000000..e7f41f3523b4b --- /dev/null +++ b/core/java/android/ext/micspoofing/MicSpoofingApi.java @@ -0,0 +1,191 @@ +package android.ext.micspoofing; + +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.ActivityThread; +import android.app.Application; +import android.app.ApplicationPackageManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.util.Log; +import android.util.Slog; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** @hide */ +@SystemApi +public final class MicSpoofingApi { + + private static final String TAG = "MicSpoofingApi"; + + private static final String MEDIA_PROVIDER_OPEN_SOURCE_PATH = "mic_spoofing_source"; + + public static final int VERSION = 1; + + public static final int MODE_DEFAULT = 0; + public static final int MODE_CUSTOM_PATH = 1; + + private MicSpoofingApi() { + } + + @NonNull + public static Intent createConfigActivityIntent(@NonNull String targetPkg) { + var intent = new Intent(); + var componentName = ComponentName.createRelative( + ApplicationPackageManager.PERMISSION_CONTROLLER_RESOURCE_PACKAGE, + ".micspoofing.MicSpoofingActivity" + ); + intent.setComponent(componentName); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPkg); + + return intent; + } + + @NonNull + public static byte[] buildDefaultConfig() { + return new byte[]{VERSION, MODE_DEFAULT}; + } + + @NonNull + public static byte[] buildCustomPathConfig(@NonNull String path) { + var byteArrayOutputStream = new ByteArrayOutputStream(2 + path.length() * 3); + var dataOutputStream = new DataOutputStream(byteArrayOutputStream); + try { + dataOutputStream.writeByte(VERSION); + dataOutputStream.writeByte(MODE_CUSTOM_PATH); + dataOutputStream.writeUTF(path); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return byteArrayOutputStream.toByteArray(); + } + + public static int getSourceMode(@Nullable byte[] config) { + if (config == null || config.length < 2) { + return MODE_DEFAULT; + } + + if (config[0] != VERSION) { + return MODE_DEFAULT; + } + + return config[1] & 0xFF; + } + + @Nullable + public static String getCustomAudioPathForApp( + @NonNull String packageName, + int userId + ) { + var gosPs = GosPackageState.get(packageName, userId); + if (!gosPs.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED)) { + Log.d(TAG, "getCustomAudioPathForApp: MIC_SPOOFING_ENABLED not set for " + + packageName + " userId " + userId); + return null; + } + + return MicSpoofingApi.getPath(gosPs.micSpoofingConfig); + } + + @Nullable + public static String getPath(@Nullable byte[] config) { + if (config == null || config.length < 2) { + return null; + } + if (config[0] != VERSION) { + return null; + } + if ((config[1] & 0xFF) != MODE_CUSTOM_PATH) { + return null; + } + + var byteArrayInputStream = new ByteArrayInputStream(config, 2, config.length - 2); + var dataInputStream = new DataInputStream(byteArrayInputStream); + + try { + return dataInputStream.readUTF(); + } catch (IOException e) { + Slog.w(TAG, "Failed to read custom path from config", e); + return null; + } + } + + @Nullable + public static ParcelFileDescriptor openCustomSourceFdForSelf() { + Application application = ActivityThread.currentApplication(); + if (application == null) { + Log.w(TAG, "openCustomSourceFdForSelf: currentApplication is null"); + return null; + } + + Uri uri = MediaStore.AUTHORITY_URI + .buildUpon() + .appendPath(MEDIA_PROVIDER_OPEN_SOURCE_PATH) + .build(); + try { + return application.getContentResolver().openFileDescriptor(uri, "r"); + } catch (FileNotFoundException e) { + Log.w(TAG, "openCustomSourceFdForSelf: unable to open " + uri, e); + return null; + } catch (SecurityException e) { + Log.w(TAG, "openCustomSourceFdForSelf: access denied for " + uri, e); + return null; + } + } + + /** @hide */ + public static int maybeReplaceFgServiceType( + @NonNull Context context, + @NonNull ComponentName service, + @ServiceInfo.ForegroundServiceType int fgsType + ) { + if (fgsType != FOREGROUND_SERVICE_TYPE_MANIFEST + && (fgsType & FOREGROUND_SERVICE_TYPE_MICROPHONE) == 0) { + return fgsType; + } + + var isMicSpoofingEnabled = GosPackageState + .getForSelf(context) + .hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + + if (!isMicSpoofingEnabled) { + return fgsType; + } + + var effectiveFgsType = fgsType; + if (fgsType == FOREGROUND_SERVICE_TYPE_MANIFEST) { + ServiceInfo serviceInfo; + try { + serviceInfo = context.getPackageManager().getServiceInfo(service, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "getServiceInfo failed for " + service, e); + return fgsType; + } + effectiveFgsType = serviceInfo.getForegroundServiceType(); + if ((effectiveFgsType & FOREGROUND_SERVICE_TYPE_MICROPHONE) == 0) { + return fgsType; + } + } + + var result = effectiveFgsType & ~FOREGROUND_SERVICE_TYPE_MICROPHONE; + return result != 0 ? result : FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED; + } +} diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index f13720c699eb5..40db76edca383 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -1153,6 +1153,10 @@ public boolean setAutoRevokeExempted(@NonNull String packageName, boolean exempt */ //@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public boolean shouldShowRequestPermissionRationale(@NonNull String permissionName) { + if (AppPermissionUtils.shouldSpoofSelfCheck(permissionName)) { + return false; + } + try { final String packageName = mContext.getPackageName(); return mPermissionManager.shouldShowRequestPermissionRationale(packageName, diff --git a/core/tests/coretests/src/android/ext/micspoofing/MicSpoofingApiTest.java b/core/tests/coretests/src/android/ext/micspoofing/MicSpoofingApiTest.java new file mode 100644 index 0000000000000..4d5795d77366d --- /dev/null +++ b/core/tests/coretests/src/android/ext/micspoofing/MicSpoofingApiTest.java @@ -0,0 +1,42 @@ +package android.ext.micspoofing; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MicSpoofingApiTest { + + @Test + public void customPathConfigRoundTrip() { + var path = "/storage/emulated/0/Music/test.wav"; + + var config = MicSpoofingApi.buildCustomPathConfig(path); + + assertEquals(MicSpoofingApi.MODE_CUSTOM_PATH, MicSpoofingApi.getSourceMode(config)); + assertEquals(path, MicSpoofingApi.getPath(config)); + } + + @Test + public void defaultConfigHasNoPath() { + var config = MicSpoofingApi.buildDefaultConfig(); + + assertNull(MicSpoofingApi.getPath(config)); + assertEquals(MicSpoofingApi.MODE_DEFAULT, MicSpoofingApi.getSourceMode(config)); + } + + @Test + public void getPathReturnsNullForNull() { + assertNull(MicSpoofingApi.getPath(null)); + } + + @Test + public void getPathReturnsNullForTruncatedConfig() { + assertNull(MicSpoofingApi.getPath(new byte[]{MicSpoofingApi.VERSION})); + } + +} diff --git a/media/jni/Android.bp b/media/jni/Android.bp index ee63616a87fcf..d29bfe522889b 100644 --- a/media/jni/Android.bp +++ b/media/jni/Android.bp @@ -93,6 +93,8 @@ cc_library_shared { "libmediadrm", "libmediadrmmetrics_consumer", "libmediametrics", + "libmicspoofing", + "libmicspoofing_decoder", "libmtp", "libnativehelper", "libnativewindow", diff --git a/media/jni/android_media_MediaRecorder.cpp b/media/jni/android_media_MediaRecorder.cpp index 35781e4068687..9255ef0cbb6e2 100644 --- a/media/jni/android_media_MediaRecorder.cpp +++ b/media/jni/android_media_MediaRecorder.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include #include #include @@ -522,6 +524,24 @@ android_media_MediaRecorder_start(JNIEnv *env, jobject thiz) jniThrowException(env, "java/lang/IllegalStateException", NULL); return; } + + const int32_t uid = static_cast(getuid()); + if (mic_spoofing_is_enabled_for_uid(uid)) { + uint32_t sampleRate = 0; + uint32_t channelCount = 0; + const int pipeFd = mic_spoofing_start_streaming_decoder(uid, &sampleRate, + &channelCount); + if (pipeFd >= 0) { + const status_t err = mr->setMicSpoofingSourceFd(pipeFd, sampleRate, channelCount); + close(pipeFd); + if (err != OK) { + ALOGW("setMicSpoofingSourceFd failed: %d", err); + } + } else { + ALOGW("mic_spoofing_start_streaming_decoder failed for uid %" PRId32, uid); + } + } + process_media_recorder_call(env, mr->start(), "java/lang/RuntimeException", "start failed."); } @@ -917,6 +937,14 @@ static const JNINativeMethod gMethods[] = { // JNI_OnLoad in android_media_MediaPlayer.cpp int register_android_media_MediaRecorder(JNIEnv *env) { - return AndroidRuntime::registerNativeMethods(env, - "android/media/MediaRecorder", gMethods, NELEM(gMethods)); + int result = AndroidRuntime::registerNativeMethods( + env, "android/media/MediaRecorder", gMethods, NELEM(gMethods)); + if (result != 0) { + return result; + } + + // Register the app-process decoder factory once for MediaRecorder + // Mediaserver never loads libmedia_jni, so it remains fail-closed to silence + mic_spoofing_set_decoder_factory(&mic_spoofing_decoder_start); + return result; } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 989a0ce31a20a..4467fb52bab21 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -183,6 +183,8 @@ import android.content.IntentSender; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; @@ -2248,7 +2250,14 @@ private void setServiceForegroundInnerLocked(final ServiceRecord r, int id, r.app.getPid(), r.appInfo.uid, "startForeground"); } } - final int manifestType = r.serviceInfo.getForegroundServiceType(); + final var isMicSpoofingEnabled = LocalServices.getService(PackageManagerInternal.class) + .getGosPackageState(r.packageName, r.userId) + .hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + final var micSpoofingExtraFgsType = isMicSpoofingEnabled ? + ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED : 0; + + final var manifestType = r.serviceInfo.getForegroundServiceType() + | micSpoofingExtraFgsType; // If passed in foreground service type is FOREGROUND_SERVICE_TYPE_MANIFEST, // consider it is the same as manifest foreground service type. if (foregroundServiceType == FOREGROUND_SERVICE_TYPE_MANIFEST) { @@ -2902,6 +2911,17 @@ private Pair validateForegroundServiceType(ServiceRec @ForegroundServiceType int type, @ForegroundServiceType int defaultToType, @ForegroundServiceType int startType) { + if (type == ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED) { + final var isMicSpoofingEnabled = mAm + .getPackageManagerInternal() + .getGosPackageState(r.packageName, r.userId) + .hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + + if (isMicSpoofingEnabled) { + return Pair.create(FGS_TYPE_POLICY_CHECK_OK, null); + } + } + final ForegroundServiceTypePolicy policy = ForegroundServiceTypePolicy.getDefaultPolicy(); final ForegroundServiceTypePolicyInfo policyInfo = policy.getForegroundServiceTypePolicyInfo(type, defaultToType); diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermission.java b/services/core/java/com/android/server/pm/GosPackageStatePermission.java index 1013ec825b420..e7df9c4be52b5 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermission.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermission.java @@ -25,9 +25,10 @@ class GosPackageStatePermission { static final int FIELD_STORAGE_SCOPES = 0; static final int FIELD_CONTACT_SCOPES = 1; static final int FIELD_PACKAGE_FLAGS = 2; + static final int FIELD_MIC_SPOOFING_CONFIG = 3; @IntDef(prefix = "FIELD_", value = { - FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, FIELD_PACKAGE_FLAGS + FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, FIELD_PACKAGE_FLAGS, FIELD_MIC_SPOOFING_CONFIG }) @Retention(RetentionPolicy.SOURCE) @interface Field {} @@ -214,6 +215,7 @@ GosPackageState filterRead(GosPackageState ps) { , canReadField(FIELD_PACKAGE_FLAGS) ? ps.packageFlagStorage : default_.packageFlagStorage , canReadField(FIELD_STORAGE_SCOPES) ? ps.storageScopes : default_.storageScopes , canReadField(FIELD_CONTACT_SCOPES) ? ps.contactScopes : default_.contactScopes + , canReadField(FIELD_MIC_SPOOFING_CONFIG) ? ps.micSpoofingConfig : default_.micSpoofingConfig ); if (default_.equals(res)) { return default_; @@ -235,6 +237,7 @@ GosPackageState filterWrite(GosPackageState current, GosPackageState update) { , canWriteField(FIELD_PACKAGE_FLAGS) ? update.packageFlagStorage : current.packageFlagStorage , canWriteField(FIELD_STORAGE_SCOPES) ? update.storageScopes : current.storageScopes , canWriteField(FIELD_CONTACT_SCOPES) ? update.contactScopes : current.contactScopes + , canWriteField(FIELD_MIC_SPOOFING_CONFIG) ? update.micSpoofingConfig : current.micSpoofingConfig ); var default_ = GosPackageState.DEFAULT; if (default_.equals(res)) { diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java index f3fcd750cfe45..e0814d1b1adf7 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java @@ -27,6 +27,7 @@ import static android.content.pm.GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF; import static android.content.pm.GosPackageStateFlag.BLOCK_PLAY_INTEGRITY_API; import static android.content.pm.GosPackageStateFlag.CONTACT_SCOPES_ENABLED; +import static android.content.pm.GosPackageStateFlag.MIC_SPOOFING_ENABLED; import static android.content.pm.GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE; import static android.content.pm.GosPackageStateFlag.FORCE_MEMTAG; import static android.content.pm.GosPackageStateFlag.FORCE_MEMTAG_NON_DEFAULT; @@ -49,6 +50,7 @@ import static com.android.server.pm.GosPackageStatePermission.ALLOW_CROSS_USER_PROFILE_READS; import static com.android.server.pm.GosPackageStatePermission.ALLOW_CROSS_USER_PROFILE_WRITES; import static com.android.server.pm.GosPackageStatePermission.FIELD_CONTACT_SCOPES; +import static com.android.server.pm.GosPackageStatePermission.FIELD_MIC_SPOOFING_CONFIG; import static com.android.server.pm.GosPackageStatePermission.FIELD_PACKAGE_FLAGS; import static com.android.server.pm.GosPackageStatePermission.FIELD_STORAGE_SCOPES; @@ -76,7 +78,7 @@ static void init(PackageManagerService pm) { selfAccessPermission = builder() .readFlags(STORAGE_SCOPES_ENABLED, ALLOW_ACCESS_TO_OBB_DIRECTORY, - CONTACT_SCOPES_ENABLED) + CONTACT_SCOPES_ENABLED, MIC_SPOOFING_ENABLED) .readFlags(playIntegrityFlags) .readWriteFlag(PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE) .create(); @@ -95,22 +97,23 @@ static void init(PackageManagerService pm) { KnownSystemPackages ksp = KnownSystemPackages.get(pm.getContext()); builder() - .readFlag(STORAGE_SCOPES_ENABLED) - .readField(FIELD_STORAGE_SCOPES) + .readFlags(STORAGE_SCOPES_ENABLED, MIC_SPOOFING_ENABLED) + .readFields(FIELD_STORAGE_SCOPES, FIELD_MIC_SPOOFING_CONFIG) .apply(ksp.mediaProvider, computer); builder() .readFlag(CONTACT_SCOPES_ENABLED) .readField(FIELD_CONTACT_SCOPES) .apply(ksp.contactsProvider, computer); builder() - .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED) + .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED, MIC_SPOOFING_ENABLED) // user profiles are handled by the launcher instance in profile parent user .crossUserPermission(ALLOW_CROSS_USER_PROFILE_READS) .apply(ksp.launcher, computer); builder() - .readWriteFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED) + .readWriteFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED, + MIC_SPOOFING_ENABLED) .readWriteFields(FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, - FIELD_PACKAGE_FLAGS) + FIELD_PACKAGE_FLAGS, FIELD_MIC_SPOOFING_CONFIG) // in some cases PermissionController handles user profile from profile parent user .crossUserPermission(ALLOW_CROSS_USER_PROFILE_READS) .apply(ksp.permissionController, computer); @@ -121,6 +124,7 @@ static void init(PackageManagerService pm) { @GosPackageStateFlag.Enum int[] settingsReadWriteFlags = { ALLOW_ACCESS_TO_OBB_DIRECTORY, + MIC_SPOOFING_ENABLED, BLOCK_NATIVE_DEBUGGING_NON_DEFAULT, BLOCK_NATIVE_DEBUGGING, BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF, diff --git a/services/core/java/com/android/server/pm/GosPackageStatePersistence.java b/services/core/java/com/android/server/pm/GosPackageStatePersistence.java index ee90810b457cc..77dcb26e25582 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePersistence.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePersistence.java @@ -21,6 +21,7 @@ class GosPackageStatePersistence { private static final String ATTR_PACKAGE_FLAG_STORAGE = "package-flags"; private static final String ATTR_STORAGE_SCOPES = "storage-scopes"; private static final String ATTR_CONTACT_SCOPES = "contact-scopes"; + private static final String ATTR_MIC_SPOOFING_CONFIG = "mic-spoofing-config"; /** @see Settings#writePackageRestrictions */ static void serialize(PackageUserStateInternal packageUserState, TypedXmlSerializer serializer) throws IOException { @@ -50,6 +51,12 @@ private static void serializeInner(GosPackageState ps, TypedXmlSerializer serial serializer.attributeBytesHex(null, ATTR_CONTACT_SCOPES, s); } } + if (ps.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED)) { + byte[] s = ps.micSpoofingConfig; + if (s != null) { + serializer.attributeBytesHex(null, ATTR_MIC_SPOOFING_CONFIG, s); + } + } long packageFlagStorage = ps.packageFlagStorage; if (packageFlagStorage != 0L) { serializer.attributeLong(null, ATTR_PACKAGE_FLAG_STORAGE, ps.packageFlagStorage); @@ -61,6 +68,7 @@ static GosPackageState deserialize(TypedXmlPullParser parser) throws XmlPullPars long packageFlagStorage = 0L; byte[] storageScopes = null; byte[] contactScopes = null; + byte[] micSpoofingConfig = null; for (int i = 0, numAttr = parser.getAttributeCount(); i < numAttr; ++i) { String attr = parser.getAttributeName(i); @@ -73,11 +81,13 @@ static GosPackageState deserialize(TypedXmlPullParser parser) throws XmlPullPars storageScopes = parser.getAttributeBytesHex(i); case ATTR_CONTACT_SCOPES -> contactScopes = parser.getAttributeBytesHex(i); + case ATTR_MIC_SPOOFING_CONFIG -> + micSpoofingConfig = parser.getAttributeBytesHex(i); default -> Slog.e(TAG, "deserialize: unknown attribute " + attr); } } - return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes); + return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes, micSpoofingConfig); } // Compatibility with legacy serialized GosPackageState. @@ -91,7 +101,7 @@ static GosPackageState maybeDeserializeLegacy(TypedXmlPullParser parser) { long packageFlagStorage = parser.getAttributeLong(null, "GrapheneOS-package-flags", 0L); byte[] storageScopes = parser.getAttributeBytesHex(null, "GrapheneOS-storage-scopes", null); byte[] contactScopes = parser.getAttributeBytesHex(null, "GrapheneOS-contact-scopes", null); - return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes); + return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes, null); } private static long migrateLegacyFlags(int flags) { diff --git a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java index c5fcdea679ac8..e1debb7952aa5 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java @@ -173,7 +173,11 @@ private static void maybeDeriveFlags(Computer snapshot, GosPackageState gosPs, P return; } - if (!gosPs.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED) && !gosPs.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED)) { + boolean hasStorageScopesEnabledFlag = gosPs.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED); + boolean hasContactScopesEnabledFlag = gosPs.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED); + boolean hasMicSpoofingEnabledFlag = gosPs.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + + if (!hasStorageScopesEnabledFlag && !hasContactScopesEnabledFlag && !hasMicSpoofingEnabledFlag) { return; } @@ -276,6 +280,10 @@ private static int deriveFlags(int flags, AndroidPackage pkg) { case Manifest.permission.GET_ACCOUNTS: flags |= DerivedPackageFlag.HAS_GET_ACCOUNTS_DECLARATION; continue; + + case Manifest.permission.RECORD_AUDIO: + flags |= DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION; + continue; } } @@ -355,6 +363,8 @@ static int runShellCommand(PackageManagerShellCommand cmd) { ed.setStorageScopes(getByteArrArg(cmd)); case "set-contact-scopes" -> ed.setContactScopes(getByteArrArg(cmd)); + case "set-mic-spoofing-config" -> + ed.setMicSpoofingConfig(getByteArrArg(cmd)); case "set-kill-uid-after-apply" -> ed.setKillUidAfterApply(Boolean.parseBoolean(cmd.getNextArgRequired())); case "set-notify-uid-after-apply" -> diff --git a/services/core/java/com/android/server/pm/PackageManagerNative.java b/services/core/java/com/android/server/pm/PackageManagerNative.java index 20592797dfbcd..15e97ccbc1cb0 100644 --- a/services/core/java/com/android/server/pm/PackageManagerNative.java +++ b/services/core/java/com/android/server/pm/PackageManagerNative.java @@ -21,6 +21,7 @@ import static com.android.server.pm.PackageManagerService.TAG; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageStateFlag; import android.content.pm.IPackageManagerNative; import android.content.pm.IStagedApexObserver; import android.content.pm.PackageInfo; @@ -38,12 +39,22 @@ import android.util.Slog; import java.util.Arrays; +import java.util.function.IntSupplier; final class PackageManagerNative extends IPackageManagerNative.Stub { private final PackageManagerService mPm; + private final IntSupplier mCallingUidSupplier; + private final IntSupplier mLocalUidSupplier; PackageManagerNative(PackageManagerService pm) { + this(pm, Binder::getCallingUid, android.os.Process::myUid); + } + + PackageManagerNative(PackageManagerService pm, IntSupplier callingUidSupplier, + IntSupplier localUidSupplier) { mPm = pm; + mCallingUidSupplier = callingUidSupplier; + mLocalUidSupplier = localUidSupplier; } @Override @@ -260,4 +271,38 @@ public void onDeniedSpecialRuntimePermissionOp(String permissionName, int uid, S com.android.server.ext.MissingSpecialRuntimePermissionNotification .maybeShow(mPm.getContext(), permissionName, uid, packageName); } + + static boolean canAccessMicSpoofingStateForUid(int callingUid, int targetUid, int localUid) { + if (callingUid == targetUid || callingUid == localUid) { + return true; + } + + var callingAppId = UserHandle.getAppId(callingUid); + return callingAppId == android.os.Process.SYSTEM_UID + || callingAppId == android.os.Process.MEDIA_UID + || callingAppId == android.os.Process.AUDIOSERVER_UID; + } + + private void enforceMicSpoofingUidAccess(int uid) { + var callingUid = mCallingUidSupplier.getAsInt(); + if (canAccessMicSpoofingStateForUid(callingUid, uid, mLocalUidSupplier.getAsInt())) { + return; + } + + throw new SecurityException( + "UID " + callingUid + " cannot access mic spoofing state for UID " + uid); + } + + @Override + public boolean isMicSpoofingEnabledForUid(int uid) { + enforceMicSpoofingUidAccess(uid); + + var packages = mPm.snapshotComputer().getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + return false; + } + var userId = UserHandle.getUserId(uid); + var gosPs = GosPackageStatePmHooks.getUnfiltered(mPm, packages[0], userId); + return gosPs.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + } } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index e4958e83552bc..0584ce1faddd3 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -1509,7 +1509,8 @@ private static GosPackageState createTestGosPackageState() { // argument values are random return new GosPackageState(0xf0_bc_06_f1_f1_67_2e_b8L, 0xf4_93_53_00_98_c8_f0_0cL, hf.parseHex("2d f6 37 f2 90 39 da ef"), - hf.parseHex("8b 9d 61 a3 3e 45 12") + hf.parseHex("8b 9d 61 a3 3e 45 12"), + hf.parseHex("01 01 aa bb cc dd ee ff") ); } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerNativeMicSpoofingTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerNativeMicSpoofingTest.java new file mode 100644 index 0000000000000..53ba36d35a819 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerNativeMicSpoofingTest.java @@ -0,0 +1,213 @@ +package com.android.server.pm; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.micspoofing.MicSpoofingApi; +import android.os.UserHandle; +import android.util.ArrayMap; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.pkg.PackageUserStateInternal; + +import com.google.common.truth.Truth; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(AndroidJUnit4.class) +public class PackageManagerNativeMicSpoofingTest { + + private static final String TEST_PKG = "com.android.test.app"; + private static final int TEST_UID = UserHandle.getUid(10, 12345); + private static final String TEST_FUSE_PATH = "/storage/emulated/10/Music/test.wav"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private PackageManagerService packageManagerService; + + @Mock + private Computer computer; + + @Mock + private PackageStateInternal packageState; + + @Mock + private PackageUserStateInternal userState; + + @Test + public void canAccessMicSpoofingStateForUid_sameUid_returnsTrue() { + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + TEST_UID, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_callerIsLocalProcess_returnsTrue() { + var callingUid = UserHandle.getUid(11, 23456); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + callingUid, TEST_UID, callingUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_systemUidCrossUid_returnsTrue() { + var systemUid = UserHandle.getUid(0, android.os.Process.SYSTEM_UID); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + systemUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_mediaUidCrossUid_returnsTrue() { + var mediaUid = UserHandle.getUid(0, android.os.Process.MEDIA_UID); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + mediaUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_audioserverUidCrossUid_returnsTrue() { + var audioserverUid = UserHandle.getUid(0, android.os.Process.AUDIOSERVER_UID); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + audioserverUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_untrustedCrossUid_returnsFalse() { + var callingUid = UserHandle.getUid(11, 23456); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + callingUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_enabled_returnsTrue() { + mockUidState(createGosPackageState( + true, MicSpoofingApi.buildCustomPathConfig(TEST_FUSE_PATH))); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isTrue(); + } + + @Test + public void isMicSpoofingEnabledForUid_disabled_returnsFalse() { + mockUidState(createGosPackageState(false, null)); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_noPackages_returnsFalse() { + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(null); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_emptyPackages_returnsFalse() { + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(new String[0]); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_privilegedCrossUidCallers_allowed() { + var privilegedAppIds = new int[]{ + android.os.Process.SYSTEM_UID, + android.os.Process.MEDIA_UID, + android.os.Process.AUDIOSERVER_UID + }; + var localUid = UserHandle.getUid(0, 34567); + mockUidState(createGosPackageState( + true, MicSpoofingApi.buildCustomPathConfig(TEST_FUSE_PATH))); + + for (var appId : privilegedAppIds) { + var callingUid = UserHandle.getUid(11, appId); + var pmNative = new PackageManagerNative( + packageManagerService, + () -> callingUid, + () -> localUid + ); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isTrue(); + } + } + + @Test + public void isMicSpoofingEnabledForUid_untrustedCrossUid_throwsSecurityException() { + var callingUid = UserHandle.getUid(11, 23456); + var localUid = UserHandle.getUid(0, 34567); + var pmNative = new PackageManagerNative( + packageManagerService, + () -> callingUid, + () -> localUid + ); + + var exception = assertThrows( + SecurityException.class, + () -> pmNative.isMicSpoofingEnabledForUid(TEST_UID) + ); + Truth.assertThat(exception).hasMessageThat().contains("UID " + callingUid); + Mockito.verify(packageManagerService, never()).snapshotComputer(); + } + + private void mockUidState(GosPackageState gosPackageState) { + var userId = UserHandle.getUserId(TEST_UID); + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(new String[]{TEST_PKG}); + when(packageState.getUserStateOrDefault(userId)).thenReturn(userState); + when(userState.getGosPackageState()).thenReturn(gosPackageState); + var packageStates = new ArrayMap(); + packageStates.put(TEST_PKG, packageState); + Mockito.doReturn(packageStates).when(computer).getPackageStates(); + } + + private static GosPackageState createGosPackageState( + boolean enabled, + byte[] micSpoofingConfig + ) { + var flagStorage1 = enabled ? (1L << GosPackageStateFlag.MIC_SPOOFING_ENABLED) : 0L; + return new GosPackageState(flagStorage1, 0L, null, null, micSpoofingConfig); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java index 6da0cabe07c4c..5e9da9095e632 100644 --- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -30,6 +30,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.internal.widget.LockDomain.Primary; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; @@ -673,7 +674,7 @@ private void attemptSuccessfulUnlock(int userId) throws RemoteException { ArgumentCaptor.forClass(LockSettingsStateListener.class); verify(mLockSettingsInternal).registerLockSettingsStateListener(captor.capture()); LockSettingsStateListener listener = captor.getValue(); - listener.onAuthenticationSucceeded(userId); + listener.onAuthenticationSucceeded(userId, Primary); } else { mTrustManager.reportUnlockAttempt(/* successful= */ true, userId); } @@ -685,7 +686,7 @@ private void attemptFailedUnlock(int userId) throws RemoteException { ArgumentCaptor.forClass(LockSettingsStateListener.class); verify(mLockSettingsInternal).registerLockSettingsStateListener(captor.capture()); LockSettingsStateListener listener = captor.getValue(); - listener.onAuthenticationFailed(userId); + listener.onAuthenticationFailed(userId, Primary); } else { mTrustManager.reportUnlockAttempt(/* successful= */ false, userId); }