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