1 /* 2 * Copyright (C) 2020 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 android.server.biometrics.util; 18 19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 20 21 import android.content.ComponentName; 22 import android.hardware.biometrics.BiometricManager; 23 import android.hardware.biometrics.BiometricPrompt; 24 import android.hardware.biometrics.Flags; 25 import android.hardware.biometrics.SensorProperties; 26 import android.os.ParcelFileDescriptor; 27 import android.os.Process; 28 import android.os.SystemProperties; 29 import android.security.keystore.KeyGenParameterSpec; 30 import android.security.keystore.KeyProperties; 31 import android.server.wm.Condition; 32 import android.util.Log; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.server.biometrics.nano.BiometricServiceStateProto; 38 39 import java.io.ByteArrayOutputStream; 40 import java.io.FileInputStream; 41 import java.io.IOException; 42 import java.nio.charset.StandardCharsets; 43 import java.security.KeyStore; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.function.BooleanSupplier; 47 import java.util.function.Consumer; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 51 import javax.crypto.Cipher; 52 import javax.crypto.KeyGenerator; 53 import javax.crypto.SecretKey; 54 55 public class Utils { 56 57 private static final String TAG = "BiometricTestUtils"; 58 private static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; 59 60 /** adb command for dumping the biometric proto */ 61 public static final String DUMPSYS_BIOMETRIC = "dumpsys biometric --proto"; 62 63 /** 64 * Retrieves the current SensorStates. 65 */ 66 public interface SensorStatesSupplier { getSensorStates()67 SensorStates getSensorStates() throws Exception; 68 } 69 70 /** 71 * The Biometric {@link com.android.server.biometrics.SensorConfig}. 72 * Parsed sensor config. See core/res/res/values/config.xml config_biometric_sensors 73 */ 74 public static class SensorConfig { 75 public final int id; 76 public final int modality; 77 public final int strength; 78 SensorConfig(String config)79 public SensorConfig(String config) { 80 String[] elems = config.split(":"); 81 id = Integer.parseInt(elems[0]); 82 modality = Integer.parseInt(elems[1]); 83 strength = Integer.parseInt(elems[2]); 84 } 85 } 86 87 /** Waits for the service to become idle. */ waitForIdleService()88 public static void waitForIdleService() throws Exception { 89 waitForIdleService(() -> getBiometricServiceCurrentState().mSensorStates); 90 } 91 92 /** 93 * Waits for the service to become idle 94 * @throws Exception 95 */ waitForIdleService(@onNull SensorStatesSupplier supplier)96 public static void waitForIdleService(@NonNull SensorStatesSupplier supplier) throws Exception { 97 for (int i = 0; i < 20; i++) { 98 if (!supplier.getSensorStates().areAllSensorsIdle()) { 99 Log.d(TAG, "Not idle yet.."); 100 Thread.sleep(300); 101 } else { 102 return; 103 } 104 } 105 Log.d(TAG, "Timed out waiting for idle"); 106 } 107 108 109 /** Waits for the specified sensor to become non-idle. */ waitForBusySensor(int sensorId)110 public static void waitForBusySensor(int sensorId) throws Exception { 111 waitForBusySensor(sensorId, () -> getBiometricServiceCurrentState().mSensorStates); 112 } 113 114 /** 115 * Waits for the specified sensor to become non-idle 116 */ waitForBusySensor(int sensorId, @NonNull SensorStatesSupplier supplier)117 public static void waitForBusySensor(int sensorId, @NonNull SensorStatesSupplier supplier) 118 throws Exception { 119 for (int i = 0; i < 10; i++) { 120 if (!supplier.getSensorStates().sensorStates.get(sensorId).isBusy()) { 121 Log.d(TAG, "Not busy yet.."); 122 Thread.sleep(300); 123 } else { 124 return; 125 } 126 } 127 Log.d(TAG, "Timed out waiting to become busy"); 128 } 129 waitFor(@onNull String message, @NonNull BooleanSupplier condition)130 public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition) { 131 waitFor(message, condition, null /* onFailure */); 132 } 133 waitFor(@onNull String message, @NonNull BooleanSupplier condition, @Nullable Consumer<Object> onFailure)134 public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition, 135 @Nullable Consumer<Object> onFailure) { 136 Condition.waitFor(new Condition<>(message, condition) 137 .setRetryIntervalMs(500) 138 .setRetryLimit(20) 139 .setOnFailure(onFailure)); 140 } 141 142 /** 143 * Retrieves the current states of all biometric sensor services (e.g. FingerprintService, 144 * FaceService, etc). 145 * 146 * Note that the states are retrieved from BiometricService, instead of individual services. 147 * This is because 1) BiometricService is the source of truth for all public API-facing things, 148 * and 2) This to include other information, such as UI states, etc as well. 149 */ 150 @NonNull getBiometricServiceCurrentState()151 public static BiometricServiceState getBiometricServiceCurrentState() throws Exception { 152 final byte[] dump = Utils.executeShellCommand(DUMPSYS_BIOMETRIC); 153 final BiometricServiceStateProto proto = BiometricServiceStateProto.parseFrom(dump); 154 return BiometricServiceState.parseFrom(proto); 155 } 156 157 /** 158 * Runs a shell command, similar to running "adb shell ..." from the command line. 159 * @param cmd A command, without the preceding "adb shell" portion. For example, 160 * passing in "dumpsys fingerprint" would be the equivalent of running 161 * "adb shell dumpsys fingerprint" from the command line. 162 * @return The result of the command. 163 */ executeShellCommand(String cmd)164 public static byte[] executeShellCommand(String cmd) { 165 Log.d(TAG, "execute: " + cmd); 166 try { 167 ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation() 168 .executeShellCommand(cmd); 169 byte[] buf = new byte[512]; 170 int bytesRead; 171 FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd); 172 ByteArrayOutputStream stdout = new ByteArrayOutputStream(); 173 while ((bytesRead = fis.read(buf)) != -1) { 174 stdout.write(buf, 0, bytesRead); 175 } 176 fis.close(); 177 return stdout.toByteArray(); 178 } catch (IOException e) { 179 throw new RuntimeException(e); 180 } 181 } 182 forceStopActivity(ComponentName componentName)183 public static void forceStopActivity(ComponentName componentName) { 184 executeShellCommand("am force-stop " + componentName.getPackageName() 185 + " " + componentName.getShortClassName().replaceAll("\\.", "")); 186 } 187 numberOfSpecifiedOperations(@onNull BiometricServiceState state, int sensorId, int operation)188 public static int numberOfSpecifiedOperations(@NonNull BiometricServiceState state, 189 int sensorId, int operation) { 190 int count = 0; 191 final List<Integer> recentOps = state.mSensorStates.sensorStates.get(sensorId) 192 .getSchedulerState().getRecentOperations(); 193 for (Integer i : recentOps) { 194 if (i == operation) { 195 count++; 196 } 197 } 198 return count; 199 } 200 createTimeBoundSecretKey_deprecated(String keyName, boolean useStrongBox)201 public static void createTimeBoundSecretKey_deprecated(String keyName, boolean useStrongBox) 202 throws Exception { 203 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 204 keyStore.load(null); 205 KeyGenerator keyGenerator = KeyGenerator.getInstance( 206 KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); 207 208 // Set the alias of the entry in Android KeyStore where the key will appear 209 // and the constrains (purposes) in the constructor of the Builder 210 keyGenerator.init(new KeyGenParameterSpec.Builder(keyName, 211 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 212 .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 213 .setUserAuthenticationRequired(true) 214 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 215 .setIsStrongBoxBacked(useStrongBox) 216 .setUserAuthenticationValidityDurationSeconds(5 /* seconds */) 217 .build()); 218 keyGenerator.generateKey(); 219 } 220 createTimeBoundSecretKey(String keyName, int authTypes, boolean useStrongBox)221 public static void createTimeBoundSecretKey(String keyName, int authTypes, boolean useStrongBox) 222 throws Exception { 223 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 224 keyStore.load(null); 225 KeyGenerator keyGenerator = KeyGenerator.getInstance( 226 KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); 227 228 // Set the alias of the entry in Android KeyStore where the key will appear 229 // and the constrains (purposes) in the constructor of the Builder 230 keyGenerator.init(new KeyGenParameterSpec.Builder(keyName, 231 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 232 .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 233 .setUserAuthenticationRequired(true) 234 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 235 .setIsStrongBoxBacked(useStrongBox) 236 .setUserAuthenticationParameters(1 /* seconds */, authTypes) 237 .build()); 238 keyGenerator.generateKey(); 239 } 240 generateBiometricBoundKey(String keyName, boolean useStrongBox)241 public static void generateBiometricBoundKey(String keyName, boolean useStrongBox) 242 throws Exception { 243 final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER); 244 keystore.load(null); 245 KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( 246 keyName, 247 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 248 .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 249 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 250 .setUserAuthenticationRequired(true) 251 .setInvalidatedByBiometricEnrollment(true) 252 .setIsStrongBoxBacked(useStrongBox) 253 .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG); 254 255 KeyGenerator keyGenerator = KeyGenerator 256 .getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER); 257 keyGenerator.init(builder.build()); 258 259 // Generates and stores the key in Android KeyStore under the keystoreAlias (keyName) 260 // specified in the builder. 261 keyGenerator.generateKey(); 262 } 263 initializeCryptoObject(String keyName)264 public static BiometricPrompt.CryptoObject initializeCryptoObject(String keyName) 265 throws Exception { 266 final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER); 267 keystore.load(null); 268 final SecretKey secretKey = (SecretKey) keystore.getKey( 269 keyName, null /* password */); 270 final Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" 271 + KeyProperties.BLOCK_MODE_CBC + "/" 272 + KeyProperties.ENCRYPTION_PADDING_PKCS7); 273 cipher.init(Cipher.ENCRYPT_MODE, secretKey); 274 275 final BiometricPrompt.CryptoObject cryptoObject = 276 new BiometricPrompt.CryptoObject(cipher); 277 return cryptoObject; 278 } 279 isPublicAuthenticatorConstant(int authenticator)280 public static boolean isPublicAuthenticatorConstant(int authenticator) { 281 switch (authenticator) { 282 case BiometricManager.Authenticators.BIOMETRIC_STRONG: 283 case BiometricManager.Authenticators.BIOMETRIC_WEAK: 284 case BiometricManager.Authenticators.DEVICE_CREDENTIAL: 285 return true; 286 default: 287 return false; 288 } 289 } 290 testApiStrengthToAuthenticatorStrength(int testApiStrength)291 public static int testApiStrengthToAuthenticatorStrength(int testApiStrength) { 292 switch (testApiStrength) { 293 case SensorProperties.STRENGTH_STRONG: 294 return BiometricManager.Authenticators.BIOMETRIC_STRONG; 295 case SensorProperties.STRENGTH_WEAK: 296 return BiometricManager.Authenticators.BIOMETRIC_WEAK; 297 default: 298 throw new IllegalArgumentException("Unable to convert testApiStrength: " 299 + testApiStrength); 300 } 301 } 302 isFirstApiLevel29orGreater()303 public static boolean isFirstApiLevel29orGreater() { 304 int firstApiLevel = SystemProperties.getInt("ro.product.first_api_level", 0); 305 if (firstApiLevel >= 29) { 306 return true; 307 } 308 return false; 309 } 310 311 /** Find the sensor id of the AIDL fingerprint HAL, or -1 if not present. */ getAidlFingerprintSensorId()312 public static int getAidlFingerprintSensorId() { 313 return getAidlSensorId("dumpsys fingerprint", ", provider: FingerprintProvider"); 314 } 315 316 /** Find all of the sensor ids of the AIDL fingerprint HALs */ getAidlFingerprintSensorIds()317 public static List<Integer> getAidlFingerprintSensorIds() { 318 return getAidlSensorIds("dumpsys fingerprint", ", provider: FingerprintProvider"); 319 } 320 321 /** Find the sensor id of the AIDL face HAL, or -1 if not present. */ getAidlFaceSensorId()322 public static int getAidlFaceSensorId() { 323 return getAidlSensorId("dumpsys face", ", provider: FaceProvider"); 324 } 325 326 /** Find current user id. */ getUserId()327 public static int getUserId() { 328 return Process.myUserHandle().getIdentifier(); 329 } 330 getAidlSensorId(String adbCommand, String providerRegex)331 private static int getAidlSensorId(String adbCommand, String providerRegex) { 332 List<Integer> ids = getAidlSensorIds(adbCommand, providerRegex); 333 334 if (ids.isEmpty()) { 335 return -1; 336 } 337 338 return ids.get(0); 339 } 340 getAidlSensorIds(String adbCommand, String providerRegex)341 private static List<Integer> getAidlSensorIds(String adbCommand, String providerRegex) { 342 final byte[] dump = executeShellCommand(adbCommand); 343 final String fpsDumpSys = new String(dump, StandardCharsets.UTF_8); 344 final Matcher matcher = 345 Pattern.compile("sensorId: (\\d+)" + providerRegex).matcher(fpsDumpSys); 346 347 final List<Integer> ids = new ArrayList<>(); 348 while (matcher.find()) { 349 ids.add(Integer.parseInt(matcher.group(1))); 350 } 351 352 return ids; 353 } 354 355 /** 356 * @param isVerticalContentView Whether the content view is 357 * {@link 358 * android.hardware.biometrics.PromptVerticalListContentView}. 359 * @param isBiometricAllowed Whether there is any biometric authenticator 360 * allowed. 361 * @return Whether biometric prompt without icon should show prior to credential view. 362 */ shouldShowBpWithoutIconForCredential( boolean isVerticalContentView, boolean isBiometricAllowed)363 public static boolean shouldShowBpWithoutIconForCredential( 364 boolean isVerticalContentView, boolean isBiometricAllowed) { 365 return Flags.customBiometricPrompt() && !isBiometricAllowed && isVerticalContentView; 366 } 367 } 368