diff --git a/binder/build.gradle b/binder/build.gradle index 24dd0d9015d..ada6cca4434 100644 --- a/binder/build.gradle +++ b/binder/build.gradle @@ -23,14 +23,14 @@ android { } } } - compileSdkVersion 29 + compileSdkVersion 30 compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } defaultConfig { minSdkVersion 19 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java index 25f215be696..653ae90bd77 100644 --- a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java +++ b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java @@ -17,11 +17,14 @@ package io.grpc.binder; import android.annotation.SuppressLint; +import android.app.admin.DevicePolicyManager; +import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; import android.os.Build; +import android.os.Build.VERSION; import android.os.Process; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; @@ -173,6 +176,50 @@ public Status checkAuthorization(int uid) { }; } + /** + * Creates {@link SecurityPolicy} which checks if the app is a device owner app. See + * {@link DevicePolicyManager}. + */ + public static SecurityPolicy isDeviceOwner(Context applicationContext) { + DevicePolicyManager devicePolicyManager = + (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + return anyPackageWithUidSatisfies( + applicationContext, + pkg -> VERSION.SDK_INT >= 18 && devicePolicyManager.isDeviceOwnerApp(pkg), + "Rejected by device owner policy. No packages found for UID.", + "Rejected by device owner policy"); + } + + /** + * Creates {@link SecurityPolicy} which checks if the app is a profile owner app. See + * {@link DevicePolicyManager}. + */ + public static SecurityPolicy isProfileOwner(Context applicationContext) { + DevicePolicyManager devicePolicyManager = + (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + return anyPackageWithUidSatisfies( + applicationContext, + pkg -> VERSION.SDK_INT >= 21 && devicePolicyManager.isProfileOwnerApp(pkg), + "Rejected by profile owner policy. No packages found for UID.", + "Rejected by profile owner policy"); + } + + /** + * Creates {@link SecurityPolicy} which checks if the app is a profile owner app on an + * organization-owned device. See {@link DevicePolicyManager}. + */ + public static SecurityPolicy isProfileOwnerOnOrganizationOwnedDevice(Context applicationContext) { + DevicePolicyManager devicePolicyManager = + (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + return anyPackageWithUidSatisfies( + applicationContext, + pkg -> VERSION.SDK_INT >= 30 + && devicePolicyManager.isProfileOwnerApp(pkg) + && devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile(), + "Rejected by profile owner on organization-owned device policy. No packages found for UID.", + "Rejected by profile owner on organization-owned device policy"); + } + private static Status checkUidSignature( PackageManager packageManager, int uid, @@ -406,6 +453,29 @@ private static Status checkPermissions( return Status.OK; } + private static SecurityPolicy anyPackageWithUidSatisfies( + Context applicationContext, + Predicate condition, + String errorMessageForNoPackages, + String errorMessageForDenied) { + return new SecurityPolicy() { + @Override + public Status checkAuthorization(int uid) { + String[] packages = applicationContext.getPackageManager().getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + return Status.UNAUTHENTICATED.withDescription(errorMessageForNoPackages); + } + + for (String pkg : packages) { + if (condition.apply(pkg)) { + return Status.OK; + } + } + return Status.PERMISSION_DENIED.withDescription(errorMessageForDenied); + } + }; + } + /** * Checks if the SHA-256 hash of the {@code signature} matches one of the {@code * expectedSignatureSha256Hashes}. diff --git a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java index a26d4dd58af..909547a902b 100644 --- a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java +++ b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java @@ -24,10 +24,15 @@ import static com.google.common.truth.Truth.assertThat; import static org.robolectric.Shadows.shadowOf; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Process; import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; @@ -40,6 +45,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) public final class SecurityPoliciesTest { @@ -59,6 +65,7 @@ public final class SecurityPoliciesTest { private Context appContext; private PackageManager packageManager; + private DevicePolicyManager devicePolicyManager; private SecurityPolicy policy; @@ -66,6 +73,8 @@ public final class SecurityPoliciesTest { public void setUp() { appContext = ApplicationProvider.getApplicationContext(); packageManager = appContext.getPackageManager(); + devicePolicyManager = + (DevicePolicyManager) appContext.getSystemService(Context.DEVICE_POLICY_SERVICE); } @SuppressWarnings("deprecation") @@ -323,6 +332,171 @@ public void testHasPermissions_failsIfPackageDoesNotHavePermissions() throws Exc .contains(OTHER_UID_PACKAGE_NAME); } + @Test + @Config(sdk = 18) + public void testIsDeviceOwner_succeedsForDeviceOwner() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + shadowOf(devicePolicyManager) + .setDeviceOwner(new ComponentName(OTHER_UID_PACKAGE_NAME, "foo")); + + policy = SecurityPolicies.isDeviceOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); + } + + @Test + @Config(sdk = 18) + public void testIsDeviceOwner_failsForNotDeviceOwner() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + + policy = SecurityPolicies.isDeviceOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + + @Test + @Config(sdk = 18) + public void testIsDeviceOwner_failsWhenNoPackagesForUid() throws Exception { + policy = SecurityPolicies.isDeviceOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.UNAUTHENTICATED.getCode()); + } + + @Test + @Config(sdk = 17) + public void testIsDeviceOwner_failsForSdkLevelTooLow() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + + policy = SecurityPolicies.isDeviceOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + + @Test + @Config(sdk = 21) + public void testIsProfileOwner_succeedsForProfileOwner() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + shadowOf(devicePolicyManager) + .setProfileOwner(new ComponentName(OTHER_UID_PACKAGE_NAME, "foo")); + + policy = SecurityPolicies.isProfileOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); + } + + @Test + @Config(sdk = 21) + public void testIsProfileOwner_failsForNotProfileOwner() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + + policy = SecurityPolicies.isProfileOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + + @Test + @Config(sdk = 21) + public void testIsProfileOwner_failsWhenNoPackagesForUid() throws Exception { + policy = SecurityPolicies.isProfileOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.UNAUTHENTICATED.getCode()); + } + + @Test + @Config(sdk = 19) + public void testIsProfileOwner_failsForSdkLevelTooLow() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + + policy = SecurityPolicies.isProfileOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + + @Test + @Config(sdk = 30) + public void testIsProfileOwnerOnOrgOwned_succeedsForProfileOwnerOnOrgOwned() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + shadowOf(devicePolicyManager) + .setProfileOwner(new ComponentName(OTHER_UID_PACKAGE_NAME, "foo")); + shadowOf(devicePolicyManager).setOrganizationOwnedDeviceWithManagedProfile(true); + + policy = SecurityPolicies.isProfileOwnerOnOrganizationOwnedDevice(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); + + } + + @Test + @Config(sdk = 30) + public void testIsProfileOwnerOnOrgOwned_failsForProfileOwnerOnNonOrgOwned() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + shadowOf(devicePolicyManager) + .setProfileOwner(new ComponentName(OTHER_UID_PACKAGE_NAME, "foo")); + shadowOf(devicePolicyManager).setOrganizationOwnedDeviceWithManagedProfile(false); + + policy = SecurityPolicies.isProfileOwnerOnOrganizationOwnedDevice(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); + } + + @Test + @Config(sdk = 21) + public void testIsProfileOwnerOnOrgOwned_failsForNotProfileOwner() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + + policy = SecurityPolicies.isProfileOwnerOnOrganizationOwnedDevice(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + + @Test + @Config(sdk = 21) + public void testIsProfileOwnerOnOrgOwned_failsWhenNoPackagesForUid() throws Exception { + policy = SecurityPolicies.isProfileOwnerOnOrganizationOwnedDevice(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.UNAUTHENTICATED.getCode()); + } + + @Test + @Config(sdk = 29) + public void testIsProfileOwnerOnOrgOwned_failsForSdkLevelTooLow() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + + policy = SecurityPolicies.isProfileOwner(appContext); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + private static PackageInfoBuilder newBuilder() { return new PackageInfoBuilder(); }