blob: 316e15ad36d86639cf7c2f9db680b84227ad32a2 [file] [log] [blame]
/*
* Copyright (C) 2019 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.security;
import android.Manifest;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricManager.Authenticators;
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.security.identity.AccessControlProfile;
import android.security.identity.AccessControlProfileId;
import android.security.identity.CredentialDataRequest;
import android.security.identity.CredentialDataResult;
import android.security.identity.IdentityCredential;
import android.security.identity.IdentityCredentialStore;
import android.security.identity.PersonalizationData;
import android.security.identity.PresentationSession;
import android.security.identity.WritableIdentityCredential;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
/**
* @hide
*/
public class IdentityCredentialAuthenticationMultiDocument extends PassFailButtons.Activity {
private static final boolean DEBUG = false;
private static final String TAG = "IdentityCredentialAuthenticationMultiDocument";
private static final int BIOMETRIC_REQUEST_PERMISSION_CODE = 0;
private static final int PRESENTATION_SESSION_FEATURE_VERSION_NEEDED = 202201;
private BiometricManager mBiometricManager;
private KeyguardManager mKeyguardManager;
protected int getTitleRes() {
return R.string.sec_identity_credential_authentication_multi_document_test;
}
private int getDescriptionRes() {
return R.string.sec_identity_credential_authentication_multi_document_test_info;
}
// Returns 0 if Identity Credential is not implemented. Otherwise returns the feature version.
//
private static int getFeatureVersion(Context appContext) {
PackageManager pm = appContext.getPackageManager();
if (pm.hasSystemFeature(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) {
FeatureInfo[] infos = pm.getSystemAvailableFeatures();
for (int n = 0; n < infos.length; n++) {
FeatureInfo info = infos[n];
if (info.name.equals(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) {
return info.version;
}
}
}
// Use of the system feature is not required since Android 12. So for Android 11
// return 202009 which is the feature version shipped with Android 11.
IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
if (store != null) {
return 202009;
}
return 0;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sec_screen_lock_keys_main);
setPassFailButtonClickListeners();
setInfoResources(getTitleRes(), getDescriptionRes(), -1);
getPassButton().setEnabled(false);
requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC},
BIOMETRIC_REQUEST_PERMISSION_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
if (requestCode == BIOMETRIC_REQUEST_PERMISSION_CODE
&& state[0] == PackageManager.PERMISSION_GRANTED) {
mBiometricManager = getSystemService(BiometricManager.class);
mKeyguardManager = getSystemService(KeyguardManager.class);
Button startTestButton = findViewById(R.id.sec_start_test_button);
if (!mKeyguardManager.isKeyguardSecure()) {
// Show a message that the user hasn't set up a lock screen.
showToast("Secure lock screen hasn't been set up.\n Go to "
+ "'Settings -> Security -> Screen lock' to set up a lock screen");
startTestButton.setEnabled(false);
return;
}
startTestButton.setOnClickListener(v -> startTest());
}
}
protected void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
Log.i(TAG, "Showing Toast: " + message);
}
private void provisionCredential(IdentityCredentialStore store,
String credentialName) throws Exception {
store.deleteCredentialByName(credentialName);
WritableIdentityCredential wc = store.createCredential(
credentialName, "org.iso.18013-5.2019.mdl");
// 'Bar' encoded as CBOR tstr
byte[] barCbor = {0x63, 0x42, 0x61, 0x72};
AccessControlProfile acp = new AccessControlProfile.Builder(new AccessControlProfileId(0))
.setUserAuthenticationRequired(true)
.setUserAuthenticationTimeout(0)
.build();
LinkedList<AccessControlProfileId> idsProfile0 = new LinkedList<AccessControlProfileId>();
idsProfile0.add(new AccessControlProfileId(0));
PersonalizationData pd = new PersonalizationData.Builder()
.addAccessControlProfile(acp)
.putEntry("org.iso.18013-5.2019", "Foo", idsProfile0, barCbor)
.build();
byte[] proofOfProvisioningSignature = wc.personalize(pd);
// Create authentication keys.
IdentityCredential credential = store.getCredentialByName(credentialName,
IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
credential.setAvailableAuthenticationKeys(1, 10);
Collection<X509Certificate> dynAuthKeyCerts = credential.getAuthKeysNeedingCertification();
credential.storeStaticAuthenticationData(dynAuthKeyCerts.iterator().next(), new byte[0]);
}
private int getFooStatus(PresentationSession session, String credentialName) throws Exception {
Map<String, Collection<String>> isEntriesToRequest = new LinkedHashMap<>();
isEntriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo"));
CredentialDataResult rd = session.getCredentialData(
credentialName,
new CredentialDataRequest.Builder()
.setIssuerSignedEntriesToRequest(isEntriesToRequest)
.build());
return rd.getIssuerSignedEntries().getStatus("org.iso.18013-5.2019", "Foo");
}
protected void startTest() {
IdentityCredentialStore store = IdentityCredentialStore.getInstance(this);
if (store == null) {
showToast("No Identity Credential support, test passed.");
getPassButton().setEnabled(true);
return;
}
int featureVersion = getFeatureVersion(this);
Log.i(TAG, "Identity Credential featureVersion: " + featureVersion);
if (featureVersion < PRESENTATION_SESSION_FEATURE_VERSION_NEEDED) {
showToast(String.format(
Locale.US,
"Identity Credential version %d or later is required but "
+ "version %d was found. Test passed.",
PRESENTATION_SESSION_FEATURE_VERSION_NEEDED,
featureVersion));
getPassButton().setEnabled(true);
return;
}
final int result = mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG);
switch (result) {
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
showToast("No biometrics enrolled.\n"
+ "Go to 'Settings -> Security' to enroll");
Button startTestButton = findViewById(R.id.sec_start_test_button);
startTestButton.setEnabled(false);
return;
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
showToast("No strong biometrics, test passed.");
showToast("No Identity Credential support, test passed.");
getPassButton().setEnabled(true);
return;
}
try {
provisionCredential(store, "credential1");
provisionCredential(store, "credential2");
PresentationSession session = store.createPresentationSession(
IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
// First, check that Foo cannot be retrieved without authentication.
//
int status = getFooStatus(session, "credential1");
if (status != CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
showToast("Unexpected credential1 pre-auth status "
+ status + " expected STATUS_USER_AUTHENTICATION_FAILED");
return;
}
status = getFooStatus(session, "credential2");
if (status != CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
showToast("Unexpected credential2 pre-auth status "
+ status + " expected STATUS_USER_AUTHENTICATION_FAILED");
return;
}
// Try one more time, this time with a CryptoObject that we'll use with
// BiometricPrompt. This should work.
//
CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(session);
BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG);
builder.setTitle("Identity Credential");
builder.setDescription("Authenticate to unlock multiple credentials.");
builder.setNegativeButton("Cancel",
getMainExecutor(),
(dialogInterface, i) -> showToast("Canceled biometric prompt."));
final BiometricPrompt prompt = builder.build();
final AuthenticationCallback callback = new AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(AuthenticationResult authResult) {
try {
// Check that Foo can be retrieved because we used
// the CryptoObject to auth with.
int status = getFooStatus(session, "credential1");
if (status != CredentialDataResult.Entries.STATUS_OK) {
showToast("Unexpected credential1 post-auth status "
+ status + " expected STATUS_OK");
return;
}
status = getFooStatus(session, "credential2");
if (status != CredentialDataResult.Entries.STATUS_OK) {
showToast("Unexpected credential2 post-auth status "
+ status + " expected STATUS_OK");
return;
}
// Finally, check that Foo cannot be retrieved again from another session
PresentationSession anotherSession = store.createPresentationSession(
IdentityCredentialStore
.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
status = getFooStatus(anotherSession, "credential1");
if (status
!= CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
showToast("Unexpected credential1 other session status "
+ status + " expected STATUS_USER_AUTHENTICATION_FAILED");
return;
}
status = getFooStatus(anotherSession, "credential2");
if (status
!= CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
showToast("Unexpected credential2 other session status "
+ status + " expected STATUS_USER_AUTHENTICATION_FAILED");
return;
}
showToast("Test passed.");
getPassButton().setEnabled(true);
} catch (Exception e) {
showToast("Unexpected exception " + e);
}
}
};
prompt.authenticate(cryptoObject, new CancellationSignal(), getMainExecutor(),
callback);
} catch (Exception e) {
showToast("Unexpection exception " + e);
}
}
}