blob: 91e31ab1110cee1de1da0b31d4fe139fa97c3ecb [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.server.biometrics;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import android.content.ComponentName;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.SensorProperties;
import android.os.SystemProperties;
import android.os.ParcelFileDescriptor;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.server.wm.Condition;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.server.biometrics.nano.BiometricServiceStateProto;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
public class Utils {
private static final String TAG = "BiometricTestUtils";
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
/** adb command for dumping the biometric proto */
public static final String DUMPSYS_BIOMETRIC = "dumpsys biometric --proto";
/**
* Retrieves the current SensorStates.
*/
public interface SensorStatesSupplier {
SensorStates getSensorStates() throws Exception;
}
/**
* Waits for the service to become idle
* @throws Exception
*/
public static void waitForIdleService(@NonNull SensorStatesSupplier supplier) throws Exception {
for (int i = 0; i < 10; i++) {
if (!supplier.getSensorStates().areAllSensorsIdle()) {
Log.d(TAG, "Not idle yet..");
Thread.sleep(300);
} else {
return;
}
}
Log.d(TAG, "Timed out waiting for idle");
}
/**
* Waits for the specified sensor to become non-idle
*/
public static void waitForBusySensor(int sensorId, @NonNull SensorStatesSupplier supplier)
throws Exception {
for (int i = 0; i < 10; i++) {
if (!supplier.getSensorStates().sensorStates.get(sensorId).isBusy()) {
Log.d(TAG, "Not busy yet..");
Thread.sleep(300);
} else {
return;
}
}
Log.d(TAG, "Timed out waiting to become busy");
}
public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition) {
waitFor(message, condition, null /* onFailure */);
}
public static void waitFor(@NonNull String message, @NonNull BooleanSupplier condition,
@Nullable Consumer<Object> onFailure) {
Condition.waitFor(new Condition<>(message, condition)
.setRetryIntervalMs(500)
.setRetryLimit(20)
.setOnFailure(onFailure));
}
/**
* Retrieves the current states of all biometric sensor services (e.g. FingerprintService,
* FaceService, etc).
*
* Note that the states are retrieved from BiometricService, instead of individual services.
* This is because 1) BiometricService is the source of truth for all public API-facing things,
* and 2) This to include other information, such as UI states, etc as well.
*/
@NonNull
public static BiometricServiceState getBiometricServiceCurrentState() throws Exception {
final byte[] dump = Utils.executeShellCommand(DUMPSYS_BIOMETRIC);
final BiometricServiceStateProto proto = BiometricServiceStateProto.parseFrom(dump);
return BiometricServiceState.parseFrom(proto);
}
/**
* Runs a shell command, similar to running "adb shell ..." from the command line.
* @param cmd A command, without the preceding "adb shell" portion. For example,
* passing in "dumpsys fingerprint" would be the equivalent of running
* "adb shell dumpsys fingerprint" from the command line.
* @return The result of the command.
*/
public static byte[] executeShellCommand(String cmd) {
Log.d(TAG, "execute: " + cmd);
try {
ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation()
.executeShellCommand(cmd);
byte[] buf = new byte[512];
int bytesRead;
FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
while ((bytesRead = fis.read(buf)) != -1) {
stdout.write(buf, 0, bytesRead);
}
fis.close();
return stdout.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void forceStopActivity(ComponentName componentName) {
executeShellCommand("am force-stop " + componentName.getPackageName()
+ " " + componentName.getShortClassName().replaceAll("\\.", ""));
}
public static int numberOfSpecifiedOperations(@NonNull BiometricServiceState state,
int sensorId, int operation) {
int count = 0;
final List<Integer> recentOps = state.mSensorStates.sensorStates.get(sensorId)
.getSchedulerState().getRecentOperations();
for (Integer i : recentOps) {
if (i == operation) {
count++;
}
}
return count;
}
public static void createTimeBoundSecretKey_deprecated(String keyName, boolean useStrongBox)
throws Exception {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setIsStrongBoxBacked(useStrongBox)
.setUserAuthenticationValidityDurationSeconds(5 /* seconds */)
.build());
keyGenerator.generateKey();
}
static void createTimeBoundSecretKey(String keyName, int authTypes, boolean useStrongBox)
throws Exception {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator.init(new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setIsStrongBoxBacked(useStrongBox)
.setUserAuthenticationParameters(1 /* seconds */, authTypes)
.build());
keyGenerator.generateKey();
}
public static void generateBiometricBoundKey(String keyName, boolean useStrongBox)
throws Exception {
final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
keystore.load(null);
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.setIsStrongBoxBacked(useStrongBox)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG);
KeyGenerator keyGenerator = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER);
keyGenerator.init(builder.build());
// Generates and stores the key in Android KeyStore under the keystoreAlias (keyName)
// specified in the builder.
keyGenerator.generateKey();
}
public static BiometricPrompt.CryptoObject initializeCryptoObject(String keyName)
throws Exception {
final KeyStore keystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
keystore.load(null);
final SecretKey secretKey = (SecretKey) keystore.getKey(
keyName, null /* password */);
final Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
final BiometricPrompt.CryptoObject cryptoObject =
new BiometricPrompt.CryptoObject(cipher);
return cryptoObject;
}
public static boolean isPublicAuthenticatorConstant(int authenticator) {
switch (authenticator) {
case BiometricManager.Authenticators.BIOMETRIC_STRONG:
case BiometricManager.Authenticators.BIOMETRIC_WEAK:
case BiometricManager.Authenticators.DEVICE_CREDENTIAL:
return true;
default:
return false;
}
}
public static int testApiStrengthToAuthenticatorStrength(int testApiStrength) {
switch (testApiStrength) {
case SensorProperties.STRENGTH_STRONG:
return BiometricManager.Authenticators.BIOMETRIC_STRONG;
case SensorProperties.STRENGTH_WEAK:
return BiometricManager.Authenticators.BIOMETRIC_WEAK;
default:
throw new IllegalArgumentException("Unable to convert testApiStrength: "
+ testApiStrength);
}
}
public static boolean isFirstApiLevel29orGreater() {
int firstApiLevel = SystemProperties.getInt("ro.product.first_api_level", 0);
if (firstApiLevel >= 29) {
return true;
}
return false;
}
}