1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.service.common; 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.content.pm.PackageInfo; 22 import android.content.pm.PackageManager; 23 import android.content.pm.Signature; 24 import android.content.pm.SigningInfo; 25 26 import androidx.annotation.Nullable; 27 28 import com.android.adservices.LogUtil; 29 import com.android.internal.annotations.VisibleForTesting; 30 31 import java.security.MessageDigest; 32 import java.security.NoSuchAlgorithmException; 33 import java.util.Arrays; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Objects; 37 import java.util.stream.Collectors; 38 39 /** Utility class to handle AllowList for Apps and SDKs. */ 40 public class AllowLists { 41 @VisibleForTesting public static final String ALLOW_ALL = "*"; 42 43 private static final String SPLITTER = ","; 44 private static final String HASH_ALGORITHM = "SHA-256"; 45 46 /** Returns whether all entities are allowlisted or not based on the given {@code allowList}. */ doesAllowListAllowAll(@onNull String allowList)47 public static boolean doesAllowListAllowAll(@NonNull String allowList) { 48 Objects.requireNonNull(allowList); 49 return ALLOW_ALL.equals(allowList); 50 } 51 52 /** Splits the given {@code allowList} into the list of entities allowed. */ splitAllowList(@onNull String allowList)53 public static List<String> splitAllowList(@NonNull String allowList) { 54 Objects.requireNonNull(allowList); 55 56 if (allowList.trim().isEmpty()) { 57 return Collections.emptyList(); 58 } 59 60 return Arrays.stream(allowList.split(SPLITTER)) 61 .map(String::trim) 62 .collect(Collectors.toList()); 63 } 64 65 /** 66 * A utility to check if an app package exists in the provided allow-list. The allow-list to 67 * search is split by {@link #SPLITTER} without any white spaces. E.g. of a valid allow list - 68 * "abc.package1.app,com.package2.app,com.package3.xyz" If the provided parameter {@code 69 * appPackageName} exists in the allow-list (e.g. com.package2.app), then the method returns 70 * true, false otherwise. 71 */ isPackageAllowListed( @onNull String allowList, @NonNull String appPackageName)72 public static boolean isPackageAllowListed( 73 @NonNull String allowList, @NonNull String appPackageName) { 74 if (ALLOW_ALL.equals(allowList)) { 75 return true; 76 } 77 78 // TODO(b/237686242): Cache the AllowList so that we don't need to read from Flags and split 79 // on every API call. 80 return Arrays.stream(allowList.split(SPLITTER)) 81 .map(String::trim) 82 .anyMatch(packageName -> packageName.equals(appPackageName)); 83 } 84 85 /** 86 * A utility to check if all app signatures exist in the provided allow-list. The allow-list to 87 * search is split by {@link #SPLITTER} without any white spaces. 88 * 89 * @param context the Context 90 * @param signatureAllowList the list of signatures that is allowed. 91 * @param appPackageName the package name of an app 92 * @return true if this app is allowed. Otherwise, it returns false. 93 */ isSignatureAllowListed( @onNull Context context, @NonNull String signatureAllowList, @NonNull String appPackageName)94 public static boolean isSignatureAllowListed( 95 @NonNull Context context, 96 @NonNull String signatureAllowList, 97 @NonNull String appPackageName) { 98 if (ALLOW_ALL.equals(signatureAllowList)) { 99 return true; 100 } 101 102 byte[] appSignatureHash = getAppSignatureHash(context, appPackageName); 103 104 // App must have signatures queried from Package Manager in order to use PPAPI. Otherwise, 105 // it is not allowed. 106 if (appSignatureHash == null) { 107 return false; 108 } 109 110 String hexSignature = toHexString(appSignatureHash); 111 112 LogUtil.v("App %s has signature(s) as %s", appPackageName, hexSignature); 113 114 // TODO(b/237686242): Cache the AllowList so that we don't need to read from Flags and split 115 // on every API call. 116 return Arrays.stream(signatureAllowList.split(SPLITTER)) 117 .map(String::trim) 118 .anyMatch(signature -> signature.equals(hexSignature)); 119 } 120 121 /** 122 * Get Hash for the signature of an app. Most of the methods invoked are from {@link 123 * PackageManager}. 124 * 125 * @param context the Context 126 * @param packageName package name of the app 127 * @return the hash in byte array format to represent the signature of an app. Returns {@code 128 * null} if app cannot be fetched from package manager or there is no {@link 129 * PackageInfo}/{@link SigningInfo} associated with this app. 130 */ 131 @VisibleForTesting 132 @Nullable getAppSignatureHash( @onNull Context context, @NonNull String packageName)133 public static byte[] getAppSignatureHash( 134 @NonNull Context context, @NonNull String packageName) { 135 PackageInfo packageInfo; 136 try { 137 packageInfo = 138 context.getPackageManager() 139 .getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES); 140 } catch (PackageManager.NameNotFoundException e) { 141 LogUtil.e("Package %s is not found in Package Manager!", packageName); 142 return null; 143 } 144 if (packageInfo == null) { 145 LogUtil.w("There is not package info for package %s", packageName); 146 return null; 147 } 148 149 SigningInfo signingInfo = packageInfo.signingInfo; 150 if (signingInfo == null) { 151 LogUtil.w( 152 "There is no signing info for package %s. Please check the signature!", 153 packageName); 154 return null; 155 } 156 157 Signature[] signatures = signingInfo.getSigningCertificateHistory(); 158 if (signatures == null || signatures.length == 0) { 159 LogUtil.w( 160 "There is no signature fetched from signing info for package %s.", packageName); 161 return null; 162 } 163 164 byte[] signatureHash = null; 165 // Current signature is at the last index of the history. 166 // This AllowList mechanism is actually not design for authentication. We only use it for 167 // system health and decide who can participate in our beta release. In this case, it's fine 168 // to just use whatever the current signing key is for all the apps that can participate 169 // in the beta 1 without needing to worry about subsequent rotations. 170 Signature currentSignature = signatures[signatures.length - 1]; 171 try { 172 MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM); 173 signatureHash = digest.digest(currentSignature.toByteArray()); 174 } catch (NoSuchAlgorithmException e) { 175 LogUtil.e("SHA not available: " + e.getMessage()); 176 } 177 178 return signatureHash; 179 } 180 181 /** 182 * Convert byte array to a hex string. 183 * 184 * <p>Mostly copied from frameworks/base/core/java/android/content/pm/Signature.java 185 * 186 * @param bytes the byte array 187 * @return the hex string format of the byte array 188 */ 189 @VisibleForTesting toHexString(byte[] bytes)190 protected static String toHexString(byte[] bytes) { 191 StringBuilder sb = new StringBuilder(); 192 for (byte v : bytes) { 193 int d = (v >> 4) & 0xf; 194 sb.append((char) (d >= 10 ? ('a' + d - 10) : ('0' + d))); 195 d = v & 0xf; 196 sb.append((char) (d >= 10 ? ('a' + d - 10) : ('0' + d))); 197 } 198 199 return sb.toString(); 200 } 201 } 202