blob: d5d219994177db541809480a3773037b8be25a1d [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 com.android.cts.verifier.biometrics;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
import android.hardware.biometrics.BiometricPrompt.AuthenticationResult;
import android.hardware.biometrics.BiometricPrompt.CryptoObject;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.provider.Settings;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import com.android.cts.verifier.R;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
/**
* On devices without a strong biometric, ensure that the
* {@link BiometricManager#canAuthenticate(int)} returns
* {@link BiometricManager#BIOMETRIC_ERROR_NO_HARDWARE}
*
* Ensure that this result is consistent with the configuration in core/res/res/values/config.xml
*
* Ensure that invoking {@link Settings.ACTION_BIOMETRIC_ENROLL} with its corresponding
* {@link Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED} enrolls a
* {@link BiometricManager.Authenticators.BIOMETRIC_STRONG} authenticator. This can be done by
* authenticating a {@link BiometricPrompt.CryptoObject}.
*
* Ensure that authentication with a strong biometric unlocks the appropriate keys.
*
* Ensure that the BiometricPrompt UI displays all fields in the public API surface.
*/
public class BiometricStrongTests extends AbstractBaseTest {
private static final String TAG = "BiometricStrongTests";
private static final String KEY_NAME_STRONGBOX = "key_using_strongbox";
private static final String KEY_NAME_NO_STRONGBOX = "key_without_strongbox";
private static final byte[] PAYLOAD = new byte[] {1, 2, 3, 4, 5, 6};
// TODO: Build these lists in a smarter way. For now, when adding a test to this list, please
// double check the logic in isOnPauseAllowed()
private boolean mHasStrongBox;
private Button mCheckAndEnrollButton;
private Button mAuthenticateWithoutStrongBoxButton;
private Button mAuthenticateWithStrongBoxButton;
private Button mKeyInvalidatedButton;
private boolean mAuthenticateWithoutStrongBoxPassed;
private boolean mAuthenticateWithStrongBoxPassed;
private boolean mKeyInvalidatedStrongboxPassed;
private boolean mKeyInvalidatedNoStrongboxPassed;
@Override
protected String getTag() {
return TAG;
}
@Override
protected void onBiometricEnrollFinished() {
final int biometricStatus =
mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG);
if (biometricStatus == BiometricManager.BIOMETRIC_SUCCESS) {
showToastAndLog("Successfully enrolled, please continue the test");
mCheckAndEnrollButton.setEnabled(false);
mAuthenticateWithoutStrongBoxButton.setEnabled(true);
mAuthenticateWithStrongBoxButton.setEnabled(true);
} else {
showToastAndLog("Unexpected result after enrollment: " + biometricStatus);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.biometric_test_strong_tests);
setPassFailButtonClickListeners();
getPassButton().setEnabled(false);
mCheckAndEnrollButton = findViewById(R.id.check_and_enroll_button);
mAuthenticateWithoutStrongBoxButton = findViewById(R.id.authenticate_no_strongbox_button);
mAuthenticateWithStrongBoxButton = findViewById(R.id.authenticate_strongbox_button);
mKeyInvalidatedButton = findViewById(R.id.authenticate_key_invalidated_button);
mHasStrongBox = getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
if (!mHasStrongBox) {
Log.d(TAG, "Device does not support StrongBox");
mAuthenticateWithStrongBoxButton.setVisibility(View.GONE);
mAuthenticateWithStrongBoxPassed = true;
mKeyInvalidatedStrongboxPassed = true;
}
mCheckAndEnrollButton.setOnClickListener((view) -> {
checkAndEnroll(mCheckAndEnrollButton, Authenticators.BIOMETRIC_STRONG);
});
mAuthenticateWithoutStrongBoxButton.setOnClickListener((view) -> {
testBiometricBoundEncryption(KEY_NAME_NO_STRONGBOX, PAYLOAD,
false /* useStrongBox */);
});
mAuthenticateWithStrongBoxButton.setOnClickListener((view) -> {
testBiometricBoundEncryption(KEY_NAME_STRONGBOX, PAYLOAD,
true /* useStrongBox */);
});
mKeyInvalidatedButton.setOnClickListener((view) -> {
Utils.showInstructionDialog(this,
R.string.biometric_test_strong_authenticate_invalidated_instruction_title,
R.string.biometric_test_strong_authenticate_invalidated_instruction_contents,
R.string.biometric_test_strong_authenticate_invalidated_instruction_continue,
(dialog, which) -> {
if (which == DialogInterface.BUTTON_POSITIVE) {
// If the device supports StrongBox, check that this key is invalidated.
if (mHasStrongBox)
if (isKeyInvalidated(KEY_NAME_STRONGBOX)) {
mKeyInvalidatedStrongboxPassed = true;
} else {
showToastAndLog("StrongBox key not invalidated");
return;
}
}
// Always check that non-StrongBox keys are invalidated.
if (isKeyInvalidated(KEY_NAME_NO_STRONGBOX)) {
mKeyInvalidatedNoStrongboxPassed = true;
} else {
showToastAndLog("Key not invalidated");
return;
}
mKeyInvalidatedButton.setEnabled(false);
updatePassButton();
});
});
}
@Override
protected boolean isOnPauseAllowed() {
// Test hasn't started yet, user may need to go to Settings to remove enrollments
if (mCheckAndEnrollButton.isEnabled()) {
return true;
}
// Key invalidation test is currently the last test. Thus, if every other test is currently
// completed, let's allow onPause (allow tester to go into settings multiple times if
// needed).
if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed) {
return true;
}
if (mCurrentlyEnrolling) {
return true;
}
return false;
}
private boolean isKeyInvalidated(String keyName) {
try {
Utils.initCipher(keyName);
} catch (KeyPermanentlyInvalidatedException e) {
return true;
} catch (Exception e) {
showToastAndLog("Unexpected exception: " + e);
}
return false;
}
private void testBiometricBoundEncryption(String keyName, byte[] secret, boolean useStrongBox) {
try {
// Create the biometric-bound key
Utils.createBiometricBoundKey(keyName, useStrongBox);
// Initialize a cipher and try to use it before a biometric has been authenticated
Cipher tryUseBeforeAuthCipher = Utils.initCipher(keyName);
try {
byte[] encrypted = Utils.doEncrypt(tryUseBeforeAuthCipher, secret);
showToastAndLog("Should not be able to encrypt prior to authenticating: "
+ Arrays.toString(encrypted));
return;
} catch (IllegalBlockSizeException e) {
// Normal, user has not authenticated yet
Log.d(TAG, "Exception before authentication has occurred: " + e);
}
// Initialize a cipher and try to use it after a biometric has been authenticated
final Cipher tryUseAfterAuthCipher = Utils.initCipher(keyName);
CryptoObject crypto = new CryptoObject(tryUseAfterAuthCipher);
final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
builder.setTitle("Please authenticate");
builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG);
builder.setNegativeButton("Cancel", mExecutor, (dialog, which) -> {
// Do nothing
});
final BiometricPrompt prompt = builder.build();
prompt.authenticate(crypto, new CancellationSignal(), mExecutor,
new AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(AuthenticationResult result) {
try {
final int authenticationType = result.getAuthenticationType();
if (authenticationType
!= BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC) {
showToastAndLog("Unexpected authenticationType: "
+ authenticationType);
return;
}
byte[] encrypted = Utils.doEncrypt(tryUseAfterAuthCipher,
secret);
showToastAndLog("Encrypted payload: " + Arrays.toString(encrypted)
+ ", please run the next test");
if (useStrongBox) {
mAuthenticateWithStrongBoxPassed = true;
mAuthenticateWithStrongBoxButton.setEnabled(false);
} else {
mAuthenticateWithoutStrongBoxPassed = true;
mAuthenticateWithoutStrongBoxButton.setEnabled(false);
}
updatePassButton();
} catch (Exception e) {
showToastAndLog("Failed to encrypt after biometric was"
+ "authenticated: " + e, e);
}
}
});
} catch (Exception e) {
showToastAndLog("Failed during Crypto test: " + e);
}
}
private void updatePassButton() {
if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed) {
if (!mKeyInvalidatedStrongboxPassed || !mKeyInvalidatedNoStrongboxPassed) {
mKeyInvalidatedButton.setEnabled(true);
}
if (mKeyInvalidatedStrongboxPassed && mKeyInvalidatedNoStrongboxPassed) {
showToastAndLog("All tests passed");
getPassButton().setEnabled(true);
}
}
}
}