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