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 // TODO(b/311183933): Remove passed in Context from static method. 41 @SuppressWarnings("AvoidStaticContext") 42 public class AllowLists { 43 @VisibleForTesting public static final String ALLOW_ALL = "*"; 44 public static final String ALLOW_NONE = ""; 45 46 private static final String SPLITTER = ","; 47 private static final String HASH_ALGORITHM = "SHA-256"; 48 49 /** Returns whether all entities are allowlisted or not based on the given {@code allowList}. */ doesAllowListAllowAll(@onNull String allowList)50 public static boolean doesAllowListAllowAll(@NonNull String allowList) { 51 Objects.requireNonNull(allowList); 52 return ALLOW_ALL.equals(allowList); 53 } 54 55 /** Splits the given {@code allowList} into the list of entities allowed. */ splitAllowList(@onNull String allowList)56 public static List<String> splitAllowList(@NonNull String allowList) { 57 Objects.requireNonNull(allowList); 58 59 if (allowList.trim().isEmpty()) { 60 return Collections.emptyList(); 61 } 62 63 return Arrays.stream(allowList.split(SPLITTER)) 64 .map(String::trim) 65 .collect(Collectors.toList()); 66 } 67 68 /** 69 * A utility to check if an app package exists in the provided allow-list. The allow-list to 70 * search is split by {@link #SPLITTER} without any white spaces. E.g. of a valid allow list - 71 * "abc.package1.app,com.package2.app,com.package3.xyz" If the provided parameter {@code 72 * appPackageName} exists in the allow-list (e.g. com.package2.app), then the method returns 73 * true, false otherwise. 74 */ isPackageAllowListed( @onNull String allowList, @NonNull String appPackageName)75 public static boolean isPackageAllowListed( 76 @NonNull String allowList, @NonNull String appPackageName) { 77 if (ALLOW_ALL.equals(allowList)) { 78 return true; 79 } 80 81 // TODO(b/237686242): Cache the AllowList so that we don't need to read from Flags and split 82 // on every API call. 83 return Arrays.stream(allowList.split(SPLITTER)) 84 .map(String::trim) 85 .anyMatch(packageName -> packageName.equals(appPackageName)); 86 } 87 88 /** 89 * A utility to check if all app signatures exist in the provided allow-list. The allow-list to 90 * search is split by {@link #SPLITTER} without any white spaces. 91 * 92 * @param context the Context 93 * @param signatureAllowList the list of signatures that is allowed. 94 * @param appPackageName the package name of an app 95 * @return true if this app is allowed. Otherwise, it returns false. 96 */ 97 isSignatureAllowListed( @onNull Context context, @NonNull String signatureAllowList, @NonNull String appPackageName)98 public static boolean isSignatureAllowListed( 99 @NonNull Context context, 100 @NonNull String signatureAllowList, 101 @NonNull String appPackageName) { 102 if (ALLOW_ALL.equals(signatureAllowList)) { 103 return true; 104 } 105 106 byte[] appSignatureHash = getAppSignatureHash(context, appPackageName); 107 108 // App must have signatures queried from Package Manager in order to use PPAPI. Otherwise, 109 // it is not allowed. 110 if (appSignatureHash == null) { 111 return false; 112 } 113 114 String hexSignature = toHexString(appSignatureHash); 115 116 LogUtil.v("App %s has signature(s) as %s", appPackageName, hexSignature); 117 118 // TODO(b/237686242): Cache the AllowList so that we don't need to read from Flags and split 119 // on every API call. 120 return Arrays.stream(signatureAllowList.split(SPLITTER)) 121 .map(String::trim) 122 .anyMatch(signature -> signature.equals(hexSignature)); 123 } 124 125 /** 126 * Get Hash for the signature of an app. Most of the methods invoked are from {@link 127 * PackageManager}. 128 * 129 * @param context the Context 130 * @param packageName package name of the app 131 * @return the hash in byte array format to represent the signature of an app. Returns {@code 132 * null} if app cannot be fetched from package manager or there is no {@link 133 * PackageInfo}/{@link SigningInfo} associated with this app. 134 */ 135 @VisibleForTesting 136 @Nullable getAppSignatureHash( @onNull Context context, @NonNull String packageName)137 public static byte[] getAppSignatureHash( 138 @NonNull Context context, @NonNull String packageName) { 139 PackageInfo packageInfo; 140 try { 141 packageInfo = 142 context.getPackageManager() 143 .getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES); 144 } catch (PackageManager.NameNotFoundException e) { 145 LogUtil.e("Package %s is not found in Package Manager!", packageName); 146 return null; 147 } 148 if (packageInfo == null) { 149 LogUtil.w("There is not package info for package %s", packageName); 150 return null; 151 } 152 153 SigningInfo signingInfo = packageInfo.signingInfo; 154 if (signingInfo == null) { 155 LogUtil.w( 156 "There is no signing info for package %s. Please check the signature!", 157 packageName); 158 return null; 159 } 160 161 Signature[] signatures = signingInfo.getSigningCertificateHistory(); 162 if (signatures == null || signatures.length == 0) { 163 LogUtil.w( 164 "There is no signature fetched from signing info for package %s.", packageName); 165 return null; 166 } 167 168 byte[] signatureHash = null; 169 // Current signature is at the last index of the history. 170 // This AllowList mechanism is actually not design for authentication. We only use it for 171 // system health and decide who can participate in our beta release. In this case, it's fine 172 // to just use whatever the current signing key is for all the apps that can participate 173 // in the beta 1 without needing to worry about subsequent rotations. 174 Signature currentSignature = signatures[signatures.length - 1]; 175 try { 176 MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM); 177 signatureHash = digest.digest(currentSignature.toByteArray()); 178 } catch (NoSuchAlgorithmException e) { 179 LogUtil.e("SHA not available: " + e.getMessage()); 180 } 181 182 return signatureHash; 183 } 184 185 /** 186 * Convert byte array to a hex string. 187 * 188 * <p>Mostly copied from frameworks/base/core/java/android/content/pm/Signature.java 189 * 190 * @param bytes the byte array 191 * @return the hex string format of the byte array 192 */ 193 @VisibleForTesting toHexString(byte[] bytes)194 protected static String toHexString(byte[] bytes) { 195 StringBuilder sb = new StringBuilder(); 196 for (byte v : bytes) { 197 int d = (v >> 4) & 0xf; 198 sb.append((char) (d >= 10 ? ('a' + d - 10) : ('0' + d))); 199 d = v & 0xf; 200 sb.append((char) (d >= 10 ? ('a' + d - 10) : ('0' + d))); 201 } 202 203 return sb.toString(); 204 } 205 } 206