SoftwareIdentityCredential.java

/*
 * Copyright 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 androidx.security.identity;

import android.annotation.SuppressLint;
import android.content.Context;
import android.security.keystore.KeyProperties;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.biometric.BiometricPrompt;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECPoint;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyAgreement;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.builder.MapBuilder;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.UnicodeString;

class SoftwareIdentityCredential extends IdentityCredential {

    private static final String TAG = "SWIdentityCredential";
    private String mCredentialName;

    private Context mContext;

    private CredentialData mData;

    private KeyPair mEphemeralKeyPair = null;

    private SecretKey mSecretKey = null;
    private SecretKey mReaderSecretKey = null;

    private int mEphemeralCounter;
    private int mReadersExpectedEphemeralCounter;
    private byte[] mAuthKeyAssociatedData = null;
    private PrivateKey mAuthKey = null;
    private BiometricPrompt.CryptoObject mCryptoObject = null;

    private PublicKey mReaderEphemeralPublicKey = null;
    private byte[] mSessionTranscript = null;

    SoftwareIdentityCredential(Context context, String credentialName,
            @IdentityCredentialStore.Ciphersuite int cipherSuite)
            throws CipherSuiteNotSupportedException {
        if (cipherSuite
                != IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256) {
            throw new CipherSuiteNotSupportedException("Unsupported Cipher Suite");
        }
        mContext = context;
        mCredentialName = credentialName;
    }

    boolean loadData() {
        mData = CredentialData.loadCredentialData(mContext, mCredentialName);
        if (mData == null) {
            return false;
        }
        return true;
    }

    static byte[] delete(Context context, String credentialName) {
        return CredentialData.delete(context, credentialName);
    }

    // This only extracts the requested namespaces, not DocType or RequestInfo. We
    // can do this later if it's needed.
    private static HashMap<String, Collection<String>> parseRequestMessage(
            @Nullable byte[] requestMessage) {
        HashMap<String, Collection<String>> result = new HashMap<>();

        if (requestMessage == null) {
            return result;
        }

        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(requestMessage);
            List<DataItem> dataItems = new CborDecoder(bais).decode();
            if (dataItems.size() != 1) {
                throw new RuntimeException("Expected 1 item, found " + dataItems.size());
            }
            if (!(dataItems.get(0) instanceof Map)) {
                throw new RuntimeException("Item is not a map");
            }
            Map map = (Map) dataItems.get(0);

            DataItem nameSpaces = map.get(new UnicodeString("nameSpaces"));
            if (!(nameSpaces instanceof Map)) {
                throw new RuntimeException(
                        "nameSpaces entry not found or not map");
            }

            for (DataItem keyItem : ((Map) nameSpaces).getKeys()) {
                if (!(keyItem instanceof UnicodeString)) {
                    throw new RuntimeException(
                            "Key item in NameSpaces map not UnicodeString");
                }
                String nameSpace = ((UnicodeString) keyItem).getString();
                ArrayList<String> names = new ArrayList<>();

                DataItem valueItem = ((Map) nameSpaces).get(keyItem);
                if (!(valueItem instanceof Map)) {
                    throw new RuntimeException(
                            "Value item in NameSpaces map not Map");
                }
                for (DataItem item : ((Map) valueItem).getKeys()) {
                    if (!(item instanceof UnicodeString)) {
                        throw new RuntimeException(
                                "Item in nameSpaces array not UnicodeString");
                    }
                    names.add(((UnicodeString) item).getString());
                    // TODO: check that value is a boolean
                }
                result.put(nameSpace, names);
            }

        } catch (CborException e) {
            throw new RuntimeException("Error decoding request message", e);
        }
        return result;
    }

    @SuppressLint("NewApi")
    @Override
    public @NonNull KeyPair createEphemeralKeyPair() {
        if (mEphemeralKeyPair == null) {
            try {
                KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC);
                ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime256v1");
                kpg.initialize(ecSpec);
                mEphemeralKeyPair = kpg.generateKeyPair();
            } catch (NoSuchAlgorithmException
                    | InvalidAlgorithmParameterException e) {
                throw new RuntimeException("Error generating ephemeral key", e);
            }
        }
        return mEphemeralKeyPair;
    }

    @Override
    public void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey) {
        mReaderEphemeralPublicKey = readerEphemeralPublicKey;
    }

    @Override
    public void setSessionTranscript(@NonNull byte[] sessionTranscript) {
        if (mSessionTranscript != null) {
            throw new RuntimeException("SessionTranscript already set");
        }
        mSessionTranscript = sessionTranscript.clone();
    }

    private void ensureSessionEncryptionKey() {
        if (mSecretKey != null) {
            return;
        }
        if (mReaderEphemeralPublicKey == null) {
            throw new RuntimeException("Reader ephemeral key not set");
        }
        if (mSessionTranscript == null) {
            throw new RuntimeException("Session transcript not set");
        }
        try {
            KeyAgreement ka = KeyAgreement.getInstance("ECDH");
            ka.init(mEphemeralKeyPair.getPrivate());
            ka.doPhase(mReaderEphemeralPublicKey, true);
            byte[] sharedSecret = ka.generateSecret();

            byte[] sessionTranscriptBytes =
                    Util.prependSemanticTagForEncodedCbor(mSessionTranscript);
            byte[] sharedSecretWithSessionTranscriptBytes =
                    Util.concatArrays(sharedSecret, sessionTranscriptBytes);

            byte[] salt = new byte[1];
            byte[] info = new byte[0];

            salt[0] = 0x01;
            byte[] derivedKey = Util.computeHkdf("HmacSha256",
                    sharedSecretWithSessionTranscriptBytes, salt, info, 32);
            mSecretKey = new SecretKeySpec(derivedKey, "AES");

            salt[0] = 0x00;
            derivedKey = Util.computeHkdf("HmacSha256", sharedSecretWithSessionTranscriptBytes,
                    salt, info, 32);
            mReaderSecretKey = new SecretKeySpec(derivedKey, "AES");

            mEphemeralCounter = 1;
            mReadersExpectedEphemeralCounter = 1;

        } catch (InvalidKeyException
                | NoSuchAlgorithmException e) {
            throw new RuntimeException("Error performing key agreement", e);
        }
    }

    @Override
    public @NonNull
    byte[] encryptMessageToReader(@NonNull byte[] messagePlaintext) {
        ensureSessionEncryptionKey();
        byte[] messageCiphertextAndAuthTag = null;
        try {
            ByteBuffer iv = ByteBuffer.allocate(12);
            iv.putInt(0, 0x00000000);
            iv.putInt(4, 0x00000001);
            iv.putInt(8, mEphemeralCounter);
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec encryptionParameterSpec = new GCMParameterSpec(128, iv.array());
            cipher.init(Cipher.ENCRYPT_MODE, mSecretKey, encryptionParameterSpec);
            messageCiphertextAndAuthTag = cipher.doFinal(messagePlaintext);
        } catch (BadPaddingException
                | IllegalBlockSizeException
                | NoSuchPaddingException
                | InvalidKeyException
                | NoSuchAlgorithmException
                | InvalidAlgorithmParameterException e) {
            throw new RuntimeException("Error encrypting message", e);
        }
        mEphemeralCounter += 1;
        return messageCiphertextAndAuthTag;
    }

    @Override
    public @NonNull
    byte[] decryptMessageFromReader(@NonNull byte[] messageCiphertext)
            throws MessageDecryptionException {
        ensureSessionEncryptionKey();
        ByteBuffer iv = ByteBuffer.allocate(12);
        iv.putInt(0, 0x00000000);
        iv.putInt(4, 0x00000000);
        iv.putInt(8, mReadersExpectedEphemeralCounter);
        byte[] plainText = null;
        try {
            final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, mReaderSecretKey, new GCMParameterSpec(128,
                    iv.array()));
            plainText = cipher.doFinal(messageCiphertext);
        } catch (BadPaddingException
                | IllegalBlockSizeException
                | InvalidAlgorithmParameterException
                | InvalidKeyException
                | NoSuchAlgorithmException
                | NoSuchPaddingException e) {
            throw new MessageDecryptionException("Error decrypting message", e);
        }
        mReadersExpectedEphemeralCounter += 1;
        return plainText;
    }

    @Override
    public @NonNull
    Collection<X509Certificate> getCredentialKeyCertificateChain() {
        return mData.getCredentialKeyCertificateChain();
    }

    private void ensureCryptoObject() {
        String aliasForCryptoObject = mData.getPerReaderSessionKeyAlias();
        if (aliasForCryptoObject.isEmpty()) {
            // This happens if there are no ACPs with user-auth.
            return;
        }
        try {
            KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
            ks.load(null);
            KeyStore.Entry entry = ks.getEntry(aliasForCryptoObject, null);
            SecretKey perReaderSessionKey = ((KeyStore.SecretKeyEntry) entry).getSecretKey();
            Cipher perReaderSessionCipher = Cipher.getInstance("AES/GCM/NoPadding");
            perReaderSessionCipher.init(Cipher.ENCRYPT_MODE, perReaderSessionKey);
            mCryptoObject = new BiometricPrompt.CryptoObject(perReaderSessionCipher);
        } catch (CertificateException
                | NoSuchPaddingException
                | InvalidKeyException
                | IOException
                | NoSuchAlgorithmException
                | KeyStoreException
                | UnrecoverableEntryException e) {
            throw new RuntimeException("Error creating Cipher for perReaderSessionKey", e);
        }
    }

    private void ensureAuthKey() throws NoAuthenticationKeyAvailableException {
        if (mAuthKey != null) {
            return;
        }

        Pair<PrivateKey, byte[]> keyAndStaticData = mData.selectAuthenticationKey(
                mAllowUsingExhaustedKeys);
        if (keyAndStaticData == null) {
            throw new NoAuthenticationKeyAvailableException(
                    "No authentication key available for signing");
        }
        mAuthKey = keyAndStaticData.first;
        mAuthKeyAssociatedData = keyAndStaticData.second;
    }

    boolean mAllowUsingExhaustedKeys = true;

    @Override
    public void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) {
        mAllowUsingExhaustedKeys = allowUsingExhaustedKeys;
    }

    @Override
    @Nullable
    public BiometricPrompt.CryptoObject getCryptoObject() {
        ensureCryptoObject();
        return mCryptoObject;
    }

    private boolean hasEphemeralKeyInDeviceEngagement(@NonNull byte[] sessionTranscript) {
        if (mEphemeralKeyPair == null) {
            return false;
        }
        // The place to search for X and Y is in the DeviceEngagementBytes which is
        // the first bstr in the SessionTranscript array.
        ByteArrayInputStream bais = new ByteArrayInputStream(sessionTranscript);
        List<DataItem> dataItems = null;
        try {
            dataItems = new CborDecoder(bais).decode();
        } catch (CborException e) {
            Log.e(TAG, "Error parsing SessionTranscript CBOR");
            return false;
        }
        if (dataItems.size() != 1
                || !(dataItems.get(0) instanceof Array)
                || ((Array) dataItems.get(0)).getDataItems().size() != 2) {
            Log.e(TAG, "SessionTranscript is not an array of length 2");
            return false;
        }
        if (!(((Array) dataItems.get(0)).getDataItems().get(0) instanceof ByteString)) {
            Log.e(TAG, "First element of SessionTranscript array is not a bstr");
            return false;
        }
        byte[] deviceEngagementBytes =
                ((ByteString) ((Array) dataItems.get(0)).getDataItems().get(0)).getBytes();

        ECPoint w = ((ECPublicKey) mEphemeralKeyPair.getPublic()).getW();
        // X and Y are always positive so for interop we remove any leading zeroes
        // inserted by the BigInteger encoder.
        byte[] x = Util.stripLeadingZeroes(w.getAffineX().toByteArray());
        byte[] y = Util.stripLeadingZeroes(w.getAffineY().toByteArray());

        if (!Util.hasSubByteArray(deviceEngagementBytes, x)
                && !Util.hasSubByteArray(deviceEngagementBytes, y)) {
            return false;
        }
        return true;
    }


    private boolean mDidUserAuthResult = false;

    private boolean mDidUserAuthAlreadyCalled = false;

    // Returns true if the user authenticated using the cryptoObject we gave the
    // app in the getCryptoObject() method.
    //
    private boolean didUserAuth() {
        if (!mDidUserAuthAlreadyCalled) {
            mDidUserAuthResult = didUserAuthNoCache();
            mDidUserAuthAlreadyCalled = true;
        }
        return mDidUserAuthResult;
    }

    private boolean didUserAuthNoCache() {
        if (mCryptoObject == null) {
            // Certainly didn't auth since they didn't even get a cryptoObject (or no ACPs
            // are using user-auth).
            return false;
        }
        try {
            Cipher cipher = mCryptoObject.getCipher();
            byte[] clearText = new byte[16];
            // We don't care about the cipherText, only whether the key is unlocked.
            cipher.doFinal(clearText);
        } catch (IllegalBlockSizeException
                | BadPaddingException e) {
            // If we get here, it's because the user didn't auth.
            return false;
        }
        return true;
    }


    @NonNull
    @Override
    public ResultData getEntries(
            @Nullable byte[] requestMessage,
            @NonNull java.util.Map<String, Collection<String>> entriesToRequest,
            @Nullable byte[] readerSignature)
            throws NoAuthenticationKeyAvailableException,
            InvalidReaderSignatureException, InvalidRequestMessageException,
            EphemeralPublicKeyNotFoundException {

        if (mSessionTranscript != null && !hasEphemeralKeyInDeviceEngagement(mSessionTranscript)) {
            throw new EphemeralPublicKeyNotFoundException(
                    "Did not find ephemeral public key X and Y coordinates in SessionTranscript "
                            + "(make sure leading zeroes are not used)");
        }

        HashMap<String, Collection<String>> requestMessageMap = parseRequestMessage(requestMessage);

        // Check reader signature, if requested.
        Collection<X509Certificate> readerCertChain = null;
        if (readerSignature != null) {
            if (mSessionTranscript == null) {
                throw new InvalidReaderSignatureException(
                        "readerSignature non-null but sessionTranscript was null");
            }
            if (requestMessage == null) {
                throw new InvalidReaderSignatureException(
                        "readerSignature non-null but requestMessage was null");
            }

            try {
                readerCertChain = Util.coseSign1GetX5Chain(readerSignature);
            } catch (CertificateException e) {
                throw new InvalidReaderSignatureException("Error getting x5chain element", e);
            }
            if (readerCertChain.size() < 1) {
                throw new InvalidReaderSignatureException("No x5chain element in reader signature");
            }
            if (!Util.validateCertificateChain(readerCertChain)) {
                throw new InvalidReaderSignatureException("Error validating certificate chain");
            }
            PublicKey readerTopmostPublicKey = readerCertChain.iterator().next().getPublicKey();

            byte[] readerAuthentication = Util.buildReaderAuthenticationCbor(
                    mSessionTranscript,
                    requestMessage);
            byte[] readerAuthenticationBytes =
                    Util.prependSemanticTagForEncodedCbor(readerAuthentication);
            try {
                if (!Util.coseSign1CheckSignature(
                        readerSignature,
                        readerAuthenticationBytes,
                        readerTopmostPublicKey)) {
                    throw new InvalidReaderSignatureException("Reader signature check failed");
                }
            } catch (NoSuchAlgorithmException | InvalidKeyException e) {
                throw new InvalidReaderSignatureException("Error checking reader signature", e);
            }
        }

        SimpleResultData.Builder resultBuilder = new SimpleResultData.Builder();

        CborBuilder deviceNameSpaceBuilder = new CborBuilder();
        MapBuilder<CborBuilder> deviceNameSpacesMapBuilder = deviceNameSpaceBuilder.addMap();


        retrieveValues(requestMessage,
                requestMessageMap,
                readerCertChain,
                entriesToRequest,
                resultBuilder,
                deviceNameSpacesMapBuilder);

        ByteArrayOutputStream adBaos = new ByteArrayOutputStream();
        CborEncoder adEncoder = new CborEncoder(adBaos);
        DataItem deviceNameSpace = deviceNameSpaceBuilder.build().get(0);
        try {
            adEncoder.encode(deviceNameSpace);
        } catch (CborException e) {
            throw new RuntimeException("Error encoding deviceNameSpace", e);
        }
        byte[] authenticatedData = adBaos.toByteArray();
        resultBuilder.setAuthenticatedData(authenticatedData);

        // If the sessionTranscript is available, create the ECDSA signature
        // so the reader can authenticate the DeviceNamespaces CBOR. Also
        // return the staticAuthenticationData associated with the key chosen
        // to be used for signing.
        //
        // Unfortunately we can't do MACing because Android Keystore doesn't
        // implement ECDH. So we resort to ECSDA signing instead.
        if (mSessionTranscript != null) {
            ensureAuthKey();
            resultBuilder.setStaticAuthenticationData(mAuthKeyAssociatedData);

            byte[] deviceAuthentication = Util.buildDeviceAuthenticationCbor(
                    mData.getDocType(),
                    mSessionTranscript,
                    authenticatedData);

            byte[] deviceAuthenticationBytes =
                    Util.prependSemanticTagForEncodedCbor(deviceAuthentication);

            try {
                Signature authKeySignature = Signature.getInstance("SHA256withECDSA");
                authKeySignature.initSign(mAuthKey);
                resultBuilder.setEcdsaSignature(
                        Util.coseSign1Sign(authKeySignature,
                                null,
                                deviceAuthenticationBytes,
                                null));
            } catch (NoSuchAlgorithmException
                    | InvalidKeyException
                    | CertificateEncodingException e) {
                throw new RuntimeException("Error signing DeviceAuthentication CBOR", e);
            }
        }

        return resultBuilder.build();
    }

    private void retrieveValues(
            byte[] requestMessage,
            HashMap<String, Collection<String>> requestMessageMap,
            Collection<X509Certificate> readerCertChain,
            java.util.Map<String, Collection<String>> entriesToRequest,
            SimpleResultData.Builder resultBuilder,
            MapBuilder<CborBuilder> deviceNameSpacesMapBuilder) {

        for (String namespaceName : entriesToRequest.keySet()) {
            Collection<String> entriesToRequestInNamespace = entriesToRequest.get(namespaceName);

            PersonalizationData.NamespaceData loadedNamespace = mData.lookupNamespaceData(
                    namespaceName);

            Collection<String> requestMessageNamespace = requestMessageMap.get(namespaceName);

            retrieveValuesForNamespace(resultBuilder,
                    deviceNameSpacesMapBuilder,
                    entriesToRequestInNamespace,
                    requestMessage,
                    requestMessageNamespace,
                    readerCertChain,
                    namespaceName,
                    loadedNamespace);
        }
    }

    private void retrieveValuesForNamespace(
            SimpleResultData.Builder resultBuilder,
            MapBuilder<CborBuilder> deviceNameSpacesMapBuilder,
            Collection<String> entriesToRequestInNamespace,
            byte[] requestMessage,
            Collection<String> requestMessageNamespace,
            Collection<X509Certificate> readerCertChain,
            String namespaceName,
            PersonalizationData.NamespaceData loadedNamespace) {
        MapBuilder<MapBuilder<CborBuilder>> deviceNamespaceBuilder = null;

        for (String requestedEntryName : entriesToRequestInNamespace) {

            byte[] value = null;
            if (loadedNamespace != null) {
                value = loadedNamespace.getEntryValue(requestedEntryName);
            }

            if (value == null) {
                resultBuilder.addErrorStatus(namespaceName,
                        requestedEntryName,
                        ResultData.STATUS_NO_SUCH_ENTRY);
                continue;
            }

            if (requestMessage != null) {
                if (requestMessageNamespace == null
                        || !requestMessageNamespace.contains(requestedEntryName)) {
                    resultBuilder.addErrorStatus(namespaceName,
                            requestedEntryName,
                            ResultData.STATUS_NOT_IN_REQUEST_MESSAGE);
                    continue;
                }
            }

            Collection<AccessControlProfileId> accessControlProfileIds =
                    loadedNamespace.getAccessControlProfileIds(requestedEntryName);

            @ResultData.Status
            int status = checkAccess(accessControlProfileIds, readerCertChain);
            if (status != ResultData.STATUS_OK) {
                resultBuilder.addErrorStatus(namespaceName, requestedEntryName, status);
                continue;
            }

            resultBuilder.addEntry(namespaceName, requestedEntryName, value);
            if (deviceNamespaceBuilder == null) {
                deviceNamespaceBuilder = deviceNameSpacesMapBuilder.putMap(
                        namespaceName);
            }
            DataItem dataItem = Util.cborToDataItem(value);
            deviceNamespaceBuilder.put(new UnicodeString(requestedEntryName), dataItem);
        }
    }

    @ResultData.Status
    private int checkAccessSingleProfile(AccessControlProfile profile,
            Collection<X509Certificate> readerCertChain) {

        if (profile.isUserAuthenticationRequired()) {
            if (!mData.checkUserAuthentication(profile.getAccessControlProfileId(),
                    didUserAuth())) {
                return ResultData.STATUS_USER_AUTHENTICATION_FAILED;
            }
        }

        X509Certificate profileCert = profile.getReaderCertificate();
        if (profileCert != null) {
            if (readerCertChain == null) {
                return ResultData.STATUS_READER_AUTHENTICATION_FAILED;
            }

            // Need to check if the cert required by the profile is in the given chain.
            boolean foundMatchingCert = false;
            byte[] profilePublicKeyEncoded = profileCert.getPublicKey().getEncoded();
            for (X509Certificate readerCert : readerCertChain) {
                byte[] readerCertPublicKeyEncoded = readerCert.getPublicKey().getEncoded();
                if (Arrays.equals(profilePublicKeyEncoded, readerCertPublicKeyEncoded)) {
                    foundMatchingCert = true;
                    break;
                }
            }
            if (!foundMatchingCert) {
                return ResultData.STATUS_READER_AUTHENTICATION_FAILED;
            }
        }

        // Neither user auth nor reader auth required. This means access is always granted.
        return ResultData.STATUS_OK;
    }

    @ResultData.Status
    private int checkAccess(Collection<AccessControlProfileId> accessControlProfileIds,
            Collection<X509Certificate> readerCertChain) {
        // Access is granted if at least one of the profiles grants access.
        //
        // If an item is configured without any profiles, access is denied.
        //
        @ResultData.Status int lastStatus = ResultData.STATUS_NO_ACCESS_CONTROL_PROFILES;

        for (AccessControlProfileId id : accessControlProfileIds) {
            AccessControlProfile profile = mData.getAccessControlProfile(id);
            lastStatus = checkAccessSingleProfile(profile, readerCertChain);
            if (lastStatus == ResultData.STATUS_OK) {
                return lastStatus;
            }
        }
        return lastStatus;
    }

    @Override
    public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey) {
        mData.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey);
    }

    @Override
    public @NonNull
    Collection<X509Certificate> getAuthKeysNeedingCertification() {
        return mData.getAuthKeysNeedingCertification();
    }

    @Override
    public void storeStaticAuthenticationData(@NonNull X509Certificate authenticationKey,
            @NonNull byte[] staticAuthData) throws UnknownAuthenticationKeyException {
        mData.storeStaticAuthenticationData(authenticationKey, staticAuthData);
    }

    @Override
    public @NonNull
    int[] getAuthenticationDataUsageCount() {
        return mData.getAuthKeyUseCounts();
    }
}