diff --git a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java index 1b4672165e1..879cc69167c 100644 --- a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java +++ b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java @@ -25,6 +25,7 @@ import android.os.Process; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.ExperimentalApi; import io.grpc.Status; @@ -216,4 +217,47 @@ public Status checkAuthorization(int uid) { } }; } + + /** + * Creates a {@link SecurityPolicy} which checks if the caller has all of the given permissions + * from {@code permissions}. + * + * @param permissions all permissions that the calling package needs to have + * @throws NullPointerException if any of the inputs are {@code null} + * @throws IllegalArgumentException if {@code permissions} is empty + */ + public static SecurityPolicy hasPermissions( + PackageManager packageManager, ImmutableSet permissions) { + Preconditions.checkNotNull(packageManager, "packageManager"); + Preconditions.checkNotNull(permissions, "permissions"); + Preconditions.checkArgument(!permissions.isEmpty(), "permissions"); + return new SecurityPolicy() { + @Override + public Status checkAuthorization(int uid) { + return checkPermissions(uid, packageManager, permissions); + } + }; + } + + private static Status checkPermissions( + int uid, PackageManager packageManager, ImmutableSet permissions) { + String[] packages = packageManager.getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + return Status.UNAUTHENTICATED.withDescription( + "Rejected by permission check security policy. No packages found for uid"); + } + for (String pkg : packages) { + for (String permission : permissions) { + if (packageManager.checkPermission(permission, pkg) != PackageManager.PERMISSION_GRANTED) { + return Status.PERMISSION_DENIED.withDescription( + "Rejected by permission check security policy. " + + pkg + + " does not have permission " + + permission); + } + } + } + + return Status.OK; + } } diff --git a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java index dbac29681b9..bff414db60f 100644 --- a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java +++ b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java @@ -16,6 +16,11 @@ package io.grpc.binder; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.truth.Truth.assertThat; import static org.robolectric.Shadows.shadowOf; @@ -26,8 +31,9 @@ import android.os.Process; import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import io.grpc.Status; -import io.grpc.binder.SecurityPolicy; +import java.util.HashMap; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,7 +45,6 @@ public final class SecurityPoliciesTest { private static final int MY_UID = Process.myUid(); private static final int OTHER_UID = MY_UID + 1; private static final int OTHER_UID_SAME_SIGNATURE = MY_UID + 2; - private static final int OTHER_UID_NO_SIGNATURE = MY_UID + 3; private static final int OTHER_UID_UNKNOWN = MY_UID + 4; private static final String PERMISSION_DENIED_REASONS = "some reasons"; @@ -49,7 +54,6 @@ public final class SecurityPoliciesTest { private static final String OTHER_UID_PACKAGE_NAME = "other.package"; private static final String OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME = "other.package.samesignature"; - private static final String OTHER_UID_NO_SIGNATURE_PACKAGE_NAME = "other.package.nosignature"; private Context appContext; private PackageManager packageManager; @@ -60,24 +64,22 @@ public final class SecurityPoliciesTest { public void setUp() { appContext = ApplicationProvider.getApplicationContext(); packageManager = appContext.getPackageManager(); - installPackage(MY_UID, appContext.getPackageName(), SIG1); - installPackage(OTHER_UID, OTHER_UID_PACKAGE_NAME, SIG2); - installPackage(OTHER_UID_SAME_SIGNATURE, OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME, SIG1); - installPackage(OTHER_UID_NO_SIGNATURE, OTHER_UID_NO_SIGNATURE_PACKAGE_NAME); } @SuppressWarnings("deprecation") - private void installPackage(int uid, String packageName, Signature... signatures) { - PackageInfo info = new PackageInfo(); - info.packageName = packageName; - info.signatures = signatures; - shadowOf(packageManager).installPackage(info); - shadowOf(packageManager).setPackagesForUid(uid, packageName); + private void installPackages(int uid, PackageInfo... packageInfo) { + String[] packageNames = new String[packageInfo.length]; + for (int i = 0; i < packageInfo.length; i++) { + shadowOf(packageManager).installPackage(packageInfo[i]); + packageNames[i] = packageInfo[i].packageName; + } + shadowOf(packageManager).setPackagesForUid(uid, packageNames); } @Test public void testInternalOnly() throws Exception { policy = SecurityPolicies.internalOnly(); + assertThat(policy.checkAuthorization(MY_UID).getCode()).isEqualTo(Status.OK.getCode()); assertThat(policy.checkAuthorization(OTHER_UID).getCode()) .isEqualTo(Status.PERMISSION_DENIED.getCode()); @@ -99,6 +101,11 @@ public void testPermissionDenied() throws Exception { @Test public void testHasSignature_succeedsIfPackageNameAndSignaturesMatch() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + policy = SecurityPolicies.hasSignature(packageManager, OTHER_UID_PACKAGE_NAME, SIG2); // THEN UID for package that has SIG2 will be authorized @@ -107,6 +114,14 @@ public void testHasSignature_succeedsIfPackageNameAndSignaturesMatch() @Test public void testHasSignature_failsIfPackageNameDoesNotMatch() throws Exception { + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME) + .setSignatures(SIG1) + .build(); + + installPackages(OTHER_UID_SAME_SIGNATURE, info); + policy = SecurityPolicies.hasSignature(packageManager, appContext.getPackageName(), SIG1); // THEN UID for package that has SIG1 but different package name will not be authorized @@ -116,6 +131,11 @@ public void testHasSignature_failsIfPackageNameDoesNotMatch() throws Exception { @Test public void testHasSignature_failsIfSignatureDoesNotMatch() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + policy = SecurityPolicies.hasSignature(packageManager, OTHER_UID_PACKAGE_NAME, SIG1); // THEN UID for package that doesn't have SIG1 will not be authorized @@ -126,6 +146,11 @@ public void testHasSignature_failsIfSignatureDoesNotMatch() throws Exception { @Test public void testOneOfSignatures_succeedsIfPackageNameAndSignaturesMatch() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + policy = SecurityPolicies.oneOfSignatures( packageManager, OTHER_UID_PACKAGE_NAME, ImmutableList.of(SIG2)); @@ -136,6 +161,14 @@ public void testOneOfSignatures_succeedsIfPackageNameAndSignaturesMatch() @Test public void testOneOfSignature_failsIfAllSignaturesDoNotMatch() throws Exception { + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME) + .setSignatures(SIG1) + .build(); + + installPackages(OTHER_UID_SAME_SIGNATURE, info); + policy = SecurityPolicies.oneOfSignatures( packageManager, @@ -150,11 +183,14 @@ public void testOneOfSignature_failsIfAllSignaturesDoNotMatch() throws Exception @Test public void testOneOfSignature_succeedsIfPackageNameAndOneOfSignaturesMatch() throws Exception { + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + + installPackages(OTHER_UID, info); + policy = SecurityPolicies.oneOfSignatures( - packageManager, - OTHER_UID_PACKAGE_NAME, - ImmutableList.of(SIG1, SIG2)); + packageManager, OTHER_UID_PACKAGE_NAME, ImmutableList.of(SIG1, SIG2)); // THEN UID for package that has SIG2 will be authorized assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); @@ -164,12 +200,177 @@ public void testOneOfSignature_succeedsIfPackageNameAndOneOfSignaturesMatch() public void testHasSignature_failsIfUidUnknown() throws Exception { policy = SecurityPolicies.hasSignature( - packageManager, - appContext.getPackageName(), - SIG1); + packageManager, + appContext.getPackageName(), + SIG1); assertThat(policy.checkAuthorization(OTHER_UID_UNKNOWN).getCode()) - .isEqualTo(Status.UNAUTHENTICATED.getCode()); + .isEqualTo(Status.UNAUTHENTICATED.getCode()); + } + + @Test + public void testHasPermissions_sharedUserId_succeedsIfAllPackageHavePermissions() + throws Exception { + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_PACKAGE_NAME) + .setPermission(ACCESS_FINE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(ACCESS_COARSE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .build(); + + PackageInfo infoSamePerms = + newBuilder() + .setPackageName(OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME) + .setPermission(ACCESS_FINE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(ACCESS_COARSE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .build(); + + installPackages(OTHER_UID, info, infoSamePerms); + + policy = + SecurityPolicies.hasPermissions( + packageManager, ImmutableSet.of(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)); + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); + } + + @Test + public void testHasPermissions_sharedUserId_failsIfOnePackageHasNoPermissions() throws Exception { + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_PACKAGE_NAME) + .setPermission(ACCESS_FINE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .build(); + + PackageInfo infoNoPerms = + newBuilder() + .setPackageName(OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME) + .setPermission(ACCESS_FINE_LOCATION, 0) + .build(); + + installPackages(OTHER_UID, info, infoNoPerms); + + policy = SecurityPolicies.hasPermissions(packageManager, ImmutableSet.of(ACCESS_FINE_LOCATION)); + assertThat(policy.checkAuthorization(OTHER_UID).getCode()) + .isEqualTo(Status.PERMISSION_DENIED.getCode()); + assertThat(policy.checkAuthorization(OTHER_UID).getDescription()) + .contains(ACCESS_FINE_LOCATION); + assertThat(policy.checkAuthorization(OTHER_UID).getDescription()) + .contains(OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME); + } + + @Test + public void testHasPermissions_succeedsIfPackageHasPermissions() throws Exception { + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_PACKAGE_NAME) + .setPermission(ACCESS_FINE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(ACCESS_COARSE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(WRITE_EXTERNAL_STORAGE, 0) + .build(); + + installPackages(OTHER_UID, info); + + policy = + SecurityPolicies.hasPermissions( + packageManager, ImmutableSet.of(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)); + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); + } + + @Test + public void testHasPermissions_failsIfPackageDoesNotHaveOnePermission() throws Exception { + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_PACKAGE_NAME) + .setPermission(ACCESS_FINE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(ACCESS_COARSE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(WRITE_EXTERNAL_STORAGE, 0) + .build(); + + installPackages(OTHER_UID, info); + + policy = + SecurityPolicies.hasPermissions( + packageManager, ImmutableSet.of(ACCESS_FINE_LOCATION, WRITE_EXTERNAL_STORAGE)); + assertThat(policy.checkAuthorization(OTHER_UID).getCode()) + .isEqualTo(Status.PERMISSION_DENIED.getCode()); + assertThat(policy.checkAuthorization(OTHER_UID).getDescription()) + .contains(WRITE_EXTERNAL_STORAGE); + assertThat(policy.checkAuthorization(OTHER_UID).getDescription()) + .contains(OTHER_UID_PACKAGE_NAME); + } + + @Test + public void testHasPermissions_failsIfPackageDoesNotHavePermissions() throws Exception { + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_PACKAGE_NAME) + .setPermission(ACCESS_FINE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(ACCESS_COARSE_LOCATION, REQUESTED_PERMISSION_GRANTED) + .setPermission(WRITE_EXTERNAL_STORAGE, 0) + .build(); + + installPackages(OTHER_UID, info); + + policy = + SecurityPolicies.hasPermissions(packageManager, ImmutableSet.of(WRITE_EXTERNAL_STORAGE)); + assertThat(policy.checkAuthorization(OTHER_UID).getCode()) + .isEqualTo(Status.PERMISSION_DENIED.getCode()); + assertThat(policy.checkAuthorization(OTHER_UID).getDescription()) + .contains(WRITE_EXTERNAL_STORAGE); + assertThat(policy.checkAuthorization(OTHER_UID).getDescription()) + .contains(OTHER_UID_PACKAGE_NAME); + } + + private static PackageInfoBuilder newBuilder() { + return new PackageInfoBuilder(); + } + + private static class PackageInfoBuilder { + private String packageName; + private Signature[] signatures; + private final HashMap permissions = new HashMap<>(); + + public PackageInfoBuilder setPackageName(String packageName) { + this.packageName = packageName; + return this; + } + + public PackageInfoBuilder setPermission(String permissionName, int permissionFlag) { + this.permissions.put(permissionName, permissionFlag); + return this; + } + + public PackageInfoBuilder setSignatures(Signature... signatures) { + this.signatures = signatures; + return this; + } + + public PackageInfo build() { + checkState(this.packageName != null, "packageName is a mandatory field"); + + PackageInfo packageInfo = new PackageInfo(); + + packageInfo.packageName = this.packageName; + + if (this.signatures != null) { + packageInfo.signatures = this.signatures; + } + + if (!this.permissions.isEmpty()) { + String[] requestedPermissions = + this.permissions.keySet().toArray(new String[this.permissions.size()]); + int[] requestedPermissionsFlags = new int[requestedPermissions.length]; + + for (int i = 0; i < requestedPermissions.length; i++) { + requestedPermissionsFlags[i] = this.permissions.get(requestedPermissions[i]); + } + + packageInfo.requestedPermissions = requestedPermissions; + packageInfo.requestedPermissionsFlags = requestedPermissionsFlags; + } + + return packageInfo; + } } @Test