| /* |
| * Copyright (C) 2020 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.compatibility.common.util; |
| |
| import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; |
| |
| import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity; |
| |
| import static org.junit.Assert.fail; |
| |
| import android.Manifest; |
| import android.content.Context; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PermissionInfo; |
| import android.content.pm.Signature; |
| import android.os.Build; |
| import android.os.UserHandle; |
| import android.permission.PermissionManager; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| public abstract class BaseDefaultPermissionGrantPolicyTest extends BusinessLogicTestCase { |
| public static final String LOG_TAG = "DefaultPermissionGrantPolicy"; |
| private static final String PLATFORM_PACKAGE_NAME = "android"; |
| |
| private static final String BRAND_PROPERTY = "ro.product.brand"; |
| private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); |
| |
| private Set<DefaultPermissionGrantException> mRemoteExceptions = new HashSet<>(); |
| |
| /** |
| * Returns whether this build is a CN build. |
| */ |
| public abstract boolean isCnBuild(); |
| |
| /** |
| * Returns whether this build is a CN build with GMS. |
| */ |
| public abstract boolean isCnGmsBuild(); |
| |
| /** |
| * Add default permissions for all applicable apps. |
| */ |
| public abstract void addDefaultSystemHandlerPermissions( |
| ArrayMap<String, PackageInfo> packagesToVerify, |
| SparseArray<UidState> pregrantUidStates) throws Exception; |
| |
| /** |
| * Return the names of all the runtime permissions to check for violations. |
| */ |
| public abstract Set<String> getRuntimePermissionNames(List<PackageInfo> packageInfos); |
| |
| /** |
| * Return the names of all the packages whose permissions can always be granted as fixed. |
| */ |
| public Set<String> getGrantAsFixedPackageNames(ArrayMap<String, PackageInfo> packagesToVerify) { |
| return Collections.emptySet(); |
| } |
| |
| /** |
| * Returns whether the permission name, as defined in |
| * {@link PermissionManager.SplitPermissionInfo#getNewPermissions()} |
| * should be considered a violation. |
| */ |
| public abstract boolean isSplitPermissionNameViolation(String permissionName); |
| |
| public void testDefaultGrantsWithRemoteExceptions(boolean preGrantsOnly) throws Exception { |
| List<PackageInfo> allPackages = getAllPackages(); |
| Set<String> runtimePermNames = getRuntimePermissionNames(allPackages); |
| ArrayMap<String, PackageInfo> packagesToVerify = |
| getMsdkTargetingPackagesUsingRuntimePerms(allPackages, runtimePermNames); |
| |
| // Ignore CTS infrastructure |
| packagesToVerify.remove("android.tradefed.contentprovider"); |
| |
| SparseArray<UidState> pregrantUidStates = new SparseArray<>(); |
| |
| addDefaultSystemHandlerPermissions(packagesToVerify, pregrantUidStates); |
| |
| // Add split permissions that were split from non-runtime permissions |
| if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q)) { |
| addSplitFromNonDangerousPermissions(packagesToVerify, pregrantUidStates); |
| } |
| |
| // Add exceptions |
| addExceptionsDefaultPermissions(packagesToVerify, runtimePermNames, pregrantUidStates); |
| |
| // packageName -> message -> [permission] |
| ArrayMap<String, ArrayMap<String, ArraySet<String>>> violations = new ArrayMap(); |
| |
| // Enforce default grants in the right state |
| checkDefaultGrantsInCorrectState(packagesToVerify, pregrantUidStates, violations); |
| |
| // Nothing else should have default grants |
| checkPackagesForUnexpectedGrants(packagesToVerify, runtimePermNames, violations, |
| preGrantsOnly); |
| |
| logPregrantUidStates(pregrantUidStates); |
| |
| // Bail if we found any violations |
| if (!violations.isEmpty()) { |
| fail(createViolationsErrorString(violations)); |
| } |
| } |
| |
| |
| /** |
| * Primarily invoked by business logic, set default permission grant exceptions for this |
| * instance of the test class. This is an alternative to downloading the encrypted xml |
| * file, a process which is now deprecated. |
| * |
| * @param pkg the package name |
| * @param sha256 the sha256 cert digest of the package |
| * @param permissions the set of permissions, formatted "permission_name fixed_boolean" |
| */ |
| public void setException(String pkg, String sha256, String... permissions) { |
| HashMap<String, Boolean> permissionsMap = new HashMap<>(); |
| for (String permissionString : permissions) { |
| String[] parts = permissionString.trim().split("\\s+"); |
| if (parts.length != 2) { |
| Log.e(LOG_TAG, "Unable to parse remote exception permission: " + permissionString); |
| return; |
| } |
| permissionsMap.put(parts[0], Boolean.valueOf(parts[1])); |
| } |
| mRemoteExceptions.add(new DefaultPermissionGrantException(pkg, sha256, permissionsMap)); |
| } |
| |
| /** |
| * Primarily invoked by business logic, set default permission grant exceptions for this |
| * instance of the test class. Also enables the supply of exception metadata. |
| * |
| * @param company the company name |
| * @param metadata the exception metadata |
| * @param pkg the package name |
| * @param sha256 the sha256 cert digest of the package |
| * @param permissions the set of permissions, formatted "permission_name fixed_boolean" |
| */ |
| public void setExceptionWithMetadata(String company, String metadata, String pkg, |
| String sha256, String... permissions) { |
| HashMap<String, Boolean> permissionsMap = new HashMap<>(); |
| for (String permissionString : permissions) { |
| String[] parts = permissionString.trim().split("\\s+"); |
| if (parts.length != 2) { |
| Log.e(LOG_TAG, "Unable to parse remote exception permission: " + permissionString); |
| return; |
| } |
| permissionsMap.put(parts[0], Boolean.valueOf(parts[1])); |
| } |
| mRemoteExceptions.add(new DefaultPermissionGrantException( |
| company, metadata, pkg, sha256, permissionsMap)); |
| } |
| |
| /** |
| * Primarily invoked by business logic, set default permission grant exceptions on CN Gms |
| * for this instance of the test class. This is an alternative to downloading the encrypted |
| * xml file, a process which is now deprecated. |
| * |
| * @param pkg the package name |
| * @param sha256 the sha256 cert digest of the package |
| * @param permissions the set of permissions, formatted "permission_name fixed_boolean" |
| */ |
| public void setCNGmsException(String pkg, String sha256, String... permissions) { |
| if (!isCnGmsBuild()) { |
| Log.e(LOG_TAG, "Regular GMS build, skip allowlisting: " + pkg); |
| return; |
| } |
| setException(pkg, sha256, permissions); |
| } |
| |
| /** |
| * Primarily invoked by business logic, set default permission grant exceptions on CN Gms |
| * for this instance of the test class. This is an alternative to downloading the encrypted |
| * xml file, a process which is now deprecated. |
| * |
| * @param company the company name |
| * @param metadata the exception metadata |
| * @param pkg the package name |
| * @param sha256 the sha256 cert digest of the package |
| * @param permissions the set of permissions, formatted "permission_name fixed_boolean" |
| */ |
| public void setCNGmsExceptionWithMetadata(String company, String metadata, String pkg, |
| String sha256, String... permissions) { |
| if (!isCnGmsBuild()) { |
| Log.e(LOG_TAG, "Regular GMS build, skip allowlisting: " + pkg); |
| return; |
| } |
| setExceptionWithMetadata(company, metadata, pkg, sha256, permissions); |
| } |
| |
| |
| public List<PackageInfo> getAllPackages() { |
| return getInstrumentation().getContext().getPackageManager() |
| .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES |
| | PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES); |
| } |
| |
| public static ArrayMap<String, PackageInfo> getMsdkTargetingPackagesUsingRuntimePerms( |
| List<PackageInfo> packageInfos, Set<String> runtimePermNames) { |
| ArrayMap<String, PackageInfo> packageInfoMap = new ArrayMap<>(); |
| |
| final int packageInfoCount = packageInfos.size(); |
| for (int i = 0; i < packageInfoCount; i++) { |
| PackageInfo packageInfo = packageInfos.get(i); |
| if (packageInfo.requestedPermissions == null) { |
| continue; |
| } |
| if (packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.M) { |
| continue; |
| } |
| for (String requestedPermission : packageInfo.requestedPermissions) { |
| if (runtimePermNames.contains(requestedPermission)) { |
| packageInfoMap.put(packageInfo.packageName, packageInfo); |
| break; |
| } |
| } |
| } |
| |
| return packageInfoMap; |
| } |
| |
| public static void addViolation( |
| Map<String, ArrayMap<String, ArraySet<String>>> violations, String packageName, |
| String permission, String message) { |
| if (!violations.containsKey(packageName)) { |
| violations.put(packageName, new ArrayMap<>()); |
| } |
| |
| if (!violations.get(packageName).containsKey(message)) { |
| violations.get(packageName).put(message, new ArraySet<>()); |
| } |
| |
| violations.get(packageName).get(message).add(permission); |
| } |
| |
| public static boolean isPackageOnSystemImage(PackageInfo packageInfo) { |
| return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; |
| } |
| |
| |
| public static String computePackageCertDigest(Signature signature) { |
| MessageDigest messageDigest; |
| try { |
| messageDigest = MessageDigest.getInstance("SHA256"); |
| } catch (NoSuchAlgorithmException e) { |
| /* can't happen */ |
| return null; |
| } |
| |
| messageDigest.update(signature.toByteArray()); |
| |
| final byte[] digest = messageDigest.digest(); |
| final int digestLength = digest.length; |
| final int charCount = 2 * digestLength; |
| |
| final char[] chars = new char[charCount]; |
| for (int i = 0; i < digestLength; i++) { |
| final int byteHex = digest[i] & 0xFF; |
| chars[i * 2] = HEX_ARRAY[byteHex >>> 4]; |
| chars[i * 2 + 1] = HEX_ARRAY[byteHex & 0x0F]; |
| } |
| return new String(chars); |
| } |
| |
| public static ArrayMap<String, Object> getPackageProperties(String packageName) { |
| ArrayMap<String, Object> properties = new ArrayMap(); |
| |
| PackageManager pm = getInstrumentation().getContext().getPackageManager(); |
| PackageInfo info = null; |
| try { |
| info = pm.getPackageInfo(packageName, |
| PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_SIGNATURES); |
| } catch (PackageManager.NameNotFoundException ignored) { |
| } |
| |
| properties.put("targetSDK", info.applicationInfo.targetSdkVersion); |
| properties.put("signature", computePackageCertDigest(info.signatures[0]).toUpperCase()); |
| properties.put("uid", UserHandle.getAppId(info.applicationInfo.uid)); |
| properties.put("priv app", info.applicationInfo.isPrivilegedApp()); |
| properties.put("persistent", ((info.applicationInfo.flags |
| & ApplicationInfo.FLAG_PERSISTENT) != 0) + "\n"); |
| properties.put("has platform signature", (pm.checkSignatures(info.packageName, |
| PLATFORM_PACKAGE_NAME) == PackageManager.SIGNATURE_MATCH)); |
| properties.put("on system image", isPackageOnSystemImage(info)); |
| |
| return properties; |
| } |
| |
| |
| public void addException(DefaultPermissionGrantException exception, |
| Set<String> runtimePermNames, Map<String, PackageInfo> packageInfos, |
| Set<String> platformSignedPackages, SparseArray<UidState> outUidStates) { |
| Log.v(LOG_TAG, "Adding exception for company " + exception.company |
| + ". Metadata: " + exception.metadata); |
| String packageName = exception.pkg; |
| PackageInfo packageInfo = packageInfos.get(packageName); |
| if (packageInfo == null) { |
| Log.v(LOG_TAG, "Trying to add exception to missing package:" + packageName); |
| |
| try { |
| // Do not overwhelm logging |
| Thread.sleep(10); |
| } catch (InterruptedException ignored) { |
| } |
| return; |
| } |
| if (!packageInfo.applicationInfo.isSystemApp()) { |
| if (isCnBuild() && exception.hasNonBrandSha256()) { |
| // Due to CN app removability requirement, allow non-system app pregrant exceptions, |
| // as long as they specify a hash (b/121209050) |
| } else { |
| Log.w(LOG_TAG, "Cannot pregrant permissions to non-system package:" + packageName); |
| return; |
| } |
| } |
| // If cert SHA-256 digest is specified it is used for verification, for user builds only |
| if (exception.hasNonBrandSha256()) { |
| String expectedDigest = exception.sha256.replace(":", "").toLowerCase(); |
| String packageDigest = computePackageCertDigest(packageInfo.signatures[0]); |
| if (PropertyUtil.isUserBuild() && !expectedDigest.equalsIgnoreCase(packageDigest)) { |
| Log.w(LOG_TAG, "SHA256 cert digest does not match for package: " + packageName |
| + ". Expected: " + expectedDigest.toUpperCase() + ", observed: " |
| + packageDigest.toUpperCase()); |
| return; |
| } |
| } else if (exception.hasBrand) { |
| // Rare case -- exception.sha256 is actually brand name, verify brand instead |
| String expectedBrand = exception.sha256; |
| String actualBrand = PropertyUtil.getProperty(BRAND_PROPERTY); |
| if (!expectedBrand.equalsIgnoreCase(actualBrand)) { |
| Log.w(LOG_TAG, String.format("Brand %s does not match for package: %s", |
| expectedBrand, packageName)); |
| return; |
| } |
| } else { |
| if (!platformSignedPackages.contains(packageName)) { |
| String packageDigest = computePackageCertDigest(packageInfo.signatures[0]); |
| Log.w(LOG_TAG, "Package is not signed with the platform certificate: " + packageName |
| + ". Package signature: " + packageDigest.toUpperCase()); |
| return; |
| } |
| } |
| |
| List<String> requestedPermissions = Arrays.asList(packageInfo.requestedPermissions); |
| for (Map.Entry<String, Boolean> entry : exception.permissions.entrySet()) { |
| String permission = entry.getKey(); |
| Boolean fixed = entry.getValue(); |
| if (!requestedPermissions.contains(permission)) { |
| Log.w(LOG_TAG, "Permission " + permission + " not requested by: " + packageName); |
| continue; |
| } |
| if (!runtimePermNames.contains(permission)) { |
| Log.w(LOG_TAG, "Permission:" + permission + " in not runtime"); |
| continue; |
| } |
| final int uid = packageInfo.applicationInfo.uid; |
| UidState uidState = outUidStates.get(uid); |
| if (uidState == null) { |
| uidState = new UidState(); |
| outUidStates.put(uid, uidState); |
| } |
| |
| for (String extendedPerm : extendBySplitPermissions(permission, |
| packageInfo.applicationInfo.targetSdkVersion)) { |
| uidState.overrideGrantedPermission(packageInfo.packageName, |
| permission.equals(extendedPerm) ? "exception" |
| : "exception (split from " + permission + ")", extendedPerm, fixed); |
| } |
| } |
| } |
| |
| |
| public static ArrayList<String> extendBySplitPermissions(String permission, int appTargetSdk) { |
| ArrayList<String> extendedPermissions = new ArrayList<>(); |
| extendedPermissions.add(permission); |
| |
| if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q)) { |
| Context context = getInstrumentation().getTargetContext(); |
| PermissionManager permissionManager = context.getSystemService(PermissionManager.class); |
| |
| for (PermissionManager.SplitPermissionInfo splitPerm : |
| permissionManager.getSplitPermissions()) { |
| if (splitPerm.getSplitPermission().equals(permission) |
| && splitPerm.getTargetSdk() > appTargetSdk) { |
| extendedPermissions.addAll(splitPerm.getNewPermissions()); |
| } |
| } |
| } |
| |
| return extendedPermissions; |
| } |
| |
| |
| public void setPermissionGrantState(String packageName, String permission, |
| boolean granted) { |
| try { |
| if (granted) { |
| getInstrumentation().getUiAutomation().grantRuntimePermission(packageName, |
| permission, android.os.Process.myUserHandle()); |
| } else { |
| getInstrumentation().getUiAutomation().revokeRuntimePermission(packageName, |
| permission, android.os.Process.myUserHandle()); |
| } |
| } catch (Exception e) { |
| /* do nothing - best effort */ |
| } |
| } |
| |
| public void addExceptionsDefaultPermissions(Map<String, PackageInfo> packageInfos, |
| Set<String> runtimePermNames, |
| SparseArray<UidState> outUidStates) throws Exception { |
| // Only use exceptions from business logic if they've been added |
| if (!mRemoteExceptions.isEmpty()) { |
| Log.d(LOG_TAG, String.format("Found %d remote exceptions", mRemoteExceptions.size())); |
| Set<String> platformSignedPackages = getPlatformSignedPackages(packageInfos); |
| for (DefaultPermissionGrantException dpge : mRemoteExceptions) { |
| addException(dpge, runtimePermNames, packageInfos, platformSignedPackages, |
| outUidStates); |
| } |
| } else { |
| Log.w(LOG_TAG, "Failed to retrieve remote default permission grant exceptions."); |
| } |
| } |
| |
| private Set<String> getPlatformSignedPackages(Map<String, PackageInfo> packageInfos) { |
| Set<String> platformSignedPackages = new ArraySet<>(); |
| PackageManager pm = getInstrumentation().getContext().getPackageManager(); |
| for (PackageInfo pkg : packageInfos.values()) { |
| boolean isPlatformSigned = pm.checkSignatures(pkg.packageName, PLATFORM_PACKAGE_NAME) |
| == PackageManager.SIGNATURE_MATCH; |
| if (isPlatformSigned) { |
| platformSignedPackages.add(pkg.packageName); |
| } |
| } |
| return platformSignedPackages; |
| } |
| |
| |
| // Permissions split from non dangerous permissions |
| private void addSplitFromNonDangerousPermissions(Map<String, PackageInfo> packageInfos, |
| SparseArray<UidState> outUidStates) { |
| Context context = getInstrumentation().getTargetContext(); |
| |
| for (PackageInfo pkg : packageInfos.values()) { |
| int targetSdk = pkg.applicationInfo.targetSandboxVersion; |
| int uid = pkg.applicationInfo.uid; |
| |
| for (String permission : pkg.requestedPermissions) { |
| PermissionInfo permInfo; |
| try { |
| permInfo = context.getPackageManager().getPermissionInfo(permission, 0); |
| } catch (PackageManager.NameNotFoundException ignored) { |
| // ignore permissions that are requested but not defined |
| continue; |
| } |
| |
| |
| // Permissions split from dangerous permission are granted when the original |
| // permission permission is granted; |
| if ((permInfo.getProtection() & PROTECTION_DANGEROUS) != 0) { |
| continue; |
| } |
| |
| for (String extendedPerm : extendBySplitPermissions(permission, targetSdk)) { |
| if (!isSplitPermissionNameViolation(extendedPerm)) { |
| continue; |
| } |
| |
| if (!extendedPerm.equals(permission)) { |
| UidState uidState = outUidStates.get(uid); |
| |
| if (uidState != null |
| && uidState.grantedPermissions.containsKey(extendedPerm)) { |
| // permission is already granted. Don't override the grant-state. |
| continue; |
| } |
| |
| appendPackagePregrantedPerms(pkg, "split from non-dangerous permission " |
| + permission, false, Collections.singleton(extendedPerm), |
| outUidStates); |
| } |
| } |
| } |
| } |
| } |
| |
| public static void appendPackagePregrantedPerms(PackageInfo packageInfo, String reason, |
| boolean fixed, Set<String> pregrantedPerms, SparseArray<UidState> outUidStates) { |
| final int uid = packageInfo.applicationInfo.uid; |
| UidState uidState = outUidStates.get(uid); |
| if (uidState == null) { |
| uidState = new UidState(); |
| outUidStates.put(uid, uidState); |
| } |
| for (String requestedPermission : packageInfo.requestedPermissions) { |
| if (pregrantedPerms.contains(requestedPermission)) { |
| uidState.addGrantedPermission(packageInfo.packageName, reason, requestedPermission, |
| fixed); |
| } |
| } |
| } |
| |
| public void logPregrantUidStates(SparseArray<UidState> pregrantUidStates) { |
| Log.i(LOG_TAG, "PREGRANT UID STATES"); |
| for (int i = 0; i < pregrantUidStates.size(); i++) { |
| Log.i(LOG_TAG, "uid: " + pregrantUidStates.keyAt(i) + " {"); |
| pregrantUidStates.valueAt(i).log(); |
| Log.i(LOG_TAG, "}"); |
| } |
| } |
| |
| public void checkDefaultGrantsInCorrectState(ArrayMap<String, PackageInfo> packagesToVerify, |
| SparseArray<UidState> pregrantUidStates, |
| Map<String, ArrayMap<String, ArraySet<String>>> violations) { |
| Set<String> grantAsFixedPackageNames = getGrantAsFixedPackageNames(packagesToVerify); |
| PackageManager packageManager = getInstrumentation().getContext().getPackageManager(); |
| for (PackageInfo packageInfo : packagesToVerify.values()) { |
| final int uid = packageInfo.applicationInfo.uid; |
| UidState uidState = pregrantUidStates.get(uid); |
| if (uidState == null) { |
| continue; |
| } |
| |
| final int grantCount = uidState.grantedPermissions.size(); |
| // make a modifiable list |
| List<String> requestedPermissions = new ArrayList<>( |
| Arrays.asList(packageInfo.requestedPermissions)); |
| for (int i = 0; i < grantCount; i++) { |
| String permission = uidState.grantedPermissions.keyAt(i); |
| |
| // If the package did not request this permissions, skip as |
| // it is requested by another package in the same UID. |
| if (!requestedPermissions.contains(permission)) { |
| continue; |
| } |
| |
| // Not granting the permission is OK, as we want to catch excessive grants |
| if (packageManager.checkPermission(permission, packageInfo.packageName) |
| != PackageManager.PERMISSION_GRANTED) { |
| continue; |
| } |
| |
| boolean grantBackFineLocation = false; |
| |
| // Special case: fine location implies coarse location, so we revoke |
| // fine location when verifying coarse to avoid interference. |
| if (permission.equals(Manifest.permission.ACCESS_COARSE_LOCATION) |
| && packageManager.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION, |
| packageInfo.packageName) == PackageManager.PERMISSION_GRANTED) { |
| setPermissionGrantState(packageInfo.packageName, |
| Manifest.permission.ACCESS_FINE_LOCATION, false); |
| grantBackFineLocation = true; |
| } |
| |
| setPermissionGrantState(packageInfo.packageName, permission, false); |
| |
| Boolean fixed = grantAsFixedPackageNames.contains(packageInfo.packageName) |
| || uidState.grantedPermissions.valueAt(i); |
| |
| // Weaker grant is fine, e.g. not-fixed instead of fixed. |
| if (!fixed && packageManager.checkPermission(permission, packageInfo.packageName) |
| == PackageManager.PERMISSION_GRANTED) { |
| addViolation(violations, packageInfo.packageName, permission, |
| "granted by default should be revocable"); |
| } |
| |
| setPermissionGrantState(packageInfo.packageName, permission, true); |
| |
| if (grantBackFineLocation) { |
| setPermissionGrantState(packageInfo.packageName, |
| Manifest.permission.ACCESS_FINE_LOCATION, true); |
| } |
| |
| // Now a small trick - pretend the package does not request this permission |
| // as we will later treat each granted runtime permissions as a violation. |
| requestedPermissions.remove(permission); |
| packageInfo.requestedPermissions = requestedPermissions.toArray( |
| new String[requestedPermissions.size()]); |
| } |
| } |
| } |
| |
| public void checkPackagesForUnexpectedGrants(Map<String, PackageInfo> packagesToVerify, |
| Set<String> runtimePermNames, |
| Map<String, ArrayMap<String, ArraySet<String>>> violations, |
| boolean preGrantsOnly) throws Exception { |
| PackageManager packageManager = getInstrumentation().getContext().getPackageManager(); |
| for (PackageInfo packageInfo : packagesToVerify.values()) { |
| for (String requestedPermission : packageInfo.requestedPermissions) { |
| // If another package in the UID can get the permission |
| // then it is fine for this package to have it - skip. |
| if (runtimePermNames.contains(requestedPermission) |
| && packageManager.checkPermission(requestedPermission, |
| packageInfo.packageName) == PackageManager.PERMISSION_GRANTED |
| && (!preGrantsOnly || (callWithShellPermissionIdentity(() -> |
| packageManager.getPermissionFlags( |
| requestedPermission, |
| packageInfo.packageName, |
| getInstrumentation().getTargetContext().getUser()) |
| & PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT) |
| != 0))) { |
| addViolation(violations, |
| packageInfo.packageName, requestedPermission, |
| "cannot be granted by default to package"); |
| } |
| } |
| } |
| } |
| |
| public String createViolationsErrorString( |
| ArrayMap<String, ArrayMap<String, ArraySet<String>>> violations) { |
| StringBuilder sb = new StringBuilder(); |
| |
| for (String packageName : violations.keySet()) { |
| sb.append("packageName: " + packageName + " {\n"); |
| for (Map.Entry<String, Object> property |
| : getPackageProperties(packageName).entrySet()) { |
| sb.append(" " + property.getKey() + ": " |
| + property.getValue().toString().trim() + "\n"); |
| } |
| for (String message : violations.get(packageName).keySet()) { |
| sb.append(" message: " + message + " {\n"); |
| for (String permission : violations.get(packageName).get(message)) { |
| sb.append(" permission: " + permission + "\n"); |
| } |
| sb.append(" }\n"); |
| } |
| sb.append("}\n"); |
| } |
| |
| return sb.toString(); |
| } |
| |
| public static class UidState { |
| public class GrantReason { |
| public final String reason; |
| public final boolean override; |
| public final Boolean fixed; |
| |
| GrantReason(String reason, boolean override, Boolean fixed) { |
| this.reason = reason; |
| this.override = override; |
| this.fixed = fixed; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| GrantReason that = (GrantReason) o; |
| return override == that.override |
| && Objects.equals(reason, that.reason) |
| && Objects.equals(fixed, that.fixed); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(reason, override, fixed); |
| } |
| } |
| |
| // packageName -> permission -> [reason] |
| public ArrayMap<String, ArrayMap<String, ArraySet<GrantReason>>> mGrantReasons = |
| new ArrayMap<>(); |
| public ArrayMap<String, Boolean> grantedPermissions = new ArrayMap<>(); |
| |
| public void log() { |
| for (String packageName : mGrantReasons.keySet()) { |
| Log.i(LOG_TAG, " packageName: " + packageName + " {"); |
| |
| for (Map.Entry<String, Object> property : |
| getPackageProperties(packageName).entrySet()) { |
| Log.i(LOG_TAG, " " + property.getKey() + ": " + property.getValue()); |
| } |
| |
| // Resort permission -> reason into reason -> permission |
| ArrayMap<String, ArraySet<GrantReason>> permissionsToReasons = |
| mGrantReasons.get(packageName); |
| ArrayMap<GrantReason, List<String>> reasonsToPermissions = new ArrayMap<>(); |
| for (String permission : permissionsToReasons.keySet()) { |
| for (GrantReason reason : permissionsToReasons.get(permission)) { |
| if (!reasonsToPermissions.containsKey(reason)) { |
| reasonsToPermissions.put(reason, new ArrayList<>()); |
| } |
| |
| reasonsToPermissions.get(reason).add(permission); |
| } |
| } |
| |
| for (Map.Entry<GrantReason, List<String>> reasonEntry |
| : reasonsToPermissions.entrySet()) { |
| GrantReason reason = reasonEntry.getKey(); |
| Log.i(LOG_TAG, " reason: " + reason.reason + " {"); |
| Log.i(LOG_TAG, " override: " + reason.override); |
| Log.i(LOG_TAG, " fixed: " + reason.fixed); |
| |
| Log.i(LOG_TAG, " permissions: ["); |
| for (String permission : reasonEntry.getValue()) { |
| Log.i(LOG_TAG, " " + permission + ","); |
| } |
| Log.i(LOG_TAG, " ]"); |
| Log.i(LOG_TAG, " }"); |
| |
| // Do not overwhelm log |
| try { |
| Thread.sleep(50); |
| } catch (InterruptedException e) { |
| // ignored |
| } |
| } |
| |
| Log.i(LOG_TAG, " }"); |
| } |
| } |
| |
| public void addGrantedPermission(String packageName, String reason, String permission, |
| Boolean fixed) { |
| Context context = getInstrumentation().getTargetContext(); |
| |
| // Add permissions split off from the permission to granted |
| try { |
| PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); |
| int targetSdk = info.applicationInfo.targetSdkVersion; |
| |
| for (String extendedPerm : extendBySplitPermissions(permission, targetSdk)) { |
| mergeGrantedPermission(packageName, extendedPerm.equals(permission) ? reason |
| : reason + " (split from " + permission + ")", extendedPerm, |
| fixed, false); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| // ignore |
| } |
| } |
| |
| public void overrideGrantedPermission(String packageName, String reason, String permission, |
| Boolean fixed) { |
| mergeGrantedPermission(packageName, reason, permission, fixed, true); |
| } |
| |
| public void mergeGrantedPermission(String packageName, String reason, String permission, |
| Boolean fixed, boolean override) { |
| if (!mGrantReasons.containsKey(packageName)) { |
| mGrantReasons.put(packageName, new ArrayMap<>()); |
| } |
| |
| if (!mGrantReasons.get(packageName).containsKey(permission)) { |
| mGrantReasons.get(packageName).put(permission, new ArraySet<>()); |
| } |
| |
| mGrantReasons.get(packageName).get(permission).add(new GrantReason(reason, override, |
| fixed)); |
| |
| Boolean oldFixed = grantedPermissions.get(permission); |
| if (oldFixed == null) { |
| grantedPermissions.put(permission, fixed); |
| } else { |
| if (override) { |
| if (oldFixed == Boolean.FALSE && fixed == Boolean.TRUE) { |
| Log.w(LOG_TAG, "override already granted permission " + permission + "(" |
| + fixed + ") for " + packageName); |
| grantedPermissions.put(permission, fixed); |
| } |
| } else { |
| if (oldFixed == Boolean.TRUE && fixed == Boolean.FALSE) { |
| Log.w(LOG_TAG, "add already granted permission " + permission + "(" |
| + fixed + ") to " + packageName); |
| grantedPermissions.put(permission, fixed); |
| } |
| } |
| } |
| } |
| } |
| |
| public static class DefaultPermissionGrantException { |
| |
| public static final String UNSET_PLACEHOLDER = "(UNSET)"; |
| public String company; |
| public String metadata; |
| public String pkg; |
| public String sha256; |
| public boolean hasBrand; // in rare cases, brand will be specified instead of SHA256 hash |
| public Map<String, Boolean> permissions = new HashMap<>(); |
| |
| public boolean hasNonBrandSha256() { |
| return !sha256.isEmpty() && !hasBrand; |
| } |
| |
| public DefaultPermissionGrantException(String pkg, String sha256, |
| Map<String, Boolean> permissions) { |
| this(UNSET_PLACEHOLDER, UNSET_PLACEHOLDER, pkg, sha256, permissions); |
| } |
| |
| public DefaultPermissionGrantException(String company, String metadata, String pkg, |
| String sha256, |
| Map<String, Boolean> permissions) { |
| this.company = company; |
| this.metadata = metadata; |
| this.pkg = pkg; |
| this.sha256 = sha256; |
| if (!sha256.isEmpty() && !sha256.contains(":")) { |
| hasBrand = true; // rough approximation of brand vs. SHA256 hash |
| } |
| this.permissions = permissions; |
| } |
| } |
| |
| } |